Merge pull request #2699 from pikasTech/fix/pac-delivery-observation-mode
修复 PaC 配置型 consumer 的发布观测误阻塞
This commit is contained in:
@@ -113,7 +113,11 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
|
|||||||
- 禁止在入口内部调用完整 `status`、同步读取 TaskRun/log、执行 Git fetch 或拼接第二个 history 状态源;
|
- 禁止在入口内部调用完整 `status`、同步读取 TaskRun/log、执行 Git fetch 或拼接第二个 history 状态源;
|
||||||
- 任一批量读取失败时必须保留 typed `readErrors` 和最后成功阶段,不得提高 YAML timeout 或把读取失败投影为对象缺失;
|
- 任一批量读取失败时必须保留 typed `readErrors` 和最后成功阶段,不得提高 YAML timeout 或把读取失败投影为对象缺失;
|
||||||
- 固定 `remoteCaptureCount=1`、`mutation=false`,不得再手工串联 `status`、`history`、`delivery-timing` 或裸 Kubernetes 对象查询;
|
- 固定 `remoteCaptureCount=1`、`mutation=false`,不得再手工串联 `status`、`history`、`delivery-timing` 或裸 Kubernetes 对象查询;
|
||||||
- `complete` 表示 PipelineRun、exact-source runtime 与 Argo 全部就绪;`partial` 保留已成立的 exact-source 证据并明确 Argo 或 runtime 首断点;`blocked` 表示目标读取失败、source 不匹配、PipelineRun 未成功或当前 runtime 未观察到该 source;
|
- runtime 证据是否适用只读 owning YAML 的 `consumers[].deliveryObservation.runtimeEvidence`:
|
||||||
|
- `required` 要求 exact-source workload 存在且 ready;
|
||||||
|
- `not-applicable` 用于只有配置/控制面交付、没有独立 workload 的 consumer,缺少 workload 不得阻塞;
|
||||||
|
- 字段缺失时按非核心配置回退到 `required`,并输出配置路径、回退值和 `blocking=false` warning;
|
||||||
|
- `complete` 表示 PipelineRun、适用的 runtime 证据与 Argo 全部就绪;`partial` 保留已成立的证据并明确 Argo 或适用 runtime 首断点;`blocked` 表示目标读取失败、source 不匹配、PipelineRun 未成功或必需 runtime 未观察到该 source;
|
||||||
- TaskRun artifact 日志默认延后时只披露 `artifactEvidence.deferred=true`,不放宽既有 status evaluator,也不伪造 provenance。
|
- TaskRun artifact 日志默认延后时只披露 `artifactEvidence.deferred=true`,不放宽既有 status evaluator,也不伪造 provenance。
|
||||||
|
|
||||||
- 新增 PaC consumer 的首次引导:
|
- 新增 PaC consumer 的首次引导:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ metadata:
|
|||||||
- 1769
|
- 1769
|
||||||
- 1802
|
- 1802
|
||||||
- 1811
|
- 1811
|
||||||
|
- 2165
|
||||||
- 2274
|
- 2274
|
||||||
- 2628
|
- 2628
|
||||||
defaults:
|
defaults:
|
||||||
@@ -441,6 +442,8 @@ consumers:
|
|||||||
pipelineRunPrefix: unidesk-host-
|
pipelineRunPrefix: unidesk-host-
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: unidesk-host
|
argoApplication: unidesk-host
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: true
|
closeoutGitOpsMirrorFlush: true
|
||||||
closeoutGitOpsMirrorLane: v03
|
closeoutGitOpsMirrorLane: v03
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
@@ -459,6 +462,8 @@ consumers:
|
|||||||
pipelineRunPrefix: platform-infra-gitea-nc01-
|
pipelineRunPrefix: platform-infra-gitea-nc01-
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: platform-infra-gitea-nc01
|
argoApplication: platform-infra-gitea-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: not-applicable
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
- id: platform-infra-sub2rank-nc01
|
- id: platform-infra-sub2rank-nc01
|
||||||
repositoryRef: sub2rank-nc01
|
repositoryRef: sub2rank-nc01
|
||||||
@@ -469,6 +474,8 @@ consumers:
|
|||||||
pipelineRunPrefix: sub2rank-nc01
|
pipelineRunPrefix: sub2rank-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: sub2rank-nc01
|
argoApplication: sub2rank-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: true
|
closeoutGitOpsMirrorFlush: true
|
||||||
closeoutGitOpsMirrorLane: v03
|
closeoutGitOpsMirrorLane: v03
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
@@ -508,6 +515,8 @@ consumers:
|
|||||||
pipelineRunPrefix: sub2rank-development-nc01
|
pipelineRunPrefix: sub2rank-development-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: sub2rank-development-nc01
|
argoApplication: sub2rank-development-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
project: default
|
project: default
|
||||||
@@ -546,6 +555,8 @@ consumers:
|
|||||||
pipelineRunPrefix: hwpod-home-nc01
|
pipelineRunPrefix: hwpod-home-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: hwpod-home-nc01
|
argoApplication: hwpod-home-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: true
|
closeoutGitOpsMirrorFlush: true
|
||||||
closeoutGitOpsMirrorLane: v03
|
closeoutGitOpsMirrorLane: v03
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
@@ -634,6 +645,8 @@ consumers:
|
|||||||
pipelineRunPrefix: selfmedia-nc01
|
pipelineRunPrefix: selfmedia-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: selfmedia-nc01
|
argoApplication: selfmedia-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
project: default
|
project: default
|
||||||
@@ -675,6 +688,8 @@ consumers:
|
|||||||
pipelineRunPrefix: selfmedia-production-nc01
|
pipelineRunPrefix: selfmedia-production-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: selfmedia-production-nc01
|
argoApplication: selfmedia-production-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
params:
|
params:
|
||||||
source_branch: release
|
source_branch: release
|
||||||
@@ -729,6 +744,8 @@ consumers:
|
|||||||
pipelineRunPrefix: pikaoa-dev-nc01
|
pipelineRunPrefix: pikaoa-dev-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: pikaoa-dev-nc01
|
argoApplication: pikaoa-dev-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
argoBootstrap:
|
argoBootstrap:
|
||||||
project: default
|
project: default
|
||||||
@@ -770,6 +787,8 @@ consumers:
|
|||||||
pipelineRunPrefix: pikaoa-nc01
|
pipelineRunPrefix: pikaoa-nc01
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: pikaoa-nc01
|
argoApplication: pikaoa-nc01
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
params:
|
params:
|
||||||
source_branch: release
|
source_branch: release
|
||||||
@@ -911,6 +930,8 @@ templates:
|
|||||||
pipelineRunPrefix: "agentrun-${nodeLower}-v02-ci"
|
pipelineRunPrefix: "agentrun-${nodeLower}-v02-ci"
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: "agentrun-${nodeLower}-v02"
|
argoApplication: "agentrun-${nodeLower}-v02"
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
agentrunRelease:
|
agentrunRelease:
|
||||||
id: "agentrun-${nodeLower}-release"
|
id: "agentrun-${nodeLower}-release"
|
||||||
@@ -922,6 +943,8 @@ templates:
|
|||||||
pipelineRunPrefix: "agentrun-${nodeLower}-release-ci"
|
pipelineRunPrefix: "agentrun-${nodeLower}-release-ci"
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: "agentrun-${nodeLower}-release"
|
argoApplication: "agentrun-${nodeLower}-release"
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: false
|
closeoutGitOpsMirrorFlush: false
|
||||||
params:
|
params:
|
||||||
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
git_read_url: http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-agentrun.git
|
||||||
@@ -944,6 +967,8 @@ templates:
|
|||||||
pipelineRunPrefix: "hwlab-web-probe-sentinel-${nodeLower}"
|
pipelineRunPrefix: "hwlab-web-probe-sentinel-${nodeLower}"
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: "hwlab-web-probe-sentinel-${nodeLower}"
|
argoApplication: "hwlab-web-probe-sentinel-${nodeLower}"
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: true
|
closeoutGitOpsMirrorFlush: true
|
||||||
closeoutGitOpsMirrorLane: v03
|
closeoutGitOpsMirrorLane: v03
|
||||||
hwlabV03:
|
hwlabV03:
|
||||||
@@ -956,4 +981,6 @@ templates:
|
|||||||
pipelineRunPrefix: "hwlab-${nodeLower}-v03-ci-poll"
|
pipelineRunPrefix: "hwlab-${nodeLower}-v03-ci-poll"
|
||||||
argoNamespace: argocd
|
argoNamespace: argocd
|
||||||
argoApplication: hwlab-node-v03
|
argoApplication: hwlab-node-v03
|
||||||
|
deliveryObservation:
|
||||||
|
runtimeEvidence: required
|
||||||
closeoutGitOpsMirrorFlush: true
|
closeoutGitOpsMirrorFlush: true
|
||||||
|
|||||||
+15
@@ -275,6 +275,21 @@ preflight、CI gate 或人工确认门禁。
|
|||||||
- 禁止在 PipelineRun、Pod 或 Job 启动后临时安装 `kubectl`;
|
- 禁止在 PipelineRun、Pod 或 Job 启动后临时安装 `kubectl`;
|
||||||
- 该默认能力只属于 CI tools image,不得隐式扩大为产品业务镜像或长期 controller 的运行时依赖。
|
- 该默认能力只属于 CI tools image,不得隐式扩大为产品业务镜像或长期 controller 的运行时依赖。
|
||||||
|
|
||||||
|
### 6.10 OPS-TARGET-REQ-010 发布观测证据适用性
|
||||||
|
|
||||||
|
| 编号 | 短名 | 主责模块 | 关联模块 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| OPS-TARGET-REQ-010 | 观测证据适用性 | PJ2026-01060308 YAML目标治理 | [发布流水](PJ2026-010601-controlled-release.md)、[运维监控](PJ2026-010605-observability-monitoring.md) |
|
||||||
|
|
||||||
|
PaC consumer 必须通过 owning YAML 的
|
||||||
|
`deliveryObservation.runtimeEvidence` 声明发布观测是否需要 workload 证据。
|
||||||
|
|
||||||
|
- `required` consumer 必须观察到与 source commit 精确匹配且 ready 的 workload;
|
||||||
|
- `not-applicable` 只适用于配置或控制面交付,没有独立业务 workload 的 consumer;
|
||||||
|
- `not-applicable` consumer 仍必须满足 source commit 匹配、PipelineRun 成功和 Argo `Synced/Healthy`;
|
||||||
|
- 非核心字段缺失时可以回退到 `required`,但必须输出配置路径、回退值、原因和 `blocking=false` warning;
|
||||||
|
- 观测模式不得改变 PipelineRun、GitOps、Argo 或 runtime authority,也不得新增第二状态源。
|
||||||
|
|
||||||
## 7. 过程控制
|
## 7. 过程控制
|
||||||
|
|
||||||
本规格的执行 issue 为 [#911](https://github.com/pikasTech/unidesk/issues/911)。源码文件头部应标注 `SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets`;自动生成文件、纯配置、锁文件和无法承载注释头的二进制产物可例外。
|
本规格的执行 issue 为 [#911](https://github.com/pikasTech/unidesk/issues/911)。源码文件头部应标注 `SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets`;自动生成文件、纯配置、锁文件和无法承载注释头的二进制产物可例外。
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ const commit = "b".repeat(40);
|
|||||||
function fixture(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
function fixture(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
target: { id: "NC01" },
|
target: { id: "NC01" },
|
||||||
consumer: { id: "fixture" },
|
consumer: {
|
||||||
|
id: "fixture",
|
||||||
|
deliveryObservation: {
|
||||||
|
runtimeEvidence: "required",
|
||||||
|
configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.fixture.deliveryObservation.runtimeEvidence",
|
||||||
|
defaulted: false,
|
||||||
|
fallbackReason: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
observation: { ok: true },
|
observation: { ok: true },
|
||||||
summary: {
|
summary: {
|
||||||
latestPipelineRun: { name: "fixture-run", status: "True", reason: "Succeeded", sourceCommit: commit },
|
latestPipelineRun: { name: "fixture-run", status: "True", reason: "Succeeded", sourceCommit: commit },
|
||||||
@@ -140,3 +148,38 @@ test("delivery observe preserves a typed snapshot read failure", () => {
|
|||||||
});
|
});
|
||||||
assert.equal((result.readErrors as unknown[]).length, 1);
|
assert.equal((result.readErrors as unknown[]).length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("delivery observe completes a configuration-only consumer without runtime workloads", () => {
|
||||||
|
const status = fixture({ runtime: { workloads: [] } });
|
||||||
|
status.consumer = {
|
||||||
|
id: "fixture",
|
||||||
|
deliveryObservation: {
|
||||||
|
runtimeEvidence: "not-applicable",
|
||||||
|
configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.fixture.deliveryObservation.runtimeEvidence",
|
||||||
|
defaulted: false,
|
||||||
|
fallbackReason: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = projectPacDeliveryObserve({ status, sourceCommit: commit });
|
||||||
|
assert.equal(result.status, "complete");
|
||||||
|
assert.equal((result.runtime as Record<string, unknown>).applicable, false);
|
||||||
|
assert.equal((result.runtime as Record<string, unknown>).ready, null);
|
||||||
|
assert.deepEqual(result.firstBreak, null);
|
||||||
|
assert.deepEqual((result.warnings as Record<string, unknown>[])[0], {
|
||||||
|
code: "runtime-evidence-not-applicable",
|
||||||
|
phase: "runtime",
|
||||||
|
blocking: false,
|
||||||
|
mode: "not-applicable",
|
||||||
|
configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.fixture.deliveryObservation.runtimeEvidence",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delivery observe defaults missing non-core runtime evidence mode without blocking", () => {
|
||||||
|
const status = fixture();
|
||||||
|
status.consumer = { id: "fixture" };
|
||||||
|
const result = projectPacDeliveryObserve({ status, sourceCommit: commit });
|
||||||
|
assert.equal(result.status, "complete");
|
||||||
|
assert.equal((result.runtime as Record<string, unknown>).evidenceMode, "required");
|
||||||
|
assert.equal((result.warnings as Record<string, unknown>[])[0]?.code, "runtime-evidence-mode-defaulted");
|
||||||
|
assert.equal((result.warnings as Record<string, unknown>[])[0]?.blocking, false);
|
||||||
|
});
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Recor
|
|||||||
const runtime = record(summary.runtime);
|
const runtime = record(summary.runtime);
|
||||||
const workloads = arrayRecords(runtime.workloads);
|
const workloads = arrayRecords(runtime.workloads);
|
||||||
const matchingWorkloads = workloads.filter((item) => item.sourceCommit === input.sourceCommit);
|
const matchingWorkloads = workloads.filter((item) => item.sourceCommit === input.sourceCommit);
|
||||||
|
const deliveryObservation = record(record(input.status.consumer).deliveryObservation);
|
||||||
|
const configuredRuntimeEvidence = stringOrNull(deliveryObservation.runtimeEvidence);
|
||||||
|
const runtimeEvidence = configuredRuntimeEvidence === "not-applicable" ? "not-applicable" : "required";
|
||||||
|
const runtimeEvidenceRequired = runtimeEvidence === "required";
|
||||||
|
const observationConfigPath = stringOrNull(deliveryObservation.configPath);
|
||||||
|
const observationModeDefaulted = deliveryObservation.defaulted === true || configuredRuntimeEvidence === null;
|
||||||
const pipelineSourceCommit = stringOrNull(pipeline.sourceCommit);
|
const pipelineSourceCommit = stringOrNull(pipeline.sourceCommit);
|
||||||
const sourceMatched = pipelineSourceCommit === input.sourceCommit;
|
const sourceMatched = pipelineSourceCommit === input.sourceCommit;
|
||||||
const pipelineSucceeded = pipeline.status === "True";
|
const pipelineSucceeded = pipeline.status === "True";
|
||||||
@@ -55,15 +61,16 @@ export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Recor
|
|||||||
const argoReady = argo.sync === "Synced" && argo.health === "Healthy";
|
const argoReady = argo.sync === "Synced" && argo.health === "Healthy";
|
||||||
const observationReady = record(input.status.observation).ok === true;
|
const observationReady = record(input.status.observation).ok === true;
|
||||||
const readErrors = arrayRecords(input.status.readErrors);
|
const readErrors = arrayRecords(input.status.readErrors);
|
||||||
const state = !observationReady || !sourceMatched || !pipelineSucceeded || matchingWorkloads.length === 0
|
const state = !observationReady || !sourceMatched || !pipelineSucceeded || (runtimeEvidenceRequired && matchingWorkloads.length === 0)
|
||||||
? "blocked"
|
? "blocked"
|
||||||
: runtimeReady && argoReady
|
: (!runtimeEvidenceRequired || runtimeReady) && argoReady
|
||||||
? "complete"
|
? "complete"
|
||||||
: "partial";
|
: "partial";
|
||||||
const firstBreak = firstBreakFor({
|
const firstBreak = firstBreakFor({
|
||||||
observationReady,
|
observationReady,
|
||||||
sourceMatched,
|
sourceMatched,
|
||||||
pipelineSucceeded,
|
pipelineSucceeded,
|
||||||
|
runtimeEvidenceRequired,
|
||||||
matchingWorkloads,
|
matchingWorkloads,
|
||||||
runtimeReady,
|
runtimeReady,
|
||||||
argoReady,
|
argoReady,
|
||||||
@@ -72,6 +79,24 @@ export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Recor
|
|||||||
});
|
});
|
||||||
|
|
||||||
const artifactMode = record(record(summary.artifact).collector).mode ?? null;
|
const artifactMode = record(record(summary.artifact).collector).mode ?? null;
|
||||||
|
const warnings = runtimeEvidence === "not-applicable"
|
||||||
|
? [{
|
||||||
|
code: "runtime-evidence-not-applicable",
|
||||||
|
phase: "runtime",
|
||||||
|
blocking: false,
|
||||||
|
mode: runtimeEvidence,
|
||||||
|
configPath: observationConfigPath,
|
||||||
|
}]
|
||||||
|
: !observationModeDefaulted
|
||||||
|
? []
|
||||||
|
: [{
|
||||||
|
code: "runtime-evidence-mode-defaulted",
|
||||||
|
phase: "runtime",
|
||||||
|
blocking: false,
|
||||||
|
mode: runtimeEvidence,
|
||||||
|
configPath: observationConfigPath,
|
||||||
|
reason: stringOrNull(deliveryObservation.fallbackReason),
|
||||||
|
}];
|
||||||
return {
|
return {
|
||||||
ok: state !== "blocked",
|
ok: state !== "blocked",
|
||||||
status: state,
|
status: state,
|
||||||
@@ -101,9 +126,11 @@ export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Recor
|
|||||||
firstCondition: argo.firstCondition ?? null,
|
firstCondition: argo.firstCondition ?? null,
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
|
evidenceMode: runtimeEvidence,
|
||||||
|
applicable: runtimeEvidenceRequired,
|
||||||
workloadCount: workloads.length,
|
workloadCount: workloads.length,
|
||||||
matchingWorkloadCount: matchingWorkloads.length,
|
matchingWorkloadCount: matchingWorkloads.length,
|
||||||
ready: runtimeReady,
|
ready: runtimeEvidenceRequired ? runtimeReady : null,
|
||||||
workloads: matchingWorkloads.map((item) => ({
|
workloads: matchingWorkloads.map((item) => ({
|
||||||
deployment: item.deployment ?? null,
|
deployment: item.deployment ?? null,
|
||||||
namespace: item.namespace ?? null,
|
namespace: item.namespace ?? null,
|
||||||
@@ -118,6 +145,7 @@ export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Recor
|
|||||||
deferred: artifactMode === "taskrun-status-only" || artifactMode === "deferred-single-snapshot",
|
deferred: artifactMode === "taskrun-status-only" || artifactMode === "deferred-single-snapshot",
|
||||||
},
|
},
|
||||||
firstBreak,
|
firstBreak,
|
||||||
|
warnings,
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -136,7 +164,7 @@ export function renderPacDeliveryObserve(result: Record<string, unknown>): strin
|
|||||||
`SOURCE expected=${short(text(result.sourceCommit))} observed=${short(text(pipeline.sourceCommit))} matched=${String(pipeline.sourceMatched === true)}`,
|
`SOURCE expected=${short(text(result.sourceCommit))} observed=${short(text(pipeline.sourceCommit))} matched=${String(pipeline.sourceMatched === true)}`,
|
||||||
`PIPELINE id=${text(pipeline.id)} status=${text(pipeline.status)} reason=${text(pipeline.reason)}`,
|
`PIPELINE id=${text(pipeline.id)} status=${text(pipeline.status)} reason=${text(pipeline.reason)}`,
|
||||||
`ARGO sync=${text(argo.sync)} health=${text(argo.health)} revision=${short(text(argo.revision))}`,
|
`ARGO sync=${text(argo.sync)} health=${text(argo.health)} revision=${short(text(argo.revision))}`,
|
||||||
`RUNTIME matching=${text(runtime.matchingWorkloadCount)}/${text(runtime.workloadCount)} ready=${String(runtime.ready === true)}`,
|
`RUNTIME mode=${text(runtime.evidenceMode)} matching=${text(runtime.matchingWorkloadCount)}/${text(runtime.workloadCount)} ready=${runtime.applicable === true ? String(runtime.ready === true) : "n/a"}`,
|
||||||
...workloads.map((item) => ` ${text(item.namespace)}/${text(item.deployment)} ${text(item.readyReplicas)}/${text(item.replicas)} digest=${short(text(item.digest), 24)}`),
|
...workloads.map((item) => ` ${text(item.namespace)}/${text(item.deployment)} ${text(item.readyReplicas)}/${text(item.replicas)} digest=${short(text(item.digest), 24)}`),
|
||||||
`FIRST_BREAK ${Object.keys(firstBreak).length === 0 ? "-" : `${text(firstBreak.code)} phase=${text(firstBreak.phase)}`}`,
|
`FIRST_BREAK ${Object.keys(firstBreak).length === 0 ? "-" : `${text(firstBreak.code)} phase=${text(firstBreak.phase)}`}`,
|
||||||
"REMOTE_CAPTURE_COUNT 1",
|
"REMOTE_CAPTURE_COUNT 1",
|
||||||
@@ -147,6 +175,7 @@ function firstBreakFor(input: {
|
|||||||
observationReady: boolean;
|
observationReady: boolean;
|
||||||
sourceMatched: boolean;
|
sourceMatched: boolean;
|
||||||
pipelineSucceeded: boolean;
|
pipelineSucceeded: boolean;
|
||||||
|
runtimeEvidenceRequired: boolean;
|
||||||
matchingWorkloads: readonly Record<string, unknown>[];
|
matchingWorkloads: readonly Record<string, unknown>[];
|
||||||
runtimeReady: boolean;
|
runtimeReady: boolean;
|
||||||
argoReady: boolean;
|
argoReady: boolean;
|
||||||
@@ -165,8 +194,8 @@ function firstBreakFor(input: {
|
|||||||
}
|
}
|
||||||
if (!input.sourceMatched) return { code: "pipeline-source-commit-mismatch", phase: "pipeline" };
|
if (!input.sourceMatched) return { code: "pipeline-source-commit-mismatch", phase: "pipeline" };
|
||||||
if (!input.pipelineSucceeded) return { code: "pipeline-not-succeeded", phase: "pipeline" };
|
if (!input.pipelineSucceeded) return { code: "pipeline-not-succeeded", phase: "pipeline" };
|
||||||
if (input.matchingWorkloads.length === 0) return { code: "runtime-source-commit-not-observed", phase: "runtime" };
|
if (input.runtimeEvidenceRequired && input.matchingWorkloads.length === 0) return { code: "runtime-source-commit-not-observed", phase: "runtime" };
|
||||||
if (!input.runtimeReady) return { code: "runtime-workload-not-ready", phase: "runtime" };
|
if (input.runtimeEvidenceRequired && !input.runtimeReady) return { code: "runtime-workload-not-ready", phase: "runtime" };
|
||||||
if (!input.argoReady) {
|
if (!input.argoReady) {
|
||||||
return {
|
return {
|
||||||
code: "argo-not-ready",
|
code: "argo-not-ready",
|
||||||
|
|||||||
@@ -186,12 +186,14 @@ export interface PacConfigValidationWarning {
|
|||||||
| "pac-unselected-consumer-parse-invalid"
|
| "pac-unselected-consumer-parse-invalid"
|
||||||
| "pac-unselected-consumer-invalid"
|
| "pac-unselected-consumer-invalid"
|
||||||
| "pac-unselected-repository-parse-invalid"
|
| "pac-unselected-repository-parse-invalid"
|
||||||
| "pac-unselected-repository-invalid";
|
| "pac-unselected-repository-invalid"
|
||||||
|
| "pac-consumer-delivery-observation-defaulted";
|
||||||
readonly object: { readonly kind: "consumer" | "repository"; readonly id: string };
|
readonly object: { readonly kind: "consumer" | "repository"; readonly id: string };
|
||||||
readonly consumer: string | null;
|
readonly consumer: string | null;
|
||||||
readonly configPath: string;
|
readonly configPath: string;
|
||||||
readonly blocking: false;
|
readonly blocking: false;
|
||||||
readonly evidence: string;
|
readonly evidence: string;
|
||||||
|
readonly fallback?: { readonly value: string; readonly reason: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PacRepository {
|
interface PacRepository {
|
||||||
@@ -219,6 +221,12 @@ interface PacConsumer {
|
|||||||
pipelineRunPrefix: string;
|
pipelineRunPrefix: string;
|
||||||
argoNamespace: string;
|
argoNamespace: string;
|
||||||
argoApplication: string;
|
argoApplication: string;
|
||||||
|
deliveryObservation: {
|
||||||
|
runtimeEvidence: "required" | "not-applicable";
|
||||||
|
configPath: string;
|
||||||
|
defaulted: boolean;
|
||||||
|
fallbackReason: string | null;
|
||||||
|
};
|
||||||
argoBootstrap: {
|
argoBootstrap: {
|
||||||
project: string;
|
project: string;
|
||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
@@ -819,7 +827,22 @@ function parsePacConsumers(
|
|||||||
const id = rawPacObjectId(entry.value, entry.path, "consumer");
|
const id = rawPacObjectId(entry.value, entry.path, "consumer");
|
||||||
try {
|
try {
|
||||||
if (!isRecord(entry.value)) throw new Error(`${configLabel}.${entry.path} must be an object`);
|
if (!isRecord(entry.value)) throw new Error(`${configLabel}.${entry.path} must be an object`);
|
||||||
values.push(parseConsumer(entry.value, entry.path, defaultRepositoryRef));
|
const consumer = parseConsumer(entry.value, entry.path, defaultRepositoryRef);
|
||||||
|
values.push(consumer);
|
||||||
|
if (consumer.deliveryObservation.defaulted) {
|
||||||
|
warnings.push({
|
||||||
|
code: "pac-consumer-delivery-observation-defaulted",
|
||||||
|
object: { kind: "consumer", id: consumer.id },
|
||||||
|
consumer: consumer.id,
|
||||||
|
configPath: consumer.deliveryObservation.configPath,
|
||||||
|
blocking: false,
|
||||||
|
evidence: pacValueEvidence(consumer.deliveryObservation.fallbackReason ?? "deliveryObservation.runtimeEvidence defaulted"),
|
||||||
|
fallback: {
|
||||||
|
value: consumer.deliveryObservation.runtimeEvidence,
|
||||||
|
reason: consumer.deliveryObservation.fallbackReason ?? "non-core delivery observation mode is not configured",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (selectedConsumerIds.has(id.toLowerCase())) throw error;
|
if (selectedConsumerIds.has(id.toLowerCase())) throw error;
|
||||||
warnings.push(pacConfigWarning("pac-unselected-consumer-parse-invalid", "consumer", id, entry.path, error));
|
warnings.push(pacConfigWarning("pac-unselected-consumer-parse-invalid", "consumer", id, entry.path, error));
|
||||||
@@ -869,6 +892,7 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
|
|||||||
const argoBootstrap = consumer.argoBootstrap === undefined ? null : y.objectField(consumer, "argoBootstrap", path);
|
const argoBootstrap = consumer.argoBootstrap === undefined ? null : y.objectField(consumer, "argoBootstrap", path);
|
||||||
const deliveryProvenance = consumer.deliveryProvenance === undefined ? null : parseConsumerDeliveryProvenance(y.objectField(consumer, "deliveryProvenance", path), `${path}.deliveryProvenance`);
|
const deliveryProvenance = consumer.deliveryProvenance === undefined ? null : parseConsumerDeliveryProvenance(y.objectField(consumer, "deliveryProvenance", path), `${path}.deliveryProvenance`);
|
||||||
const runnerServiceAccount = consumer.runnerServiceAccount === undefined ? null : y.objectField(consumer, "runnerServiceAccount", path);
|
const runnerServiceAccount = consumer.runnerServiceAccount === undefined ? null : y.objectField(consumer, "runnerServiceAccount", path);
|
||||||
|
const deliveryObservation = parseConsumerDeliveryObservation(consumer.deliveryObservation, path);
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
node: y.stringField(consumer, "node", path),
|
node: y.stringField(consumer, "node", path),
|
||||||
@@ -878,6 +902,7 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
|
|||||||
pipelineRunPrefix: y.stringField(consumer, "pipelineRunPrefix", path),
|
pipelineRunPrefix: y.stringField(consumer, "pipelineRunPrefix", path),
|
||||||
argoNamespace: y.kubernetesNameField(consumer, "argoNamespace", path),
|
argoNamespace: y.kubernetesNameField(consumer, "argoNamespace", path),
|
||||||
argoApplication: y.kubernetesNameField(consumer, "argoApplication", path),
|
argoApplication: y.kubernetesNameField(consumer, "argoApplication", path),
|
||||||
|
deliveryObservation,
|
||||||
argoBootstrap: argoBootstrap === null ? null : {
|
argoBootstrap: argoBootstrap === null ? null : {
|
||||||
project: y.kubernetesNameField(argoBootstrap, "project", `${path}.argoBootstrap`),
|
project: y.kubernetesNameField(argoBootstrap, "project", `${path}.argoBootstrap`),
|
||||||
repoUrl: urlField(argoBootstrap, "repoUrl", `${path}.argoBootstrap`),
|
repoUrl: urlField(argoBootstrap, "repoUrl", `${path}.argoBootstrap`),
|
||||||
@@ -907,6 +932,34 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseConsumerDeliveryObservation(value: unknown, path: string): PacConsumer["deliveryObservation"] {
|
||||||
|
const configPath = `${configLabel}#${path}.deliveryObservation.runtimeEvidence`;
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return {
|
||||||
|
runtimeEvidence: "required",
|
||||||
|
configPath,
|
||||||
|
defaulted: true,
|
||||||
|
fallbackReason: value === undefined
|
||||||
|
? "deliveryObservation.runtimeEvidence is missing"
|
||||||
|
: "deliveryObservation must be an object",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (value.runtimeEvidence !== "required" && value.runtimeEvidence !== "not-applicable") {
|
||||||
|
return {
|
||||||
|
runtimeEvidence: "required",
|
||||||
|
configPath,
|
||||||
|
defaulted: true,
|
||||||
|
fallbackReason: "deliveryObservation.runtimeEvidence must be required or not-applicable",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
runtimeEvidence: value.runtimeEvidence,
|
||||||
|
configPath,
|
||||||
|
defaulted: false,
|
||||||
|
fallbackReason: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseFalse(record: Record<string, unknown>, key: string, path: string): false {
|
function parseFalse(record: Record<string, unknown>, key: string, path: string): false {
|
||||||
if (y.booleanField(record, key, path) !== false) throw new Error(`${path}.${key} must be false`);
|
if (y.booleanField(record, key, path) !== false) throw new Error(`${path}.${key} must be false`);
|
||||||
return false;
|
return false;
|
||||||
@@ -2594,6 +2647,7 @@ function consumerObservationSummary(consumer: PacConsumer): Record<string, unkno
|
|||||||
pipelineRunPrefix: consumer.pipelineRunPrefix,
|
pipelineRunPrefix: consumer.pipelineRunPrefix,
|
||||||
argoNamespace: consumer.argoNamespace,
|
argoNamespace: consumer.argoNamespace,
|
||||||
argoApplication: consumer.argoApplication,
|
argoApplication: consumer.argoApplication,
|
||||||
|
deliveryObservation: consumer.deliveryObservation,
|
||||||
repositoryRef: consumer.repositoryRef,
|
repositoryRef: consumer.repositoryRef,
|
||||||
params: Object.keys(consumer.params).sort(),
|
params: Object.keys(consumer.params).sort(),
|
||||||
runnerServiceAccount: consumer.runnerServiceAccount,
|
runnerServiceAccount: consumer.runnerServiceAccount,
|
||||||
@@ -2652,6 +2706,7 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
|
|||||||
pipelineRunPrefix: consumer.pipelineRunPrefix,
|
pipelineRunPrefix: consumer.pipelineRunPrefix,
|
||||||
argoNamespace: consumer.argoNamespace,
|
argoNamespace: consumer.argoNamespace,
|
||||||
argoApplication: consumer.argoApplication,
|
argoApplication: consumer.argoApplication,
|
||||||
|
deliveryObservation: consumer.deliveryObservation,
|
||||||
repositoryRef: consumer.repositoryRef,
|
repositoryRef: consumer.repositoryRef,
|
||||||
params: consumer.params,
|
params: consumer.params,
|
||||||
runnerServiceAccount: consumer.runnerServiceAccount,
|
runnerServiceAccount: consumer.runnerServiceAccount,
|
||||||
@@ -2667,6 +2722,7 @@ function compactPlanJson(result: Record<string, unknown>): Record<string, unknow
|
|||||||
|
|
||||||
function compactStatusJson(result: Record<string, unknown>): Record<string, unknown> {
|
function compactStatusJson(result: Record<string, unknown>): Record<string, unknown> {
|
||||||
const consumer = record(result.consumer);
|
const consumer = record(result.consumer);
|
||||||
|
const deliveryObservation = record(consumer.deliveryObservation);
|
||||||
return {
|
return {
|
||||||
ok: result.ok === true,
|
ok: result.ok === true,
|
||||||
action: result.action,
|
action: result.action,
|
||||||
@@ -2679,6 +2735,7 @@ function compactStatusJson(result: Record<string, unknown>): Record<string, unkn
|
|||||||
namespace: consumer.namespace,
|
namespace: consumer.namespace,
|
||||||
pipeline: consumer.pipeline,
|
pipeline: consumer.pipeline,
|
||||||
argoApplication: consumer.argoApplication,
|
argoApplication: consumer.argoApplication,
|
||||||
|
deliveryObservation,
|
||||||
},
|
},
|
||||||
deliveryAuthority: result.deliveryAuthority,
|
deliveryAuthority: result.deliveryAuthority,
|
||||||
warnings: result.warnings,
|
warnings: result.warnings,
|
||||||
@@ -2919,6 +2976,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
|||||||
const config = record(result.config);
|
const config = record(result.config);
|
||||||
const repository = record(result.repository);
|
const repository = record(result.repository);
|
||||||
const consumer = record(result.consumer);
|
const consumer = record(result.consumer);
|
||||||
|
const deliveryObservation = record(consumer.deliveryObservation);
|
||||||
const internalPublish = record(record(config.capabilities).sentinelInternalPublish);
|
const internalPublish = record(record(config.capabilities).sentinelInternalPublish);
|
||||||
const secrets = arrayRecords(result.secrets);
|
const secrets = arrayRecords(result.secrets);
|
||||||
const policy = arrayRecords(result.policy);
|
const policy = arrayRecords(result.policy);
|
||||||
@@ -2933,6 +2991,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
|||||||
["trigger", "Pipelines-as-Code", "Gitea push webhook"],
|
["trigger", "Pipelines-as-Code", "Gitea push webhook"],
|
||||||
["ci", "Tekton", stringValue(consumer.pipeline)],
|
["ci", "Tekton", stringValue(consumer.pipeline)],
|
||||||
["cd", "Argo", stringValue(consumer.argoApplication)],
|
["cd", "Argo", stringValue(consumer.argoApplication)],
|
||||||
|
["runtime evidence", "owning YAML", `${stringValue(deliveryObservation.runtimeEvidence)} (${stringValue(deliveryObservation.configPath)})`],
|
||||||
]),
|
]),
|
||||||
"",
|
"",
|
||||||
"SECRETS",
|
"SECRETS",
|
||||||
|
|||||||
Reference in New Issue
Block a user