Merge pull request #2534 from pikasTech/fix/2510-pac-release-observe
feat: 增加 PaC 单次发布后取证入口
This commit is contained in:
@@ -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 workloads;Argo 为 `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;
|
||||
|
||||
Reference in New Issue
Block a user