Merge pull request #1732 from pikasTech/fix/1729-pac-reused-delivery-results
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 修复 PaC reused delivery 终态证据
This commit is contained in:
Lyon
2026-07-11 07:32:49 +08:00
committed by GitHub
5 changed files with 665 additions and 139 deletions
+6
View File
@@ -26,6 +26,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --con
bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limit 10
bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>
bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer>
bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> --id <pipelinerun>
```
节点级只读状态必须优先用 `cicd status --node <NODE>`。它从 `config/platform-infra/pipelines-as-code.yaml` 找到该 node 的所有当前 PaC consumer,一次性汇总 PipelineRun、Argo/GitOps、runtime readiness 和诊断;不要再靠阅读源码或手动拼三条 consumer 命令来回答 “NC01 的 CI/CD 流水线情况”。`platform-infra pipelines-as-code status --target <NODE> --consumer <id>` 只作为单 consumer drill-down。
@@ -54,6 +55,11 @@ bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --c
- 仍要求 PipelineRun 成功、原 Argo `Synced/Healthy`、runtime ready、sha256 digest pin 与 health ready
- 证据缺失、冲突或 source commit 不匹配必须 fail-closed,不得等价为 delivery disabled,也不得伪造新 artifact、GitOps revision 或 runtime source commit。
- PaC `status|closeout|history``--json` 是有界 machine contract`--full` 展开单条详情,`--raw` 才展开目标侧原始 payload。`history --id --full` 不得重复嵌套同一 rows/consumer payload或依赖 `/tmp/unidesk-cli-output` 才可读。
- PaC `debug-step --id <pipelinerun>` 必须读取目标侧真实 PipelineRun、TaskRun condition/result 与合同 Task 日志:
- 默认输出每个 terminal role 的 presence、status、source 与首个断点;
- 同时披露 artifact catalog、digest、GitOps commit 和 collector 读错;
- 该入口只读且 `mutation=false`,不能创建 PipelineRun 或写 GitOps/runtime
- 未带 `--id` 时才只运行 evaluator fixture。
- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRefbranch-follower controller 读 `config/cicd-branch-followers.yaml#controller.source.githubSsh`runtime legacy git-mirror 读 owning lane/control-plane YAML 的 host proxy 和 `githubTransport`Gitea/PaC 迁移 lane 读 `config/platform-infra/gitea.yaml``config/platform-infra/pipelines-as-code.yaml`;禁止依赖未声明 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
- Gitea/PaC lane 的 GitHub -> Gitea 自动同步必须用 `platform-infra gitea mirror webhook apply|status|test`。该路径是单向同步,只更新 Gitea branch 和 immutable snapshot ref;不得引入 Gitea -> GitHub 回写、轮询 fallback、Gitea Actions/act_runner fallback 或第二套状态存储。
- Gitea webhook/FRPC/Caddy 变更的 closeout 必须看受控 CLI 的 rollout/status/test 证据;Secret 或 ConfigMap apply 成功不等于 connector Pod 已加载新 proxy/path。
+317 -21
View File
@@ -23,12 +23,85 @@ function exactSkipMarker(records, event) {
&& item.reason === "no-build-no-rollout-plan") || null;
}
const pacContractTasks = new Set(["plan-artifacts", "collect-artifacts", "gitops-promote"]);
function taskTerminalRecord(value) {
const taskRun = record(value);
const metadata = record(taskRun.metadata);
const labels = record(metadata.labels);
const pipelineTask = stringOrNull(labels["tekton.dev/pipelineTask"]);
if (pipelineTask === null || !pacContractTasks.has(pipelineTask)) return null;
const status = record(taskRun.status);
const condition = Array.isArray(status.conditions)
? status.conditions.map(record).find((item) => item.type === "Succeeded") || {}
: {};
const conditionStatus = stringOrNull(condition.status);
const normalizedStatus = conditionStatus === "True"
? "succeeded"
: conditionStatus === "False"
? "failed"
: "running";
return {
event: "pac-task-terminal",
pipelineTask,
taskRun: stringOrNull(metadata.name),
status: normalizedStatus,
reason: stringOrNull(condition.reason),
results: Array.isArray(status.results)
? status.results.map(record).map((item) => stringOrNull(item.name)).filter(Boolean)
: [],
};
}
function parsePacLogRecords(value) {
const records = [];
const lines = String(value || "").split(/\r?\n/u);
let pending = "";
for (const line of lines) {
const trimmed = line.trim();
if (pending.length === 0) {
if (!trimmed.startsWith("{")) continue;
pending = trimmed;
} else {
pending += `\n${line}`;
}
try {
records.push(JSON.parse(pending));
pending = "";
} catch {
if (pending.length > 2 * 1024 * 1024) pending = "";
}
}
return records;
}
function exactTaskTerminal(records, pipelineTask) {
return [...records].reverse().find((item) => item.event === "pac-task-terminal"
&& item.pipelineTask === pipelineTask) || null;
}
function terminalSource(task, marker, markerStatus = "skipped", markerSource = "structured-log-marker") {
return {
present: task !== null || marker !== null,
status: task?.status || (marker === null ? "missing" : markerStatus),
source: task !== null ? "taskrun-condition" : marker === null ? null : markerSource,
taskRun: task?.taskRun || null,
reason: task?.reason || marker?.reason || null,
results: Array.isArray(task?.results) ? task.results : [],
markerPresent: marker !== null,
markerSource: marker === null ? null : markerSource,
};
}
function extractPacSourceObservation(recordsInput) {
const records = Array.isArray(recordsInput) ? recordsInput.map(record) : [];
const plan = [...records].reverse().find((item) => item.event === "g14-ci-plan") || null;
const collectMarker = exactSkipMarker(records, "collect-artifacts");
const promoteMarker = exactSkipMarker(records, "gitops-promote");
if (plan === null && collectMarker === null && promoteMarker === null) return null;
const planTask = exactTaskTerminal(records, "plan-artifacts");
const collectTask = exactTaskTerminal(records, "collect-artifacts");
const promoteTask = exactTaskTerminal(records, "gitops-promote");
if (plan === null && collectMarker === null && promoteMarker === null && planTask === null && collectTask === null && promoteTask === null) return null;
const affected = stringArray(plan?.affectedServices);
const rollout = stringArray(plan?.rolloutServices);
@@ -44,6 +117,12 @@ function extractPacSourceObservation(recordsInput) {
const noRuntimeChangeMarkers = collectMarker !== null && promoteMarker !== null;
const skipMarkerSeen = collectMarker !== null || promoteMarker !== null;
const skipMarkerConflict = skipMarkerSeen && !noRuntimeChangePlan;
const deliveryTerminalReady = planTask?.status === "succeeded"
&& collectTask?.status === "succeeded"
&& promoteTask?.status === "succeeded";
const deliveryTerminalFailed = planTask?.status === "failed"
|| collectTask?.status === "failed"
|| promoteTask?.status === "failed";
let mode = "inconsistent";
let valid = false;
@@ -52,7 +131,7 @@ function extractPacSourceObservation(recordsInput) {
mode = "no-runtime-change";
valid = true;
reason = "no-build-no-rollout-plan";
} else if (planShapeValid && sourceCommitValid && !noRuntimeChangePlan && !skipMarkerConflict) {
} else if (planShapeValid && sourceCommitValid && !noRuntimeChangePlan && !skipMarkerConflict && deliveryTerminalReady) {
mode = "delivery";
valid = true;
reason = "artifact-or-runtime-delivery-required";
@@ -62,6 +141,10 @@ function extractPacSourceObservation(recordsInput) {
reason = "delivery-plan-shape-invalid-or-missing";
} else if (skipMarkerConflict) {
reason = "no-runtime-change-marker-conflicts-with-delivery-plan";
} else if (deliveryTerminalFailed) {
reason = "delivery-terminal-failed";
} else if (!noRuntimeChangePlan) {
reason = "delivery-terminal-evidence-incomplete";
}
return {
@@ -78,9 +161,134 @@ function extractPacSourceObservation(recordsInput) {
reusedServiceCount: reused?.length ?? null,
},
terminal: {
collectArtifacts: collectMarker === null ? "missing" : "no-build-no-rollout-plan",
gitopsPromote: promoteMarker === null ? "missing" : "no-build-no-rollout-plan",
planArtifacts: planTask?.status || (plan === null ? "missing" : "observed"),
collectArtifacts: collectMarker !== null ? "no-build-no-rollout-plan" : collectTask?.status || "missing",
gitopsPromote: promoteMarker !== null ? "no-build-no-rollout-plan" : promoteTask?.status || "missing",
},
terminalSources: {
planArtifacts: terminalSource(planTask, plan, "observed", "g14-ci-plan-structured-log"),
collectArtifacts: terminalSource(collectTask, collectMarker),
gitopsPromote: terminalSource(promoteTask, promoteMarker),
},
valuesPrinted: false,
};
}
function digestOf(item) {
const digest = stringOrNull(item.digest);
if (digest !== null) return digest;
const digestRef = stringOrNull(item.digestRef);
return digestRef !== null && digestRef.includes("@") ? digestRef.split("@").slice(1).join("@") : null;
}
function extractPacArtifactEvidence(recordsInput, logText) {
const records = Array.isArray(recordsInput) ? recordsInput.map(record) : [];
const sourceObservation = extractPacSourceObservation(records);
const catalog = [...records].reverse().find((item) => item.status === "published"
&& Array.isArray(item.services)
&& typeof item.registryVerified === "boolean") || null;
const publish = [...records].reverse().find((item) => item.phase === "gitops-publish" || stringOrNull(item.gitopsCommit) !== null) || null;
const image = publish || [...records].reverse().find((item) => stringOrNull(item.imageStatus) !== null
|| item.status === "reused"
|| item.status === "built") || null;
const logLines = String(logText || "").split(/\r?\n/u);
const gitopsCommit = stringOrNull(image?.gitopsCommit)
|| [...logLines].reverse().map((line) => line.match(/^\[[^\]]+\s+([0-9a-f]{7,64})\]/u)?.[1] || null).find(Boolean)
|| null;
const catalogDigests = catalog === null
? []
: [...new Set(catalog.services.map(record).map(digestOf).filter((value) => /^sha256:[0-9a-f]{64}$/iu.test(value || "")))];
const loggedDigests = [...new Set(logLines.flatMap((line) => [...line.matchAll(/"(?:digest|environmentDigest)"\s*:\s*"(sha256:[0-9a-f]{64})"/gu)].map((match) => match[1])))];
const digests = catalogDigests.length > 0 ? catalogDigests : loggedDigests;
const envHeaderIndex = logLines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
let humanEnv = null;
if (envHeaderIndex >= 0) {
const row = (logLines.slice(envHeaderIndex + 1).find((line) => line.trim()) || "").trim().split(/\s+/u);
if (row.length >= 5) humanEnv = { envReuse: row[0], nodeDeps: row[1], buildPackage: row[2], buildNetwork: row[3], cache: row[4] };
}
if (catalog !== null) {
const reusedCount = Number.isInteger(catalog.reusedCount) ? catalog.reusedCount : null;
const publishedCount = Number.isInteger(catalog.publishedCount) ? catalog.publishedCount : null;
return {
imageStatus: reusedCount !== null && reusedCount > 0 && publishedCount === 0 ? "reused" : "published",
envIdentity: null,
envReuse: reusedCount !== null && reusedCount > 0 ? "hit" : null,
nodeDepsReuse: null,
buildCache: null,
digest: digests.length === 1 ? digests[0] : null,
digests,
gitopsCommit,
sourceCommit: stringOrNull(catalog.sourceCommitId) || sourceObservation?.sourceCommit || null,
runtimeSourceCommit: null,
action: "artifact-catalog-published",
reason: reusedCount !== null && reusedCount > 0 ? "reused-env-catalog" : "artifact-catalog",
catalog: {
present: true,
status: stringOrNull(catalog.status),
sourceCommit: stringOrNull(catalog.sourceCommitId),
registryVerified: catalog.registryVerified === true,
publishedCount,
reusedCount,
requiredServiceCount: Number.isInteger(catalog.requiredServiceCount) ? catalog.requiredServiceCount : null,
digestCount: digests.length,
source: "collect-artifacts-structured-log",
valuesPrinted: false,
},
sourceObservation,
valuesPrinted: false,
};
}
if (image !== null) {
const env = record(image.envReuse);
return {
imageStatus: stringOrNull(image.imageStatus) || stringOrNull(image.status) || (stringOrNull(image.digestRef) !== null ? "built" : null),
envIdentity: stringOrNull(image.envIdentity),
envReuse: stringOrNull(env.dependencyReuse) || stringOrNull(env.mode) || stringOrNull(image.envReuseStatus),
nodeDepsReuse: null,
buildCache: null,
digest: digestOf(image) || (digests.length === 1 ? digests[0] : null),
digests,
gitopsCommit,
sourceCommit: stringOrNull(image.sourceCommit) || sourceObservation?.sourceCommit || null,
runtimeSourceCommit: stringOrNull(image.runtimeSourceCommit),
baselineSourceCommit: stringOrNull(image.baselineSourceCommit),
action: stringOrNull(image.action),
reason: stringOrNull(image.reason),
sourceObservation,
valuesPrinted: false,
};
}
if (sourceObservation !== null) {
return {
imageStatus: sourceObservation.mode === "no-runtime-change" && sourceObservation.valid === true ? "retained" : null,
envIdentity: null,
envReuse: null,
nodeDepsReuse: null,
buildCache: null,
digest: null,
digests: [],
gitopsCommit,
sourceCommit: sourceObservation.sourceCommit || null,
runtimeSourceCommit: null,
action: sourceObservation.mode,
reason: sourceObservation.reason,
sourceObservation,
valuesPrinted: false,
};
}
return {
imageStatus: humanEnv !== null ? "published" : null,
envIdentity: null,
envReuse: humanEnv?.envReuse || null,
nodeDepsReuse: humanEnv?.nodeDeps || null,
buildCache: humanEnv?.cache || null,
buildPackage: humanEnv?.buildPackage || null,
buildNetwork: humanEnv?.buildNetwork || null,
digest: digests.length === 1 ? digests[0] : null,
digests,
gitopsCommit,
sourceCommit: null,
sourceObservation: null,
valuesPrinted: false,
};
}
@@ -117,7 +325,20 @@ function evaluatePacStatus(inputValue) {
const deliveryDisabled = artifact.imageStatus === "disabled";
const deliverySkipped = artifact.imageStatus === "skipped";
const sourceObservationMode = stringOrNull(sourceObservation.mode);
const hwlabCatalogDelivery = sourceObservationMode === "delivery"
&& sourceObservation.contract === "hwlab-g14-ci-plan";
const noRuntimeChange = sourceObservationMode === "no-runtime-change";
const catalog = record(artifact.catalog);
const catalogPresent = catalog.present === true;
const catalogSourceCommit = stringOrNull(catalog.sourceCommit);
const catalogSourceMatches = catalogSourceCommit !== null && catalogSourceCommit === sourceCommit;
const catalogDeliveryEvidence = sourceObservationMode === "delivery"
&& sourceObservation.valid === true
&& catalogPresent
&& catalog.status === "published"
&& catalog.registryVerified === true
&& catalogSourceMatches;
const registryEvidenceReady = input.registryPresent === true || catalogDeliveryEvidence;
const sourceObservationInconsistent = sourceObservationMode === "inconsistent"
|| (sourceObservationMode !== null && sourceObservation.valid !== true);
const observationSourceCommit = stringOrNull(sourceObservation.sourceCommit);
@@ -146,12 +367,12 @@ function evaluatePacStatus(inputValue) {
ok = false;
code = "pac-source-observation-inconsistent";
phase = "source-observation-inconsistent";
hint = "structured delivery plan and no-runtime-change terminal markers are incomplete or contradictory";
hint = "structured delivery plan or terminal evidence is incomplete or contradictory";
} else if (sourceObservationMode !== null && !observationSourceMatches) {
ok = false;
code = "pac-source-observation-mismatch";
phase = "source-observation-mismatch";
hint = "the structured no-runtime-change observation does not belong to the selected PipelineRun source commit";
hint = "the structured source observation does not belong to the selected PipelineRun source commit";
} else if (noRuntimeChange && retainedOutputConflict) {
ok = false;
code = "pac-retained-output-conflict";
@@ -191,11 +412,31 @@ function evaluatePacStatus(inputValue) {
code = "pac-ready-no-runtime-change";
phase = "ready-no-runtime-change";
hint = "the successful source observation requires no artifact, GitOps or runtime change; retained Argo and runtime provenance remain healthy";
} else if (stringOrNull(input.imageRepository) !== null && input.registryPresent !== true) {
} else if (catalogPresent && !catalogSourceMatches) {
ok = false;
code = "pac-artifact-catalog-source-mismatch";
phase = "artifact-catalog-source-mismatch";
hint = "the artifact catalog source commit does not match the selected PipelineRun";
} else if (hwlabCatalogDelivery && (!catalogPresent || catalog.status !== "published")) {
ok = false;
code = "pac-artifact-catalog-missing";
phase = "artifact-catalog-missing";
hint = "the HWLAB delivery plan completed but a published artifact catalog is not visible";
} else if (hwlabCatalogDelivery && catalog.registryVerified !== true) {
ok = false;
code = "pac-artifact-catalog-unverified";
phase = "artifact-catalog-unverified";
hint = "the HWLAB artifact catalog did not prove registry verification";
} else if (hwlabCatalogDelivery && selectedArtifactDigests.length === 0) {
ok = false;
code = "pac-artifact-catalog-digest-missing";
phase = "artifact-catalog-digest-missing";
hint = "the HWLAB artifact catalog did not expose any sha256 digest";
} else if (stringOrNull(input.imageRepository) !== null && !registryEvidenceReady) {
ok = false;
code = "pac-registry-missing";
phase = "source-ready-registry-missing";
hint = "PaC source commit is known but the configured registry tag is missing";
hint = "PaC source commit is known but neither the configured registry tag nor a verified artifact catalog is visible";
} else if (!registryMatchesArtifact) {
ok = false;
code = "pac-artifact-registry-mismatch";
@@ -216,6 +457,11 @@ function evaluatePacStatus(inputValue) {
code = `pac-argo-revision-${gitopsRelation}`;
phase = `gitops-ready-argo-revision-${gitopsRelation}`;
hint = `Argo revision relation is ${gitopsRelation}; only exact or commit-graph-proven descendant is deployable`;
} else if (catalogDeliveryEvidence && !selectedArtifactMatchesRuntime) {
ok = false;
code = "pac-catalog-artifact-runtime-mismatch";
phase = "artifact-catalog-runtime-mismatch";
hint = "the runtime digest is not present in the verified artifact catalog for this PipelineRun";
} else if (gitopsRelation === "descendant" && !selectedArtifactMatchesRuntime) {
ok = false;
code = "pac-descendant-artifact-runtime-mismatch";
@@ -261,7 +507,9 @@ function evaluatePacStatus(inputValue) {
registryProbeBase: stringOrNull(input.registryProbeBase),
imageTag: stringOrNull(input.imageTag),
registry: {
present: input.registryPresent === true,
present: registryEvidenceReady,
probePresent: input.registryPresent === true,
catalogVerified: catalogDeliveryEvidence,
digest: registryDigest,
url: stringOrNull(input.registryUrl),
},
@@ -350,14 +598,23 @@ function noRuntimeChangeObservation(sourceCommit = "c".repeat(40)) {
}
function deliveryObservation(sourceCommit = "c".repeat(40)) {
return extractPacSourceObservation([{
event: "g14-ci-plan",
sourceCommitId: sourceCommit,
affectedServices: ["service"],
rolloutServices: ["service"],
buildServices: ["service"],
reusedServices: [],
}]);
return extractPacSourceObservation([
{
event: "g14-ci-plan",
sourceCommitId: sourceCommit,
affectedServices: ["service"],
rolloutServices: ["service"],
buildServices: ["service"],
reusedServices: [],
},
{ event: "pac-task-terminal", pipelineTask: "plan-artifacts", taskRun: "plan", status: "succeeded", reason: "Succeeded", results: [] },
{ event: "pac-task-terminal", pipelineTask: "collect-artifacts", taskRun: "collect", status: "succeeded", reason: "Succeeded", results: [] },
{ event: "pac-task-terminal", pipelineTask: "gitops-promote", taskRun: "promote", status: "succeeded", reason: "Succeeded", results: [] },
]);
}
function publishedCatalog(sourceCommit = "c".repeat(40)) {
return { present: true, status: "published", sourceCommit, registryVerified: true };
}
function runPacStatusFixtureChecks() {
@@ -384,19 +641,55 @@ function runPacStatusFixtureChecks() {
id: "delivery-ready-exact",
expectedOk: true,
expectedCode: "pac-ready-gitops-exact",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "delivery-gitops-missing",
expectedOk: false,
expectedCode: "pac-gitops-missing",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, sourceObservation: delivery }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], sourceObservation: delivery, catalog: publishedCatalog() }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
},
{
id: "delivery-runtime-mismatch",
expectedOk: false,
expectedCode: "pac-runtime-not-aligned",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestD}`, digest: digestD, replicas: 1, readyReplicas: 1 } }),
expectedCode: "pac-catalog-artifact-runtime-mismatch",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestD}`, digest: digestD, replicas: 1, readyReplicas: 1 } }),
},
{
id: "catalog-delivery-ready",
expectedOk: true,
expectedCode: "pac-ready-gitops-exact",
input: fixtureInput({ imageRepository: "registry/service", artifact: { imageStatus: "reused", digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: { present: true, status: "published", sourceCommit: "c".repeat(40), registryVerified: true } }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "catalog-delivery-missing",
expectedOk: false,
expectedCode: "pac-artifact-catalog-missing",
input: fixtureInput({ imageRepository: "registry/service", artifact: { imageStatus: "reused", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, registryPresent: true, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "catalog-delivery-unverified",
expectedOk: false,
expectedCode: "pac-artifact-catalog-unverified",
input: fixtureInput({ imageRepository: "registry/service", artifact: { imageStatus: "reused", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: { present: true, status: "published", sourceCommit: "c".repeat(40), registryVerified: false } }, registryPresent: true, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "catalog-delivery-digest-missing",
expectedOk: false,
expectedCode: "pac-artifact-catalog-digest-missing",
input: fixtureInput({ imageRepository: "registry/service", artifact: { imageStatus: "reused", gitopsCommit: revision, sourceObservation: delivery, catalog: { present: true, status: "published", sourceCommit: "c".repeat(40), registryVerified: true } }, registryPresent: true, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "catalog-delivery-runtime-mismatch",
expectedOk: false,
expectedCode: "pac-catalog-artifact-runtime-mismatch",
input: fixtureInput({ imageRepository: "registry/service", artifact: { imageStatus: "reused", digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: { present: true, status: "published", sourceCommit: "c".repeat(40), registryVerified: true } }, argo: exactArgo, runtime: { image: `registry/service@${digestD}`, digest: digestD, replicas: 1, readyReplicas: 1 } }),
},
{
id: "delivery-terminal-evidence-missing",
expectedOk: false,
expectedCode: "pac-source-observation-inconsistent",
input: fixtureInput({ artifact: { sourceObservation: extractPacSourceObservation([{ event: "g14-ci-plan", sourceCommitId: "c".repeat(40), affectedServices: ["service"], rolloutServices: ["service"], buildServices: [], reusedServices: ["service"] }]) } }),
},
{
id: "incomplete-no-runtime-change-markers",
@@ -436,6 +729,9 @@ function runPacStatusFixtureChecks() {
module.exports = {
evaluatePacStatus,
extractPacArtifactEvidence,
extractPacSourceObservation,
parsePacLogRecords,
runPacStatusFixtureChecks,
taskTerminalRecord,
};
@@ -344,7 +344,7 @@ history_rows() {
node <<'NODE'
const fs = require('node:fs');
const cp = require('node:child_process');
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
@@ -497,18 +497,41 @@ function recursiveEnvReuse(value, state) {
for (const item of Object.values(value)) recursiveEnvReuse(item, state);
}
function envReuseForPipelineRun(namespace, name) {
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'logs', sourceObservation: null };
function envReuseForPipelineRun(namespace, name, taskRuns) {
const state = { status: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, source: 'pipeline-task-logs', sourceObservation: null, artifact: null, collector: null };
const matching = (taskRuns.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name);
const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null);
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
const logErrors = [];
let logs = '';
try {
logs = cp.execFileSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers', '--tail=320'], {
for (const item of contractTaskRuns) {
const podName = item.status?.podName;
if (!podName) {
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
continue;
}
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 12000,
maxBuffer: 1024 * 1024,
maxBuffer: 4 * 1024 * 1024,
});
} catch {
return { ...state, source: 'logs-unavailable' };
if (result.error || result.status !== 0) {
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
continue;
}
logs += `${result.stdout || ''}\n`;
}
if (contractTaskRuns.length === 0) {
state.source = 'pipeline-run-log-fallback';
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers=true', '--tail=320'], {
encoding: 'utf8',
timeout: 12000,
maxBuffer: 2 * 1024 * 1024,
});
if (result.error || result.status !== 0) {
return { ...state, source: 'logs-unavailable', collector: { mode: state.source, contractTaskCount: 0, terminalRecordCount: 0, logErrorCount: 1, valuesPrinted: false } };
}
logs = result.stdout || '';
}
const lines = logs.split(/\r?\n/u);
const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
@@ -519,17 +542,18 @@ function envReuseForPipelineRun(namespace, name) {
state.buildCache = row[4];
}
}
const records = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
try {
const parsed = JSON.parse(trimmed);
records.push(parsed);
recursiveEnvReuse(parsed, state);
} catch {}
}
state.sourceObservation = extractPacSourceObservation(records);
const records = [...parsePacLogRecords(logs), ...terminalRecords];
for (const parsed of records) recursiveEnvReuse(parsed, state);
state.artifact = extractPacArtifactEvidence(records, logs);
state.sourceObservation = state.artifact.sourceObservation;
state.collector = {
mode: state.source,
contractTaskCount: contractTaskRuns.length,
terminalRecordCount: terminalRecords.length,
logErrorCount: logErrors.length,
logErrors,
valuesPrinted: false,
};
return state;
}
@@ -583,7 +607,7 @@ function rowFor(consumer, item, taskRuns) {
const labels = item.metadata?.labels || {};
const annotations = item.metadata?.annotations || {};
const sourceCommit = extractCommit(item, params);
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name);
const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name, taskRuns);
const sourceObservation = evidence.sourceObservation === null ? null : {
...evidence.sourceObservation,
sourceMatched: sourceCommit !== null && evidence.sourceObservation.sourceCommit === sourceCommit,
@@ -618,6 +642,8 @@ function rowFor(consumer, item, taskRuns) {
source: evidence.source,
},
sourceObservation,
artifact: evidence.artifact,
collector: evidence.collector,
taskRuns: tasks,
source: 'gitea-pac-tekton-live',
valuesPrinted: false,
@@ -703,98 +729,61 @@ artifact_summary() {
printf '{}'
return
fi
log_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" logs -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" --all-containers --tail=240 >"$log_file" 2>/dev/null || true
node - "$log_file" <<'NODE'
task_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
node - "$task_file" <<'NODE'
const fs = require('node:fs');
const { extractPacSourceObservation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const lines = fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n/);
const records = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
try { records.push(JSON.parse(trimmed)); } catch {}
}
const sourceObservation = extractPacSourceObservation(records);
const publish = [...records].reverse().find((item) => item.phase === 'gitops-publish' || item.gitopsCommit);
const image = publish || [...records].reverse().find((item) => item.imageStatus || item.status === 'reused' || item.status === 'built');
const gitopsCommit = image?.gitopsCommit
|| [...lines].reverse().map((line) => line.match(/^\[[^\]]+\s+([0-9a-f]{7,64})\]/u)?.[1] || null).find(Boolean)
|| null;
const loggedDigests = [...new Set(lines.flatMap((line) => [...line.matchAll(/"(?:digest|environmentDigest)"\s*:\s*"(sha256:[0-9a-f]{64})"/gu)].map((match) => match[1])))];
const envHeaderIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
let humanEnv = null;
if (envHeaderIndex >= 0) {
const row = (lines.slice(envHeaderIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
if (row.length >= 5) {
humanEnv = { envReuse: row[0], nodeDeps: row[1], buildPackage: row[2], buildNetwork: row[3], cache: row[4] };
const cp = require('node:child_process');
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}');
const namespace = process.env.UNIDESK_PAC_TARGET_NAMESPACE;
const pipelineRun = process.env.UNIDESK_PAC_TARGET_PIPELINERUN;
const taskRuns = (input.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pipelineRun);
const contractTaskRuns = taskRuns.filter((item) => taskTerminalRecord(item) !== null);
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
const logErrors = [];
let logText = '';
for (const item of contractTaskRuns) {
const podName = item.status?.podName;
if (!podName) {
logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' });
continue;
}
const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', podName, '--all-containers=true'], {
encoding: 'utf8',
timeout: 12000,
maxBuffer: 4 * 1024 * 1024,
});
if (result.error || result.status !== 0) {
logErrors.push({ taskRun: item.metadata?.name || null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
continue;
}
logText += `${result.stdout || ''}\n`;
}
function digestOf(item) {
if (item.digest) return item.digest;
const ref = item.digestRef || '';
const index = String(ref).indexOf('@');
return index >= 0 ? String(ref).slice(index + 1) : null;
}
function envReuseOf(item) {
const env = item.envReuse || {};
return env.dependencyReuse || env.mode || item.envReuseStatus || null;
}
let out = { valuesPrinted: false };
if (image) {
out = {
imageStatus: image.imageStatus || image.status || (image.digestRef ? 'built' : null),
envIdentity: image.envIdentity || null,
envReuse: envReuseOf(image),
nodeDepsReuse: null,
buildCache: null,
digest: digestOf(image) || (loggedDigests.length === 1 ? loggedDigests[0] : null),
digests: loggedDigests,
gitopsCommit,
sourceCommit: image.sourceCommit || null,
runtimeSourceCommit: image.runtimeSourceCommit || null,
baselineSourceCommit: image.baselineSourceCommit || null,
action: image.action || null,
reason: image.reason || null,
sourceObservation,
valuesPrinted: false,
};
} else if (sourceObservation) {
out = {
imageStatus: sourceObservation.mode === 'no-runtime-change' && sourceObservation.valid === true ? 'retained' : null,
envIdentity: null,
envReuse: null,
nodeDepsReuse: null,
buildCache: null,
digest: null,
digests: [],
gitopsCommit: null,
sourceCommit: sourceObservation.sourceCommit || null,
runtimeSourceCommit: null,
action: sourceObservation.mode,
reason: sourceObservation.reason,
sourceObservation,
valuesPrinted: false,
};
} else if (humanEnv || gitopsCommit || loggedDigests.length > 0) {
out = {
imageStatus: humanEnv ? 'published' : null,
envIdentity: null,
envReuse: humanEnv?.envReuse || null,
nodeDepsReuse: humanEnv?.nodeDeps || null,
buildCache: humanEnv?.cache || null,
buildPackage: humanEnv?.buildPackage || null,
buildNetwork: humanEnv?.buildNetwork || null,
digest: loggedDigests.length === 1 ? loggedDigests[0] : null,
digests: loggedDigests,
gitopsCommit,
sourceCommit: null,
valuesPrinted: false,
};
let mode = 'pipeline-task-logs';
if (contractTaskRuns.length === 0) {
mode = 'pipeline-run-log-fallback';
const fallback = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${pipelineRun}`, '--all-containers=true', '--tail=320'], {
encoding: 'utf8',
timeout: 12000,
maxBuffer: 2 * 1024 * 1024,
});
if (!fallback.error && fallback.status === 0) logText = fallback.stdout || '';
else logErrors.push({ taskRun: null, reason: fallback.error ? String(fallback.error.message || fallback.error) : String(fallback.stderr || '').slice(0, 240) });
}
const records = [...parsePacLogRecords(logText), ...terminalRecords];
const out = extractPacArtifactEvidence(records, logText);
out.collector = {
mode,
contractTaskCount: contractTaskRuns.length,
terminalRecordCount: terminalRecords.length,
logErrorCount: logErrors.length,
logErrors,
valuesPrinted: false,
};
process.stdout.write(JSON.stringify(out));
NODE
rm -f "$log_file"
rm -f "$task_file"
}
runtime_summary() {
@@ -1206,11 +1195,124 @@ history_action() {
}
debug_step_action() {
node <<'NODE'
fixtures=$(node <<'NODE'
const { runPacStatusFixtureChecks } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const result = runPacStatusFixtureChecks();
process.stdout.write(`${JSON.stringify(result)}\n`);
if (!result.ok) process.exitCode = 1;
process.stdout.write(JSON.stringify(result));
NODE
)
if [ -z "$UNIDESK_PAC_HISTORY_ID" ]; then
printf '%s\n' "$fixtures"
FIXTURES_JSON="$fixtures" node -e 'const result=JSON.parse(process.env.FIXTURES_JSON||"{}"); if(!result.ok) process.exitCode=1'
return
fi
history=$(history_rows)
UNIDESK_PAC_DEBUG_FIXTURES_JSON="$fixtures" UNIDESK_PAC_DEBUG_HISTORY_JSON="$history" node <<'NODE'
function record(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
}
function terminalRow(sourceObservation, key, step) {
const source = record(record(sourceObservation.terminalSources)[key]);
return {
step,
present: source.present === true,
status: source.status || 'missing',
source: source.source || null,
taskRun: source.taskRun || null,
reason: source.reason || null,
results: Array.isArray(source.results) ? source.results : [],
markerPresent: source.markerPresent === true,
markerSource: source.markerSource || null,
valuesPrinted: false,
};
}
const fixtures = JSON.parse(process.env.UNIDESK_PAC_DEBUG_FIXTURES_JSON || '{}');
const history = JSON.parse(process.env.UNIDESK_PAC_DEBUG_HISTORY_JSON || '{}');
const rows = Array.isArray(history.rows) ? history.rows : [];
const historyErrors = Array.isArray(history.errors) ? history.errors : [];
const row = record(rows[0]);
const sourceObservation = record(row.sourceObservation);
const artifact = record(row.artifact);
const catalog = record(artifact.catalog);
const collector = record(row.collector);
const terminal = [
terminalRow(sourceObservation, 'planArtifacts', 'plan-artifacts'),
terminalRow(sourceObservation, 'collectArtifacts', 'collect-artifacts'),
terminalRow(sourceObservation, 'gitopsPromote', 'gitops-promote'),
];
let firstBreak = null;
function breakAt(code, stage, reason) {
if (firstBreak === null) firstBreak = { code, stage, reason, valuesPrinted: false };
}
if (historyErrors.length > 0) breakAt('target-read-failed', 'target-read', historyErrors[0]?.error || historyErrors[0]?.stderr || 'target-side history read failed');
if (Object.keys(row).length === 0) breakAt('pipeline-run-not-found', 'pipeline-run', `PipelineRun ${process.env.UNIDESK_PAC_HISTORY_ID || '-'} was not found for the selected consumer`);
if (Object.keys(row).length > 0 && row.status !== 'True') breakAt('pipeline-run-not-succeeded', 'pipeline-run', `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'g14-ci-plan structured evidence is missing');
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'g14-ci-plan source commit does not match the selected PipelineRun');
if (sourceObservation.valid !== true) breakAt('source-observation-invalid', 'plan-artifacts', sourceObservation.reason || 'delivery plan is incomplete or contradictory');
for (const item of terminal) {
if (item.present !== true || item.status !== 'succeeded') breakAt('terminal-evidence-incomplete', item.step, `${item.step} TaskRun terminal status is ${item.status}`);
}
if (Number(collector.logErrorCount || 0) > 0) breakAt('contract-log-read-failed', 'collector', 'one or more contract TaskRun logs could not be read');
if (sourceObservation.mode === 'delivery') {
if (catalog.present !== true || catalog.status !== 'published') breakAt('artifact-catalog-missing', 'collect-artifacts', 'published artifact catalog evidence is missing');
if (catalog.registryVerified !== true) breakAt('artifact-catalog-unverified', 'collect-artifacts', 'artifact catalog registry verification is not true');
if (!Array.isArray(artifact.digests) || artifact.digests.length === 0) breakAt('artifact-digest-missing', 'collect-artifacts', 'artifact catalog did not expose any sha256 digest');
if (typeof artifact.gitopsCommit !== 'string' || artifact.gitopsCommit.length === 0) breakAt('gitops-commit-missing', 'gitops-promote', 'GitOps commit is not visible in the successful promotion TaskRun');
}
const realRun = {
ok: firstBreak === null,
found: Object.keys(row).length > 0,
pipelineRun: Object.keys(row).length === 0 ? null : {
id: row.id || row.pipelineRun || null,
status: row.status || null,
reason: row.reason || null,
sourceCommit: row.commit || null,
sourceMatched: sourceObservation.sourceMatched === true,
valuesPrinted: false,
},
mode: sourceObservation.mode || null,
valid: sourceObservation.valid === true,
terminal,
artifact: {
imageStatus: artifact.imageStatus || null,
envReuse: artifact.envReuse || null,
digests: Array.isArray(artifact.digests) ? artifact.digests : [],
gitopsCommit: artifact.gitopsCommit || null,
sourceCommit: artifact.sourceCommit || null,
catalog: {
present: catalog.present === true,
status: catalog.status || null,
sourceCommit: catalog.sourceCommit || null,
registryVerified: catalog.registryVerified === true,
publishedCount: catalog.publishedCount ?? null,
reusedCount: catalog.reusedCount ?? null,
requiredServiceCount: catalog.requiredServiceCount ?? null,
digestCount: catalog.digestCount ?? null,
source: catalog.source || null,
valuesPrinted: false,
},
valuesPrinted: false,
},
collector: {
mode: collector.mode || null,
contractTaskCount: collector.contractTaskCount ?? null,
terminalRecordCount: collector.terminalRecordCount ?? null,
logErrorCount: collector.logErrorCount ?? null,
logErrors: Array.isArray(collector.logErrors) ? collector.logErrors : [],
valuesPrinted: false,
},
firstBreak,
valuesPrinted: false,
};
const out = {
ok: fixtures.ok === true && realRun.ok,
checks: Array.isArray(fixtures.checks) ? fixtures.checks : [],
realRun,
valuesPrinted: false,
};
process.stdout.write(`${JSON.stringify(out)}\n`);
if (!out.ok) process.exitCode = 1;
NODE
}
+129 -7
View File
@@ -204,10 +204,10 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
if (action === "history" || action === "runs") {
const options = parseHistoryOptions(args.slice(1));
const result = await history(config, options);
return options.full || options.raw ? result : options.json ? compactHistoryJson(result) : renderHistory(result);
return options.raw ? result : options.full ? compactHistoryJson(result, true) : options.json ? compactHistoryJson(result) : renderHistory(result);
}
if (action === "debug-step") {
const options = parseCommonOptions(args.slice(1));
const options = parseHistoryOptions(args.slice(1));
const result = await debugStep(config, options);
return options.full || options.raw ? result : options.json ? result : renderDebugStep(result);
}
@@ -231,7 +231,7 @@ function help(): Record<string, unknown> {
"bun scripts/cli.ts platform-infra pipelines-as-code closeout --target JD01 --consumer <consumer> --source-commit <sha> --wait",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 [--consumer hwlab-jd01-v03] [--limit 10]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 [--consumer <consumer>] [--id <pipelinerun>] [--json]",
],
diagnostics: "webhook-test exists only for bounded connectivity diagnosis and must not be used as delivery evidence.",
boundary: "Sole CI trigger path for GH-1552/GH-1607: GitHub PR merge -> GitHub webhook bridge -> Gitea mirror/snapshot -> Pipelines-as-Code -> Tekton -> GitOps/Argo/k8s runtime.",
@@ -722,7 +722,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
};
}
async function debugStep(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const consumer = resolveConsumer(pac, options.consumerId);
@@ -735,8 +735,10 @@ async function debugStep(config: UniDeskConfig, options: CommonOptions): Promise
mutation: false,
target: targetSummary(target),
consumer: consumer.id,
detailId: options.detailId,
evaluator: "scripts/native/cicd/pac-status-evaluator.cjs",
checks: parsed === null ? [] : arrayRecords(parsed.checks),
realRun: parsed === null ? null : parsed.realRun ?? null,
remote: parsed === null || options.raw ? parsed ?? compactCapture(result, { full: true }) : undefined,
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
valuesPrinted: false,
@@ -1340,7 +1342,8 @@ function compactCloseoutJson(result: Record<string, unknown>): Record<string, un
};
}
function compactHistoryJson(result: Record<string, unknown>): Record<string, unknown> {
function compactHistoryJson(result: Record<string, unknown>, includeTaskDetails = false): Record<string, unknown> {
const detailRequested = stringValue(result.detailId) !== "-";
return {
ok: result.ok === true,
action: result.action,
@@ -1351,6 +1354,9 @@ function compactHistoryJson(result: Record<string, unknown>): Record<string, unk
detailId: result.detailId,
rows: arrayRecords(result.rows).map((row) => {
const taskRuns = record(row.taskRuns);
const artifact = record(row.artifact);
const catalog = record(artifact.catalog);
const collector = record(row.collector);
return {
id: row.id ?? row.pipelineRun,
consumer: row.consumer,
@@ -1368,10 +1374,42 @@ function compactHistoryJson(result: Record<string, unknown>): Record<string, unk
branch: row.branch,
envReuse: row.envReuse,
sourceObservation: row.sourceObservation,
artifact: detailRequested ? {
imageStatus: artifact.imageStatus,
envReuse: artifact.envReuse,
digest: artifact.digest,
digests: artifact.digests,
gitopsCommit: artifact.gitopsCommit,
sourceCommit: artifact.sourceCommit,
action: artifact.action,
reason: artifact.reason,
catalog: {
present: catalog.present === true,
status: catalog.status,
sourceCommit: catalog.sourceCommit,
registryVerified: catalog.registryVerified === true,
publishedCount: catalog.publishedCount,
reusedCount: catalog.reusedCount,
requiredServiceCount: catalog.requiredServiceCount,
digestCount: catalog.digestCount,
source: catalog.source,
valuesPrinted: false,
},
valuesPrinted: false,
} : undefined,
collector: detailRequested ? {
mode: collector.mode,
contractTaskCount: collector.contractTaskCount,
terminalRecordCount: collector.terminalRecordCount,
logErrorCount: collector.logErrorCount,
logErrors: collector.logErrors,
valuesPrinted: false,
} : undefined,
taskRuns: {
count: taskRuns.count,
longest: taskRuns.longest,
failed: taskRuns.failed,
details: includeTaskDetails ? taskRuns.details : undefined,
logsCommand: taskRuns.logsCommand,
},
};
@@ -1654,15 +1692,55 @@ function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
function renderDebugStep(result: Record<string, unknown>): RenderedCliResult {
const checks = arrayRecords(result.checks);
const realRun = record(result.realRun);
const pipelineRun = record(realRun.pipelineRun);
const artifact = record(realRun.artifact);
const catalog = record(artifact.catalog);
const terminal = arrayRecords(realRun.terminal);
const firstBreak = record(realRun.firstBreak);
const fixturePassed = checks.filter((item) => item.ok === true).length;
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE DEBUG STEP",
...table(["TARGET", "CONSUMER", "OK", "CHECKS", "MUTATION"], [[
...table(["TARGET", "CONSUMER", "PIPELINERUN", "OK", "FIXTURES", "MUTATION"], [[
stringValue(record(result.target).id),
stringValue(result.consumer),
stringValue(result.detailId),
boolText(result.ok),
stringValue(checks.length),
`${fixturePassed}/${checks.length}`,
boolText(result.mutation),
]]),
...(Object.keys(realRun).length === 0 ? [] : [
"",
"REAL PIPELINERUN EVIDENCE",
...table(["FOUND", "STATUS", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[
boolText(realRun.found),
stringValue(pipelineRun.status ?? pipelineRun.reason),
short(stringValue(pipelineRun.sourceCommit), 16),
stringValue(realRun.mode),
boolText(realRun.valid),
stringValue(firstBreak.code),
]]),
"",
"TERMINAL EVIDENCE",
...(terminal.length === 0 ? ["-"] : table(["STEP", "PRESENT", "STATUS", "SOURCE", "MARKER", "TASKRUN", "RESULTS"], terminal.map((item) => [
stringValue(item.step),
boolText(item.present),
stringValue(item.status),
stringValue(item.source),
item.markerPresent === true ? stringValue(item.markerSource) : "-",
short(stringValue(item.taskRun), 52),
compactResultNames(item.results),
]))),
"",
"ARTIFACT / GITOPS",
...table(["CATALOG", "VERIFIED", "REUSED", "DIGESTS", "GITOPS"], [[
stringValue(catalog.status),
boolText(catalog.registryVerified),
stringValue(catalog.reusedCount),
stringValue(catalog.digestCount),
short(stringValue(artifact.gitopsCommit), 16),
]]),
]),
"",
"STATUS EVALUATOR FIXTURES",
...(checks.length === 0 ? ["-"] : table(["CASE", "OK", "EXPECTED", "ACTUAL"], checks.map((item) => [
@@ -1819,6 +1897,20 @@ function closeoutCiReady(value: Record<string, unknown>, sourceCommit: string |
if (pipelineGate.ok !== true) return false;
const observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
if (sourceCommit !== null && observedSourceCommit !== sourceCommit) return false;
const sourceObservation = record(summary.sourceObservation);
if (Object.keys(sourceObservation).length > 0) {
if (sourceObservation.valid !== true || stringValue(sourceObservation.sourceCommit) !== observedSourceCommit) return false;
if (stringValue(sourceObservation.contract) === "hwlab-g14-ci-plan" && stringValue(sourceObservation.mode) === "delivery") {
const deliveryArtifact = record(summary.artifact);
const catalog = record(deliveryArtifact.catalog);
const digests = Array.isArray(deliveryArtifact.digests) ? deliveryArtifact.digests : [];
if (catalog.present !== true
|| stringValue(catalog.status) !== "published"
|| catalog.registryVerified !== true
|| stringValue(catalog.sourceCommit) !== observedSourceCommit
|| digests.length === 0) return false;
}
}
if (stringValue(diagnostics.code) === "pac-ready-no-runtime-change") return true;
const artifact = record(summary.artifact);
if (stringValue(artifact.imageStatus) === "disabled") return true;
@@ -2176,6 +2268,10 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
const envReuse = record(row.envReuse);
const sourceObservation = record(row.sourceObservation);
const deliveryPlan = record(sourceObservation.plan);
const terminalSources = record(sourceObservation.terminalSources);
const artifact = record(row.artifact);
const catalog = record(artifact.catalog);
const collector = record(row.collector);
const taskRuns = record(row.taskRuns);
const longest = record(taskRuns.longest);
const failed = record(taskRuns.failed);
@@ -2209,6 +2305,25 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
["reusedServices", stringValue(deliveryPlan.reusedServiceCount)],
]),
"",
"TERMINAL EVIDENCE",
...table(["STEP", "PRESENT", "STATUS", "SOURCE", "MARKER", "TASKRUN"], [
["plan-artifacts", boolText(record(terminalSources.planArtifacts).present), stringValue(record(terminalSources.planArtifacts).status), stringValue(record(terminalSources.planArtifacts).source), record(terminalSources.planArtifacts).markerPresent === true ? stringValue(record(terminalSources.planArtifacts).markerSource) : "-", short(stringValue(record(terminalSources.planArtifacts).taskRun), 52)],
["collect-artifacts", boolText(record(terminalSources.collectArtifacts).present), stringValue(record(terminalSources.collectArtifacts).status), stringValue(record(terminalSources.collectArtifacts).source), record(terminalSources.collectArtifacts).markerPresent === true ? stringValue(record(terminalSources.collectArtifacts).markerSource) : "-", short(stringValue(record(terminalSources.collectArtifacts).taskRun), 52)],
["gitops-promote", boolText(record(terminalSources.gitopsPromote).present), stringValue(record(terminalSources.gitopsPromote).status), stringValue(record(terminalSources.gitopsPromote).source), record(terminalSources.gitopsPromote).markerPresent === true ? stringValue(record(terminalSources.gitopsPromote).markerSource) : "-", short(stringValue(record(terminalSources.gitopsPromote).taskRun), 52)],
]),
"",
"ARTIFACT / COLLECTOR",
...table(["CATALOG", "VERIFIED", "REUSED", "DIGESTS", "GITOPS", "TASKS", "TERMINALS", "LOG_ERRORS"], [[
stringValue(catalog.status),
boolText(catalog.registryVerified),
stringValue(catalog.reusedCount),
stringValue(catalog.digestCount),
short(stringValue(artifact.gitopsCommit), 16),
stringValue(collector.contractTaskCount),
stringValue(collector.terminalRecordCount),
stringValue(collector.logErrorCount),
]]),
"",
"LOGS",
` ${stringValue(taskRuns.logsCommand)}`,
];
@@ -2219,6 +2334,13 @@ function compactLine(value: string): string {
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";
}
function compactResultNames(value: unknown): string {
if (!Array.isArray(value) || value.length === 0) return "-";
const names = value.filter((item): item is string => typeof item === "string");
if (names.length <= 2) return names.join(",");
return `${names.length}:${names.slice(0, 2).join(",")},...`;
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
+1 -1
View File
@@ -471,7 +471,7 @@ export function platformInfraHelp(): unknown {
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 --consumer <consumer> [--json|--full|--raw]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --consumer <consumer> --limit 10 [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun> [--json|--full]",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code debug-step --target JD01 --consumer <consumer> [--id <pipelinerun>] [--json]",
],
description: "Operate YAML-controlled platform-infra services such as Sub2API, LangBot, n8n, Web Terminal, WeChat archive workflows, OpenTelemetry tracing, the independent target-scoped secret plane, the target-scoped Kafka event bus, internal Gitea source authority, and Gitea + Pipelines-as-Code closeout for JD01 migrated consumers. Public services use PK01 Caddy/FRP or YAML-declared PK01 Caddy upstreams rather than Kubernetes Ingress, NodePort, or LoadBalancer.",
target,