Merge pull request #2534 from pikasTech/fix/2510-pac-release-observe
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

feat: 增加 PaC 单次发布后取证入口
This commit is contained in:
Lyon
2026-07-18 20:02:46 +08:00
committed by GitHub
7 changed files with 355 additions and 14 deletions
+8
View File
@@ -53,6 +53,7 @@ bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --limi
bun scripts/cli.ts platform-infra pipelines-as-code history --target JD01 --id <pipelinerun>
bun scripts/cli.ts platform-infra pipelines-as-code diagnose-regression --target NC01 --consumer <consumer>
bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target NC01 --consumer selfmedia-nc01
bun scripts/cli.ts platform-infra pipelines-as-code delivery-observe --target NC01 --consumer <consumer> --source-commit <full-sha>
bun scripts/cli.ts platform-infra pipelines-as-code closeout --target <NODE> --consumer <consumer> --wait --timeout 120s
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>
@@ -102,6 +103,13 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
- 该入口固定只读且 `mutation=false`,不得用于同步、触发、补跑或运行面写入。
- 验收 registry cache 性能时,按 [Gitea + PaC](references/gitea-pac.md) 的冷热两次正常 PR 流程执行,不用人工 PipelineRun 或重复 status/timing 试探。
- PaC consumer 按 source commit 做发布后取证:
- 唯一推荐入口是 `platform-infra pipelines-as-code delivery-observe --target <NODE> --consumer <id> --source-commit <full-sha>`
- 一次 target-side capture 直接返回匹配的 PipelineRun、Argo sync/health 与首个退化资源或 condition、同 namespace exact-source workloads、ready/digest、首断点和 `complete|partial|blocked`
- 固定 `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
- TaskRun artifact 日志默认延后时只披露 `artifactEvidence.deferred=true`,不放宽既有 status evaluator,也不伪造 provenance。
- 新增 PaC consumer 的首次引导:
- 先执行一次 `pipelines-as-code bootstrap --dry-run`
- 确认后把同一命令改为 `--confirm`
@@ -0,0 +1,24 @@
# R8.4 任务报告
## 结果
完成 UniDesk #2510 的 PaC 发布后一次取证入口:
- 新增 `platform-infra pipelines-as-code delivery-observe --target <NODE> --consumer <id> --source-commit <full-sha>`
- 命令复用一次既有 status target-side capture,固定 `remoteCaptureCount=1``mutation=false`,不查询 GitHub、不触发交付、不串联 history/timing/裸 Kubernetes 查询。
- 一次输出有界 PipelineRun source match、Argo sync/health/首个退化资源或 condition、runtime namespace 全部 workload 的 source commit/ready/digest、首断点和 `complete|partial|blocked`
- status evaluator 未放宽;TaskRun 日志延后显式标记 `artifactEvidence.deferred=true`
- 期望副本数为 0 的声明式缩容 workload 按 ready 处理。
## 验证
- `bun test scripts/src/platform-infra-pac-delivery-observe.test.ts scripts/src/platform-infra-pac-failure-evidence.test.ts`
- `bash -n scripts/src/platform-infra-pipelines-as-code-remote.sh`
- `bun scripts/cli.ts check --syntax-only`
- `git diff --check`
- NC01/HWLAB L1 只读 smoke:单次采集约 12.1 秒,当前 source `8b57d87ff721ab7cc815ae09e5f3bd6c5de302aa` PipelineRun exact match,观察到 18 个 exact-source workloadsArgo 为 `Synced/Degraded`,因此结果正确保留为 `partial`,未误报 source commit 缺失。
- 对已被后续发布替代的旧 source,入口返回 `blocked/pipeline-source-commit-mismatch`,保持 fail-closed。
## 工具调用减少
同类发布后取证从手工串联 status、history、delivery-timing 和多个对象查询,收敛为一次受控命令;最短路径已固化到 `unidesk-cicd` skill。
@@ -126,7 +126,7 @@
### R8.3 [in_progress]
继续解决 [UniDesk #2165](https://github.com/pikasTech/unidesk/issues/2165) 的 single-consumer `status` 可见性回归:远端观察只输出 started 后结束且缺少 success/partial/timeout/failure 终态摘要,而同 consumer `history` 已能返回 typed `pac-read-timeout`;保持目标 Kubernetes/Tekton/Argo/runtime 唯一 authority,不新增状态源或交付 mutation,修复所有退出分支的有界终态输出,并以一次目标侧最小 status smoke 验收,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8.3_Task_Report.md)。
### R8.4
### R8.4 [completed]
解决 [UniDesk #2510](https://github.com/pikasTech/unidesk/issues/2510):修复 HWLAB PaC PipelineRun 已成功且 Gitea source authority 已 committed 后,single-consumer status 仍误报 source-commit-invalid-or-missing、id-specific history 在 25 秒预算内超时且 Argo Synced/Degraded 缺少首个资源下钻的问题;保持目标 Tekton/Argo/runtime 唯一 authority,禁止人工 PipelineRun、mirror、sync、refresh、第二 history store和放宽 evaluator,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8.4_Task_Report.md)。
## R9 [in_progress]
@@ -0,0 +1,89 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { projectPacDeliveryObserve, renderPacDeliveryObserve } from "./platform-infra-pac-delivery-observe";
const commit = "b".repeat(40);
function fixture(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
target: { id: "NC01" },
consumer: { id: "fixture" },
observation: { ok: true },
summary: {
latestPipelineRun: { name: "fixture-run", status: "True", reason: "Succeeded", sourceCommit: commit },
pipelineRuns: [{ name: "fixture-run", status: "True", reason: "Succeeded", sourceCommit: commit }],
artifact: { collector: { mode: "taskrun-status-only" } },
argo: { sync: "Synced", health: "Healthy", revision: "a".repeat(40) },
runtime: {
workloads: [
{ namespace: "fixture", deployment: "api", sourceCommit: commit, readyReplicas: 1, replicas: 1, digest: "sha256:abc" },
{ namespace: "fixture", deployment: "old", sourceCommit: "c".repeat(40), readyReplicas: 1, replicas: 1 },
],
},
...overrides,
},
};
}
test("delivery observe completes from one exact source capture", () => {
const result = projectPacDeliveryObserve({ status: fixture(), sourceCommit: commit });
assert.equal(result.status, "complete");
assert.equal(result.remoteCaptureCount, 1);
assert.equal((result.pipeline as Record<string, unknown>).sourceMatched, true);
assert.equal((result.runtime as Record<string, unknown>).matchingWorkloadCount, 1);
assert.equal(result.firstBreak, null);
assert.match(renderPacDeliveryObserve(result).join("\n"), /REMOTE_CAPTURE_COUNT 1/u);
});
test("delivery observe preserves exact runtime evidence while surfacing Argo degradation", () => {
const result = projectPacDeliveryObserve({
status: fixture({
argo: {
sync: "OutOfSync",
health: "Degraded",
firstDegradedResource: { kind: "Deployment", namespace: "fixture", name: "worker" },
firstCondition: { type: "ComparisonError", message: "bounded" },
},
}),
sourceCommit: commit,
});
assert.equal(result.status, "partial");
assert.equal(result.ok, true);
assert.equal((result.firstBreak as Record<string, unknown>).code, "argo-not-ready");
});
test("delivery observe blocks a mismatched source commit without accepting another workload", () => {
const result = projectPacDeliveryObserve({ status: fixture(), sourceCommit: "d".repeat(40) });
assert.equal(result.status, "blocked");
assert.equal(result.ok, false);
assert.equal((result.firstBreak as Record<string, unknown>).code, "pipeline-source-commit-mismatch");
});
test("delivery observe selects an exact source from the bounded status capture", () => {
const older = "d".repeat(40);
const status = fixture();
const summary = status.summary as Record<string, unknown>;
summary.latestPipelineRun = { name: "newer-run", status: "True", sourceCommit: commit };
summary.pipelineRuns = [
summary.latestPipelineRun,
{ name: "older-run", status: "True", reason: "Succeeded", sourceCommit: older },
];
summary.runtime = {
workloads: [{ namespace: "fixture", deployment: "api", sourceCommit: older, readyReplicas: 1, replicas: 1 }],
};
const result = projectPacDeliveryObserve({ status, sourceCommit: older });
assert.equal(result.status, "complete");
assert.equal((result.pipeline as Record<string, unknown>).id, "older-run");
assert.equal((result.pipeline as Record<string, unknown>).selectedFrom, "bounded-status-history");
});
test("delivery observe treats an intentionally scaled-to-zero workload as ready", () => {
const status = fixture();
(status.summary as Record<string, unknown>).runtime = {
workloads: [{ namespace: "fixture", deployment: "idle", sourceCommit: commit, readyReplicas: 0, replicas: 0 }],
};
const result = projectPacDeliveryObserve({ status, sourceCommit: commit });
assert.equal(result.status, "complete");
assert.equal((result.runtime as Record<string, unknown>).ready, true);
});
@@ -0,0 +1,158 @@
// Responsibility: project one PaC status capture into explicit source-commit release evidence.
export interface PacDeliveryObserveInput {
readonly status: Record<string, unknown>;
readonly sourceCommit: string;
}
export function projectPacDeliveryObserve(input: PacDeliveryObserveInput): Record<string, unknown> {
const summary = record(input.status.summary);
const pipelineRuns = arrayRecords(summary.pipelineRuns);
const latestPipeline = record(summary.latestPipelineRun);
const pipeline = pipelineRuns.find((item) => item.sourceCommit === input.sourceCommit) ?? latestPipeline;
const argo = record(summary.argo);
const runtime = record(summary.runtime);
const workloads = arrayRecords(runtime.workloads);
const matchingWorkloads = workloads.filter((item) => item.sourceCommit === input.sourceCommit);
const pipelineSourceCommit = stringOrNull(pipeline.sourceCommit);
const sourceMatched = pipelineSourceCommit === input.sourceCommit;
const pipelineSucceeded = pipeline.status === "True";
const runtimeReady = matchingWorkloads.length > 0 && matchingWorkloads.every(workloadReady);
const argoReady = argo.sync === "Synced" && argo.health === "Healthy";
const observationReady = record(input.status.observation).ok === true;
const state = !observationReady || !sourceMatched || !pipelineSucceeded || matchingWorkloads.length === 0
? "blocked"
: runtimeReady && argoReady
? "complete"
: "partial";
const firstBreak = firstBreakFor({
observationReady,
sourceMatched,
pipelineSucceeded,
matchingWorkloads,
runtimeReady,
argoReady,
argo,
});
return {
ok: state !== "blocked",
status: state,
action: "platform-infra-pipelines-as-code-delivery-observe",
mutation: false,
sourceCommit: input.sourceCommit,
remoteCaptureCount: 1,
target: input.status.target,
consumer: input.status.consumer,
observation: input.status.observation,
pipeline: {
id: pipeline.name ?? null,
status: pipeline.status ?? null,
reason: pipeline.reason ?? null,
sourceCommit: pipelineSourceCommit,
sourceMatched,
durationSeconds: pipeline.durationSeconds ?? null,
selectedFrom: pipeline === latestPipeline ? "latest" : "bounded-status-history",
},
argo: {
sync: argo.sync ?? null,
health: argo.health ?? null,
revision: argo.revision ?? null,
targetRevision: argo.targetRevision ?? null,
firstDegradedResource: argo.firstDegradedResource ?? null,
firstCondition: argo.firstCondition ?? null,
},
runtime: {
workloadCount: workloads.length,
matchingWorkloadCount: matchingWorkloads.length,
ready: runtimeReady,
workloads: matchingWorkloads.map((item) => ({
deployment: item.deployment ?? null,
namespace: item.namespace ?? null,
readyReplicas: item.readyReplicas ?? null,
replicas: item.replicas ?? null,
sourceCommit: item.sourceCommit ?? null,
digest: item.digest ?? null,
})),
},
artifactEvidence: {
mode: record(summary.artifact).collector === undefined ? null : record(record(summary.artifact).collector).mode ?? null,
deferred: record(record(summary.artifact).collector).mode === "taskrun-status-only",
},
firstBreak,
valuesPrinted: false,
};
}
export function renderPacDeliveryObserve(result: Record<string, unknown>): string[] {
const target = record(result.target);
const consumer = record(result.consumer);
const pipeline = record(result.pipeline);
const argo = record(result.argo);
const runtime = record(result.runtime);
const firstBreak = record(result.firstBreak);
const workloads = arrayRecords(runtime.workloads);
return [
"PLATFORM-INFRA PIPELINES-AS-CODE DELIVERY OBSERVE",
`TARGET ${text(target.id)} CONSUMER ${text(consumer.id)} STATUS ${text(result.status)} MUTATION false`,
`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)}`,
`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)}`,
...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)}`}`,
"REMOTE_CAPTURE_COUNT 1",
];
}
function firstBreakFor(input: {
observationReady: boolean;
sourceMatched: boolean;
pipelineSucceeded: boolean;
matchingWorkloads: readonly Record<string, unknown>[];
runtimeReady: boolean;
argoReady: boolean;
argo: Record<string, unknown>;
}): Record<string, unknown> | null {
if (!input.observationReady) return { code: "target-capture-failed", phase: "remote-read" };
if (!input.sourceMatched) return { code: "pipeline-source-commit-mismatch", 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.runtimeReady) return { code: "runtime-workload-not-ready", phase: "runtime" };
if (!input.argoReady) {
return {
code: "argo-not-ready",
phase: "argo",
resource: input.argo.firstDegradedResource ?? null,
condition: input.argo.firstCondition ?? null,
};
}
return null;
}
function workloadReady(value: Record<string, unknown>): boolean {
return Number.isInteger(value.replicas)
&& Number(value.replicas) >= 0
&& Number.isInteger(value.readyReplicas)
&& Number(value.readyReplicas) >= Number(value.replicas);
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.map(record) : [];
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function text(value: unknown): string {
return value === null || value === undefined || value === "" ? "-" : String(value);
}
function short(value: string, max = 16): string {
return value.length <= max ? value : value.slice(0, max);
}
@@ -1280,24 +1280,48 @@ NODE
namespace=$(printf '%s' "$target" | cut -f1)
name=$(printf '%s' "$target" | cut -f2)
deploy_file=$(mktemp)
pac_read kubectl -n "$namespace" get deploy "$name" -o json >"$deploy_file" 2>/dev/null || printf '{}' >"$deploy_file"
node - "$deploy_file" <<'NODE' || printf '{}'
pac_read kubectl -n "$namespace" get deploy -o json >"$deploy_file" 2>/dev/null || printf '{"items":[]}' >"$deploy_file"
UNIDESK_PAC_RUNTIME_PRIMARY_DEPLOYMENT="$name" node - "$deploy_file" <<'NODE' || printf '{}'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8').trim();
if (!input) {
process.stdout.write('{}');
process.exit(0);
}
const deploy = JSON.parse(input);
const image = deploy.spec?.template?.spec?.containers?.[0]?.image || null;
const digest = image && String(image).includes('@') ? String(image).split('@').slice(1).join('@') : null;
const list = JSON.parse(input);
const items = Array.isArray(list.items) ? list.items : [];
const sourceCommit = (deploy) => {
const labels = deploy.spec?.template?.metadata?.labels || {};
const annotations = deploy.spec?.template?.metadata?.annotations || {};
return labels['unidesk.ai/source-commit']
|| labels['hwlab.pikastech.local/source-commit']
|| labels['agentrun.pikastech.local/source-commit']
|| annotations['unidesk.ai/source-commit']
|| annotations['hwlab.pikastech.local/source-commit']
|| annotations['agentrun.pikastech.local/source-commit']
|| null;
};
const workload = (deploy) => {
const image = deploy.spec?.template?.spec?.containers?.[0]?.image || null;
const digest = image && String(image).includes('@') ? String(image).split('@').slice(1).join('@') : null;
return {
namespace: deploy.metadata?.namespace || null,
deployment: deploy.metadata?.name || null,
readyReplicas: deploy.status?.readyReplicas ?? 0,
replicas: deploy.spec?.replicas ?? null,
sourceCommit: sourceCommit(deploy),
image,
digest,
valuesPrinted: false,
};
};
const workloads = items.map(workload).sort((left, right) => String(left.deployment).localeCompare(String(right.deployment))).slice(0, 100);
const primary = workloads.find((item) => item.deployment === process.env.UNIDESK_PAC_RUNTIME_PRIMARY_DEPLOYMENT) || {};
process.stdout.write(JSON.stringify({
namespace: deploy.metadata?.namespace || null,
deployment: deploy.metadata?.name || null,
readyReplicas: deploy.status?.readyReplicas ?? null,
replicas: deploy.spec?.replicas ?? null,
image,
digest,
...primary,
namespace: primary.namespace || process.env.UNIDESK_PAC_RUNTIME_NAMESPACE || null,
workloads,
workloadCount: workloads.length,
valuesPrinted: false,
}));
NODE
@@ -1776,7 +1800,7 @@ status_action() {
argo_file=$(mktemp)
pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$argo_file" 2>/dev/null || printf '{}' >"$argo_file"
export UNIDESK_PAC_ARGO_FILE="$argo_file"
argo=$(node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null}))' <"$argo_file" || echo '{}')
argo=$(node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} const bounded=(value)=>typeof value==="string"?value.replace(/(authorization\\s*[:=]\\s*)(?:bearer\\s+)?\\S+/giu,"$1[REDACTED]").replace(/(token|password|secret)(\\s*[:=]\\s*)\\S+/giu,"$1$2[REDACTED]").replace(/\\s+/gu," ").trim().slice(0,480):null; const resources=a.status?.resources||[]; const degraded=resources.find((item)=>item.status==="OutOfSync"||item.health?.status==="Degraded"||item.health?.status==="Missing")||resources.find((item)=>item.status!=="Synced"||(item.health?.status&&item.health.status!=="Healthy"))||null; const condition=(a.status?.conditions||[])[0]||((a.status?.operationState?.phase&&a.status.operationState.phase!=="Succeeded")?{type:a.status.operationState.phase,message:a.status.operationState.message}:null); process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null, firstDegradedResource:degraded?{group:degraded.group||null,kind:degraded.kind||null,namespace:degraded.namespace||null,name:degraded.name||null,status:degraded.status||null,health:degraded.health?.status||null}:null,firstCondition:condition?{type:condition.type||null,message:bounded(condition.message)}:null}))' <"$argo_file" || echo '{}')
argo=$(json_normalize "$argo")
status_progress gitops-revision
revision_relation=$(gitops_revision_relation "$artifact" "$argo")
@@ -48,6 +48,7 @@ import {
} from "./platform-infra-pac-delivery-timing";
import { diagnosePacRegression, renderPacRegressionDiagnosis } from "./platform-infra-pac-regression-diagnosis";
import { capturePacNodeStatusSnapshot, type PacNodeStatusConsumer } from "./platform-infra-pac-node-status";
import { projectPacDeliveryObserve, renderPacDeliveryObserve } from "./platform-infra-pac-delivery-observe";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
@@ -287,6 +288,10 @@ interface CloseoutOptions extends CommonOptions {
timeoutMs: number;
}
interface DeliveryObserveOptions extends CommonOptions {
sourceCommit: string;
}
interface ApplyOptions extends CommonOptions {
confirm: boolean;
dryRun: boolean;
@@ -380,6 +385,14 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
const result = await deliveryTiming(config, options);
return options.full || options.raw || options.json ? result : renderDeliveryTiming(result);
}
if (action === "delivery-observe" || action === "observe-delivery") {
const options = parseDeliveryObserveOptions(args.slice(1));
const observed = await status(config, options);
const result = projectPacDeliveryObserve({ status: observed, sourceCommit: options.sourceCommit });
return options.full || options.raw || options.json
? result
: rendered(result, "platform-infra pipelines-as-code delivery-observe", renderPacDeliveryObserve(result));
}
if (action === "debug-step") {
const options = parseHistoryOptions(args.slice(1));
const result = await debugStep(config, options);
@@ -509,7 +522,7 @@ function help(scope: string | null): Record<string, unknown> {
};
}
return {
command: "platform-infra pipelines-as-code plan|status|history|diagnose-regression|delivery-timing|debug-step|source-artifact",
command: "platform-infra pipelines-as-code plan|status|history|diagnose-regression|delivery-timing|delivery-observe|debug-step|source-artifact",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
@@ -517,6 +530,7 @@ function help(scope: string | null): Record<string, unknown> {
"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 diagnose-regression --target NC01 --consumer agentrun-nc01-v02 [--limit 20] [--json|--full]",
"bun scripts/cli.ts platform-infra pipelines-as-code delivery-timing --target NC01 --consumer selfmedia-nc01 [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code delivery-observe --target NC01 --consumer <consumer> --source-commit <full-sha> [--json]",
"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>] [--id <pipelinerun>] [--json]",
"bun scripts/cli.ts platform-infra pipelines-as-code source-artifact check --target NC01 --consumer agentrun-nc01-v02 --source-worktree /abs/worktree",
@@ -2385,6 +2399,7 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
repository: record(payload.repository),
webhooks,
webhookCount: webhooks.length,
pipelineRuns,
latestPipelineRun: latest,
taskRuns,
pipelineRunGate: pipelineGate,
@@ -3361,6 +3376,29 @@ function parseCloseoutOptions(args: string[]): CloseoutOptions {
return { ...parseCommonOptions(commonArgs), sourceCommit, wait, timeoutMs };
}
function parseDeliveryObserveOptions(args: string[]): DeliveryObserveOptions {
const commonArgs: string[] = [];
let sourceCommit: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--source-commit" || arg === "--commit") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
if (!/^[0-9a-f]{40}$/iu.test(value)) throw new Error(`${arg} must be a full git sha`);
sourceCommit = value.toLowerCase();
index += 1;
} else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node" || arg === "--consumer") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
if (sourceCommit === null) throw new Error("pipelines-as-code delivery-observe requires --source-commit <full-sha>");
return { ...parseCommonOptions(commonArgs), sourceCommit };
}
function parseHistoryOptions(args: string[], defaultLimit = 5): HistoryOptions {
const commonArgs: string[] = [];
let limit = defaultLimit;