fix: 修复 PaC reused delivery 终态证据

从目标 TaskRun condition/result 与结构化合同日志恢复 artifact catalog、digest 和 GitOps 证据,并为真实 PipelineRun 增加有界 debug-step。严格保留缺 artifact、缺 GitOps、runtime mismatch 与冲突证据的 fail-closed closeout。
This commit is contained in:
Codex
2026-07-11 01:29:22 +02:00
parent ea8e3ed204
commit 1eafb1f89e
5 changed files with 665 additions and 139 deletions
+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(" ");