diff --git a/scripts/src/fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log b/scripts/src/fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log new file mode 100644 index 00000000..5e265698 --- /dev/null +++ b/scripts/src/fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log @@ -0,0 +1,7 @@ +{"event":"sentinel-publish-stage","stage":"source-fetch","status":"succeeded","elapsedMs":2000,"traceId":"6e6e899d3e812891140aedfd974d524e","valuesRedacted":true} +{"event":"sentinel-publish-stage","stage":"monitor-web-verify","status":"succeeded","elapsedMs":0,"traceId":"6e6e899d3e812891140aedfd974d524e","valuesRedacted":true} +{"event":"sentinel-publish-stage","stage":"image-build","status":"running","elapsedMs":0,"traceId":"6e6e899d3e812891140aedfd974d524e","valuesRedacted":true} +could not connect to unix:///run/user/1000/buildkit/buildkitd.sock after 10 trials +time="2026-07-13T13:49:56Z" level=info msg="running server on /run/user/1000/buildkit/buildkitd.sock" +{"event":"sentinel-publish-stage","stage":"image-build","status":"failed","elapsedMs":9000,"traceId":"6e6e899d3e812891140aedfd974d524e","valuesRedacted":true} +{"ok":false,"status":"failed","traceId":"6e6e899d3e812891140aedfd974d524e","firstBreak":{"code":"child-process-exit-nonzero","phase":"image-build","reason":"image build command exited non-zero","valuesRedacted":true},"error":{"type":"ChildProcessError","code":"EXIT_NONZERO","phase":"image-build","exitCode":1,"stderrSummary":"bounded step logs contain the command failure","valuesRedacted":true},"exitCode":1,"jobName":"hwlab-web-probe-sentinel-nc01-r5ww2","valuesRedacted":true} diff --git a/scripts/src/hwlab-node-web-sentinel-cicd-jobs.ts b/scripts/src/hwlab-node-web-sentinel-cicd-jobs.ts index b1b706c6..cb442069 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd-jobs.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd-jobs.ts @@ -732,7 +732,13 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName "now_ms() { seconds=$(date +%s); printf '%s\\n' $((seconds * 1000)); }", "write_meta() { printf '%s' \"$2\" > \"$meta_dir/$1\"; }", "emit_stage() { stage=$1; status=$2; started=$3; current_phase=$stage; export current_phase; finished=$(now_ms); elapsed=$((finished - started)); trace_id=$(printf '%s' \"${TRACEPARENT:-}\" | cut -d- -f2); printf '{\"event\":\"sentinel-publish-stage\",\"stage\":\"%s\",\"status\":\"%s\",\"elapsedMs\":%s,\"traceId\":\"%s\",\"valuesRedacted\":true}\\n' \"$stage\" \"$status\" \"$elapsed\" \"$trace_id\" | tee -a \"$event_log\"; }", - "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then trace_id=$(printf '%s' \"${TRACEPARENT:-}\" | cut -d- -f2); printf '{\"ok\":false,\"status\":\"failed\",\"traceId\":\"%s\",\"firstBreak\":{\"code\":\"child-process-exit-nonzero\",\"phase\":\"%s\",\"reason\":\"image build command exited non-zero\",\"valuesRedacted\":true},\"error\":{\"type\":\"ChildProcessError\",\"code\":\"EXIT_NONZERO\",\"phase\":\"%s\",\"exitCode\":%s,\"stderrSummary\":\"bounded step logs contain the command failure\",\"valuesRedacted\":true},\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$trace_id\" \"${current_phase:-image-build}\" \"${current_phase:-image-build}\" \"$code\" \"$code\" \"$job_name\" | tee -a \"$event_log\"; fi; exit \"$code\"; }", + "failure_code=child-process-exit-nonzero", + "failure_reason='image build command exited non-zero'", + "failure_error_type=ChildProcessError", + "failure_error_code=EXIT_NONZERO", + "buildkitd_pid=''", + "cleanup_buildkitd() { if [ -n \"$buildkitd_pid\" ] && kill -0 \"$buildkitd_pid\" 2>/dev/null; then kill -TERM \"$buildkitd_pid\" 2>/dev/null || true; cleanup_attempt=0; while [ \"$cleanup_attempt\" -lt 5 ] && kill -0 \"$buildkitd_pid\" 2>/dev/null; do cleanup_attempt=$((cleanup_attempt + 1)); sleep 1; done; if kill -0 \"$buildkitd_pid\" 2>/dev/null; then kill -KILL \"$buildkitd_pid\" 2>/dev/null || true; fi; wait \"$buildkitd_pid\" 2>/dev/null || true; fi; }", + "emit_failed() { code=$?; cleanup_buildkitd; if [ \"$code\" -ne 0 ]; then trace_id=$(printf '%s' \"${TRACEPARENT:-}\" | cut -d- -f2); printf '{\"ok\":false,\"status\":\"failed\",\"traceId\":\"%s\",\"firstBreak\":{\"code\":\"%s\",\"phase\":\"%s\",\"reason\":\"%s\",\"valuesRedacted\":true},\"error\":{\"type\":\"%s\",\"code\":\"%s\",\"phase\":\"%s\",\"exitCode\":%s,\"stderrSummary\":\"bounded step logs contain the command failure\",\"valuesRedacted\":true},\"exitCode\":%s,\"jobName\":\"%s\",\"valuesRedacted\":true}\\n' \"$trace_id\" \"$failure_code\" \"${current_phase:-image-build}\" \"$failure_reason\" \"$failure_error_type\" \"$failure_error_code\" \"${current_phase:-image-build}\" \"$code\" \"$code\" \"$job_name\" | tee -a \"$event_log\"; fi; exit \"$code\"; }", "trap emit_failed EXIT", "cd /workspace/source", "image_build_http_proxy_present=false; if [ -n \"$image_build_http_proxy\" ]; then image_build_http_proxy_present=true; fi", @@ -750,8 +756,19 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName "image_build_started_ms=$(now_ms)", "write_meta image_build_started_ms \"$image_build_started_ms\"", "emit_stage image-build running \"$image_build_started_ms\"", + "buildkit_socket=/run/user/$(id -u)/buildkit/buildkitd.sock", + "buildkit_address=unix://$buildkit_socket", + "buildkit_log=/workspace/buildkitd.log", + "mkdir -p \"$(dirname \"$buildkit_socket\")\"", + "rm -f \"$buildkit_socket\"", + "buildkitd --addr \"$buildkit_address\" ${BUILDKITD_FLAGS:-} > \"$buildkit_log\" 2>&1 &", + "buildkitd_pid=$!", + "buildkit_ready=false", + "buildkit_probe_attempt=0", + "while [ \"$buildkit_probe_attempt\" -lt 30 ]; do buildkit_probe_attempt=$((buildkit_probe_attempt + 1)); if [ -S \"$buildkit_socket\" ] && buildctl --addr \"$buildkit_address\" debug workers >/dev/null 2>&1; then buildkit_ready=true; break; fi; if ! kill -0 \"$buildkitd_pid\" 2>/dev/null; then break; fi; sleep 1; done", + "if [ \"$buildkit_ready\" != true ]; then cat \"$buildkit_log\"; failure_code=buildkit-not-ready; failure_reason=readiness-timeout; failure_error_type=BuildKitReadinessError; failure_error_code=BUILDKIT_NOT_READY; emit_stage image-build failed \"$image_build_started_ms\"; exit 1; fi", "build_status=0", - "env HTTP_PROXY=\"$image_build_http_proxy\" HTTPS_PROXY=\"$image_build_https_proxy\" ALL_PROXY=\"$image_build_all_proxy\" NO_PROXY=\"$image_build_no_proxy\" http_proxy=\"$image_build_http_proxy\" https_proxy=\"$image_build_https_proxy\" all_proxy=\"$image_build_all_proxy\" no_proxy=\"$image_build_no_proxy\" buildctl-daemonless.sh build --allow network.host --frontend dockerfile.v0 --local context=/workspace/source --local dockerfile=/workspace/source --opt filename=Containerfile.web-probe-sentinel --opt \"network=$image_build_network_mode\" --opt \"build-arg:HTTP_PROXY=$image_build_http_proxy\" --opt \"build-arg:HTTPS_PROXY=$image_build_https_proxy\" --opt \"build-arg:ALL_PROXY=$image_build_all_proxy\" --opt \"build-arg:NO_PROXY=$image_build_no_proxy\" --opt \"build-arg:http_proxy=$image_build_http_proxy\" --opt \"build-arg:https_proxy=$image_build_https_proxy\" --opt \"build-arg:all_proxy=$image_build_all_proxy\" --opt \"build-arg:no_proxy=$image_build_no_proxy\" --metadata-file /workspace/build-metadata.json --output \"type=image,name=$image_ref,push=true,registry.insecure=true\" > \"$build_log\" 2>&1 || build_status=$?", + "env HTTP_PROXY=\"$image_build_http_proxy\" HTTPS_PROXY=\"$image_build_https_proxy\" ALL_PROXY=\"$image_build_all_proxy\" NO_PROXY=\"$image_build_no_proxy\" http_proxy=\"$image_build_http_proxy\" https_proxy=\"$image_build_https_proxy\" all_proxy=\"$image_build_all_proxy\" no_proxy=\"$image_build_no_proxy\" buildctl --addr \"$buildkit_address\" build --allow network.host --frontend dockerfile.v0 --local context=/workspace/source --local dockerfile=/workspace/source --opt filename=Containerfile.web-probe-sentinel --opt \"network=$image_build_network_mode\" --opt \"build-arg:HTTP_PROXY=$image_build_http_proxy\" --opt \"build-arg:HTTPS_PROXY=$image_build_https_proxy\" --opt \"build-arg:ALL_PROXY=$image_build_all_proxy\" --opt \"build-arg:NO_PROXY=$image_build_no_proxy\" --opt \"build-arg:http_proxy=$image_build_http_proxy\" --opt \"build-arg:https_proxy=$image_build_https_proxy\" --opt \"build-arg:all_proxy=$image_build_all_proxy\" --opt \"build-arg:no_proxy=$image_build_no_proxy\" --metadata-file /workspace/build-metadata.json --output \"type=image,name=$image_ref,push=true,registry.insecure=true\" > \"$build_log\" 2>&1 || build_status=$?", "cat \"$build_log\"", "image_build_finished_ms=$(now_ms)", "write_meta image_build_finished_ms \"$image_build_finished_ms\"", @@ -767,6 +784,8 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName "write_meta digest \"$digest\"", "write_meta digest_ref \"$digest_ref\"", "emit_stage image-build succeeded \"$image_build_started_ms\"", + "cleanup_buildkitd", + "buildkitd_pid=''", "trap - EXIT", ].join("\n"); } diff --git a/scripts/src/platform-infra-pac-failure-evidence.test.ts b/scripts/src/platform-infra-pac-failure-evidence.test.ts new file mode 100644 index 00000000..5c2a0c01 --- /dev/null +++ b/scripts/src/platform-infra-pac-failure-evidence.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { createRequire } from "node:module"; +import { compactHistoryJson, compactStatusSummary, renderDebugStep, renderHistory, renderStatus } from "./platform-infra-pipelines-as-code"; + +const require = createRequire(import.meta.url); +const { parsePacLogRecords } = require("../native/cicd/pac-status-evaluator.cjs") as { + parsePacLogRecords(value: string): Array>; +}; + +describe("PaC 失败证据合同", () => { + test("真实 sentinel TaskRun 日志保留 typed 首断点", () => { + const fixture = resolve(import.meta.dir, "fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log"); + const record = parsePacLogRecords(readFileSync(fixture, "utf8")).find((item) => item.ok === false); + expect(record).toMatchObject({ + traceId: "6e6e899d3e812891140aedfd974d524e", + firstBreak: { code: "child-process-exit-nonzero", phase: "image-build" }, + error: { type: "ChildProcessError", code: "EXIT_NONZERO", phase: "image-build", exitCode: 1 }, + }); + }); + + test("history、debug-step、status 默认显示 typed 失败字段", () => { + const fixture = resolve(import.meta.dir, "fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log"); + const failure = parsePacLogRecords(readFileSync(fixture, "utf8")).find((item) => item.ok === false) ?? {}; + const row = { + id: "hwlab-web-probe-sentinel-nc01-r5ww2", + consumer: "sentinel-nc01-v03", + deliveryClass: "outer-pac-event", + status: "False", + traceId: failure.traceId, + firstBreak: failure.firstBreak, + error: failure.error, + }; + const history = renderHistory({ rows: [row], config: {}, historyErrors: [], next: {} }).renderedText; + const debug = renderDebugStep({ realRun: { found: true, pipelineRun: row, firstBreak: row.firstBreak, error: row.error, traceId: row.traceId }, checks: [], target: {}, next: {} }).renderedText; + const status = renderStatus({ summary: { latestPipelineRun: row, artifact: row, diagnostics: {} }, consumer: {}, deliveryAuthority: {}, config: {}, coverage: [], target: {}, next: {} }).renderedText; + for (const output of [history, debug, status]) { + expect(output).toContain("6e6e899d3e8128"); + expect(output).toContain("child-process-exit-nonzero"); + expect(output).toContain("ChildProcessError"); + expect(output).toContain("image-build"); + expect(output).toContain("EXIT_NONZERO"); + expect(output).toContain("1"); + } + const compactHistory = compactHistoryJson({ rows: [{ ...row, collector: { matchingTaskCount: 1 } }], detailId: row.id }); + const compactStatus = compactStatusSummary({ latestPipelineRun: row, artifact: row }); + expect(compactHistory).toMatchObject({ rows: [{ traceId: row.traceId, firstBreak: row.firstBreak, error: row.error, collector: { matchingTaskCount: 1 } }] }); + expect(compactStatus).toMatchObject({ artifact: { traceId: row.traceId, firstBreak: row.firstBreak, error: row.error } }); + }); + + test("collector 遍历全部 matching TaskRun,terminal record 仍限定合同任务", () => { + const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8"); + expect(remote).toContain("for (const item of matching)"); + expect(remote).toContain("const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null)"); + expect(remote).toContain("Object.keys(record(row.firstBreak)).length > 0"); + expect(remote).toContain("traceId: row.traceId || null"); + }); + + test("sentinel image build 显式等待 socket 与 worker readiness 并清理 daemon", () => { + const source = readFileSync(resolve(import.meta.dir, "hwlab-node-web-sentinel-cicd-jobs.ts"), "utf8"); + expect(source).toContain("mkdir -p \\\"$(dirname \\\"$buildkit_socket\\\")\\\""); + expect(source).toContain("buildkitd --addr"); + expect(source).toContain("debug workers"); + expect(source).toContain("failure_error_type=BuildKitReadinessError"); + expect(source).toContain("failure_error_code=BUILDKIT_NOT_READY"); + expect(source).toContain("cleanup_buildkitd"); + expect(source).not.toContain("buildctl-daemonless.sh build"); + }); +}); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index fee50115..4ac6f318 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -748,7 +748,7 @@ function envReuseForPipelineRun(namespace, name, taskRuns) { const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean); const logErrors = []; let logs = ''; - for (const item of contractTaskRuns) { + for (const item of matching) { const podName = item.status?.podName; if (!podName) { logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' }); @@ -792,6 +792,7 @@ function envReuseForPipelineRun(namespace, name, taskRuns) { state.sourceObservation = state.artifact.sourceObservation; state.collector = { mode: state.source, + matchingTaskCount: matching.length, contractTaskCount: contractTaskRuns.length, terminalRecordCount: terminalRecords.length, logErrorCount: logErrors.length, @@ -1006,7 +1007,7 @@ const contractTaskRuns = taskRuns.filter((item) => taskTerminalRecord(item) !== const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean); const logErrors = []; let logText = ''; -for (const item of contractTaskRuns) { +for (const item of taskRuns) { const podName = item.status?.podName; if (!podName) { logErrors.push({ taskRun: item.metadata?.name || null, reason: 'pod-name-missing' }); @@ -1064,6 +1065,7 @@ out.firstBreak = diagnostic.firstBreak; out.error = diagnostic.error; out.collector = { mode, + matchingTaskCount: taskRuns.length, contractTaskCount: contractTaskRuns.length, terminalRecordCount: terminalRecords.length, logErrorCount: logErrors.length, @@ -1701,6 +1703,10 @@ function breakAt(code, stage, reason) { 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.deliveryAuthorityEligible !== true) breakAt('pipeline-run-not-delivery-authority', 'pipeline-run', `PipelineRun is classified as ${row.deliveryClass || 'unknown'}; only an outer PaC event can drive delivery closeout`); +if (Object.keys(row).length > 0 && Object.keys(record(row.firstBreak)).length > 0) { + const typed = record(row.firstBreak); + breakAt(typed.code || 'pipeline-run-not-succeeded', typed.phase || typed.stage || 'pipeline-run', typed.reason || `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`); +} 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', 'delivery plan structured evidence is missing'); if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'delivery plan source commit does not match the selected PipelineRun'); @@ -1756,12 +1762,15 @@ const realRun = { }, collector: { mode: collector.mode || null, + matchingTaskCount: collector.matchingTaskCount ?? null, contractTaskCount: collector.contractTaskCount ?? null, terminalRecordCount: collector.terminalRecordCount ?? null, logErrorCount: collector.logErrorCount ?? null, logErrors: Array.isArray(collector.logErrors) ? collector.logErrors : [], valuesPrinted: false, }, + traceId: row.traceId || null, + error: Object.keys(record(row.error)).length === 0 ? null : record(row.error), firstBreak, valuesPrinted: false, }; diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index db4fb713..6d7b6089 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -1951,7 +1951,7 @@ function compactStatusJson(result: Record): Record): Record { +export function compactStatusSummary(summary: Record): Record { const latest = record(summary.latestPipelineRun); const artifact = record(summary.artifact); const argo = record(summary.argo); @@ -1992,6 +1992,9 @@ function compactStatusSummary(summary: Record): Record): Record, includeTaskDetails = false): Record { +export function compactHistoryJson(result: Record, includeTaskDetails = false): Record { const detailRequested = stringValue(result.detailId) !== "-"; return { ok: result.ok === true, @@ -2088,6 +2091,9 @@ function compactHistoryJson(result: Record, includeTaskDetails classification: row.classification, envReuse: row.envReuse, sourceObservation: row.sourceObservation, + traceId: row.traceId, + firstBreak: row.firstBreak, + error: row.error, artifact: detailRequested ? { imageStatus: artifact.imageStatus, envReuse: artifact.envReuse, @@ -2113,6 +2119,7 @@ function compactHistoryJson(result: Record, includeTaskDetails } : undefined, collector: detailRequested ? { mode: collector.mode, + matchingTaskCount: collector.matchingTaskCount, contractTaskCount: collector.contractTaskCount, terminalRecordCount: collector.terminalRecordCount, logErrorCount: collector.logErrorCount, @@ -2237,7 +2244,7 @@ function renderBootstrap(result: Record): RenderedCliResult { return rendered(result, "platform-infra pipelines-as-code bootstrap", lines); } -function renderStatus(result: Record): RenderedCliResult { +export function renderStatus(result: Record): RenderedCliResult { const summary = record(result.summary); const consumer = record(result.consumer); const deliveryAuthority = record(result.deliveryAuthority); @@ -2248,6 +2255,9 @@ function renderStatus(result: Record): RenderedCliResult { const artifact = record(summary.artifact); const argo = record(summary.argo); const diagnostics = record(summary.diagnostics); + const latestFirstBreak = record(artifact.firstBreak ?? latest.firstBreak); + const latestError = record(artifact.error ?? latest.error); + const latestTraceId = stringValue(artifact.traceId ?? latest.traceId); const admissionProvenance = record(summary.admissionProvenance); const consumerBootstrap = record(summary.consumerBootstrap); const bootstrapRunner = record(consumerBootstrap.runner); @@ -2318,10 +2328,14 @@ function renderStatus(result: Record): RenderedCliResult { ` repo-url: ${compactLine(stringValue(argo.repoURL))}`, "", "CICD DIAGNOSIS", - ...table(["OK", "CODE", "PHASE", "SOURCE", "REGISTRY", "GITOPS", "RUNTIME"], [[ + ...table(["OK", "CODE", "PHASE", "TRACE", "FIRST_BREAK", "ERROR", "EXIT", "SOURCE", "REGISTRY", "GITOPS", "RUNTIME"], [[ boolText(diagnostics.ok !== false), stringValue(diagnostics.code), - stringValue(diagnostics.phase), + stringValue(latestFirstBreak.phase ?? latestError.phase ?? diagnostics.phase), + short(latestTraceId, 16), + stringValue(latestFirstBreak.code), + `${stringValue(latestError.type)}/${stringValue(latestError.code)}`, + stringValue(latestError.exitCode), short(stringValue(diagnostics.sourceCommit)), registryText(record(diagnostics.registry)), short(stringValue(record(diagnostics.gitops).commit)), @@ -2407,7 +2421,7 @@ function renderCloseout(result: Record): RenderedCliResult { return rendered(result, "platform-infra pipelines-as-code closeout", lines); } -function renderHistory(result: Record): RenderedCliResult { +export function renderHistory(result: Record): RenderedCliResult { const rows = arrayRecords(result.rows); const config = record(result.config); const historyErrors = arrayRecords(result.historyErrors); @@ -2424,17 +2438,26 @@ function renderHistory(result: Record): RenderedCliResult { ]), "", "TRIGGERS", - ...(rows.length === 0 ? ["-"] : table([timeHeader, "CONSUMER", "CLASS", "OWNER", "STATUS", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => [ + ...(rows.length === 0 ? ["-"] : table([timeHeader, "CONSUMER", "CLASS", "OWNER", "STATUS", "TRACE", "FIRST_BREAK", "PHASE", "ERROR", "EXIT", "DUR_S", "COMMIT", "ENV_REUSE", "ID"], rows.map((row) => { + const firstBreak = record(row.firstBreak); + const error = record(row.error); + return [ stringValue(row.displayTime) === "-" ? isoShort(stringValue(row.triggeredAt)) : stringValue(row.displayTime), stringValue(row.consumer), stringValue(row.deliveryClass), stringValue(row.deliveryOwner ?? row.executionOwner), statusText(row), + short(stringValue(row.traceId), 16), + stringValue(firstBreak.code), + stringValue(firstBreak.phase ?? error.phase), + `${stringValue(error.type)}/${stringValue(error.code)}`, + stringValue(error.exitCode), stringValue(row.durationSeconds), short(stringValue(row.commit)), envReuseText(record(row.envReuse)), stringValue(row.id ?? row.pipelineRun), - ]))), + ]; + }))), ...(detailId === "-" || rows.length !== 1 ? [] : renderHistoryDetail(rows[0] ?? {})), "", "NEXT", @@ -2444,7 +2467,7 @@ function renderHistory(result: Record): RenderedCliResult { return rendered(result, "platform-infra pipelines-as-code history", lines.filter((line): line is string => line !== null)); } -function renderDebugStep(result: Record): RenderedCliResult { +export function renderDebugStep(result: Record): RenderedCliResult { const checks = arrayRecords(result.checks); const realRun = record(result.realRun); const pipelineRun = record(realRun.pipelineRun); @@ -2452,6 +2475,7 @@ function renderDebugStep(result: Record): RenderedCliResult { const catalog = record(artifact.catalog); const terminal = arrayRecords(realRun.terminal); const firstBreak = record(realRun.firstBreak); + const typedError = record(realRun.error); const fixturePassed = checks.filter((item) => item.ok === true).length; const lines = [ "PLATFORM-INFRA PIPELINES-AS-CODE DEBUG STEP", @@ -2466,11 +2490,15 @@ function renderDebugStep(result: Record): RenderedCliResult { ...(Object.keys(realRun).length === 0 ? [] : [ "", "REAL PIPELINERUN EVIDENCE", - ...table(["FOUND", "CLASS", "AUTHORITY", "STATUS", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[ + ...table(["FOUND", "CLASS", "AUTHORITY", "STATUS", "TRACE", "PHASE", "ERROR", "EXIT", "SOURCE", "MODE", "VALID", "FIRST_BREAK"], [[ boolText(realRun.found), stringValue(pipelineRun.deliveryClass), boolText(pipelineRun.deliveryAuthorityEligible), stringValue(pipelineRun.status ?? pipelineRun.reason), + short(stringValue(realRun.traceId), 16), + stringValue(firstBreak.phase ?? firstBreak.stage), + `${stringValue(typedError.type)}/${stringValue(typedError.code)}`, + stringValue(typedError.exitCode), short(stringValue(pipelineRun.sourceCommit), 16), stringValue(realRun.mode), boolText(realRun.valid),