fix: 修正 PaC registry 状态投影

This commit is contained in:
AgentRun Codex
2026-07-13 08:16:55 +00:00
parent 23454a8884
commit f130598ca2
7 changed files with 121 additions and 14 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ bun scripts/cli.ts agentrun control-plane legacy-cicd --help
bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
```
节点级只读状态必须优先用 `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。
节点级只读状态必须优先用 `cicd status --node <NODE>`。它从 `config/platform-infra/pipelines-as-code.yaml` 找到该 node 的所有当前 PaC consumer,一次性汇总 PipelineRun、Argo/GitOps、runtime readiness 和诊断;不要再靠阅读源码或手动拼三条 consumer 命令来回答 “NC01 的 CI/CD 流水线情况”。text 输出在零 consumer、ready、warning 与失败时均返回非空 typed 摘要和精确下钻命令;GitOps-only consumer 的 registry 显示 `N/A`,只有 owning YAML 为该 consumer 对应 pipeline 声明 image repository 时,registry missing 才是 blocker。`platform-infra pipelines-as-code status --target <NODE> --consumer <id>` 只作为单 consumer drill-down。
- 新增 PaC consumer 的首次引导:
- 先执行一次 `pipelines-as-code bootstrap --dry-run`
+21 -2
View File
@@ -567,6 +567,11 @@ function extractPacArtifactEvidence(recordsInput, logText) {
function evaluatePacStatus(inputValue) {
const input = record(inputValue);
const registryApplicability = input.registryApplicability === "not-configured"
? "not-configured"
: input.registryApplicability === "configured" || stringOrNull(input.imageRepository) !== null
? "configured"
: "not-configured";
const artifact = record(input.artifact);
const argo = record(input.argo);
const runtime = record(input.runtime);
@@ -704,7 +709,7 @@ function evaluatePacStatus(inputValue) {
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) {
} else if (registryApplicability === "configured" && stringOrNull(input.imageRepository) !== null && !registryEvidenceReady) {
ok = false;
code = "pac-registry-missing";
phase = "source-ready-registry-missing";
@@ -779,7 +784,9 @@ function evaluatePacStatus(inputValue) {
registryProbeBase: stringOrNull(input.registryProbeBase),
imageTag: stringOrNull(input.imageTag),
registry: {
present: registryEvidenceReady,
applicability: registryApplicability,
status: registryApplicability === "configured" ? (registryEvidenceReady ? "present" : "missing") : "not-configured",
present: registryApplicability === "configured" ? registryEvidenceReady : null,
probePresent: input.registryPresent === true,
catalogVerified: catalogDeliveryEvidence,
digest: registryDigest,
@@ -915,6 +922,18 @@ function runPacStatusFixtureChecks() {
expectedCode: "pac-ready-gitops-exact",
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: "gitops-only-registry-not-configured",
expectedOk: true,
expectedCode: "pac-ready-gitops-exact",
input: fixtureInput({ registryApplicability: "not-configured", imageRepository: "registry/shared-repository-image", artifact: { gitopsCommit: revision }, argo: exactArgo }),
},
{
id: "image-registry-missing-fails-closed",
expectedOk: false,
expectedCode: "pac-registry-missing",
input: fixtureInput({ registryApplicability: "configured", imageRepository: "registry/service", artifact: { imageStatus: "built", digest: digestA, gitopsCommit: revision }, argo: exactArgo }),
},
{
id: "delivery-ready-descendant",
expectedOk: true,
+66
View File
@@ -0,0 +1,66 @@
import { expect, test } from "bun:test";
import { renderNodeStatus } from "./cicd-node-status";
const next = {
status: "bun scripts/cli.ts cicd status --node NC01",
history: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01",
fixAutomaticDelivery: { reference: ".agents/skills/unidesk-cicd/SKILL.md" },
};
function result(status: "ready" | "warning" | "blocked", consumers: Array<Record<string, unknown>>): Record<string, unknown> {
return {
ok: status === "ready",
node: "NC01",
target: { id: "NC01" },
summary: {
ready: status === "ready",
status,
code: status === "ready" ? "cicd-node-ready" : status === "warning" ? "cicd-node-no-consumers" : "cicd-node-blocked",
total: consumers.length,
readyCount: consumers.filter((consumer) => consumer.ready === true).length,
blockedCount: consumers.filter((consumer) => consumer.ready !== true).length,
elapsedMs: 12,
},
consumers,
next,
};
}
const readyConsumer = {
consumer: "platform-infra-gitea-nc01",
ready: true,
pipelineStatus: "Succeeded",
durationSeconds: 9,
sourceCommit: "a".repeat(40),
gitopsCommit: "b".repeat(40),
argo: { sync: "Synced", health: "Healthy", revisionRelation: { relation: "exact" } },
runtime: { readyReplicas: 1, replicas: 1, digest: `sha256:${"c".repeat(64)}` },
reason: "ready",
latestPipelineRun: "platform-infra-gitea-nc01-fixture",
digest: `sha256:${"c".repeat(64)}`,
imageStatus: "-",
diagnostics: { hint: "GitOps-only consumer is ready", registry: { applicability: "not-configured", status: "not-configured", present: null } },
};
test("node status renderer is non-empty for zero consumers, ready, warning, and blocked projections", () => {
const fixtures = [
result("warning", []),
result("ready", [readyConsumer]),
result("warning", [{ ...readyConsumer, ready: false, reason: "diagnostics-warning" }]),
result("blocked", [{ ...readyConsumer, ready: false, reason: "pac-registry-missing" }]),
];
for (const fixture of fixtures) {
const rendered = renderNodeStatus(fixture, true).renderedText;
expect(rendered.trim().length).toBeGreaterThan(0);
expect(rendered).toContain("CI/CD NODE STATUS");
expect(rendered).toContain("NEXT");
}
});
test("node full renderer shows registry N/A without hiding digest evidence", () => {
const rendered = renderNodeStatus(result("ready", [readyConsumer]), true).renderedText;
expect(rendered).toContain("N/A");
expect(rendered).toContain("sha256:");
expect(rendered).toContain("platform-infra-gitea-nc01-fixture");
});
+11 -3
View File
@@ -76,7 +76,7 @@ function parseNodeStatusOptions(args: string[]): NodeStatusOptions {
return { nodeId, targetId, full, raw, output };
}
function renderNodeStatus(result: Record<string, unknown>, full: boolean): RenderedCliResult {
export function renderNodeStatus(result: Record<string, unknown>, full: boolean): RenderedCliResult {
const target = record(result.target);
const summary = record(result.summary);
const consumers = arrayRecords(result.consumers);
@@ -97,13 +97,16 @@ function renderNodeStatus(result: Record<string, unknown>, full: boolean): Rende
stringValue(row.latestPipelineRun),
short(stringValue(row.digest), 18),
stringValue(row.imageStatus),
registryText(record(record(row.diagnostics).registry)),
compactLine(stringValue(record(row.diagnostics).hint)),
]);
const lines = [
"CI/CD NODE STATUS",
...table(["NODE", "TARGET", "READY", "READY_COUNT", "BLOCKED", "ELAPSED_MS"], [[
...table(["NODE", "TARGET", "STATUS", "CODE", "READY", "READY_COUNT", "BLOCKED", "ELAPSED_MS"], [[
stringValue(result.node),
stringValue(target.id),
stringValue(summary.status),
stringValue(summary.code),
boolText(summary.ready),
`${stringValue(summary.readyCount)}/${stringValue(summary.total)}`,
stringValue(summary.blockedCount),
@@ -115,7 +118,7 @@ function renderNodeStatus(result: Record<string, unknown>, full: boolean): Rende
"",
...(full ? [
"DETAIL",
...(detailRows.length === 0 ? ["-"] : table(["CONSUMER", "PIPELINERUN", "DIGEST", "IMAGE_STATUS", "HINT"], detailRows)),
...(detailRows.length === 0 ? ["-"] : table(["CONSUMER", "PIPELINERUN", "DIGEST", "IMAGE_STATUS", "REGISTRY", "HINT"], detailRows)),
"",
] : []),
"NEXT",
@@ -126,6 +129,11 @@ function renderNodeStatus(result: Record<string, unknown>, full: boolean): Rende
return { ok: result.ok !== false, command: "cicd status", renderedText: lines.join("\n"), contentType: "text/plain" };
}
function registryText(registry: Record<string, unknown>): string {
const status = stringValue(registry.status);
return status === "not-configured" ? "N/A" : status;
}
function isHelpToken(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
}
@@ -347,7 +347,7 @@ test("AgentRun rendered delivery plan and real TaskRun terminals close the statu
test("provenance fixtures fail closed for pre-bootstrap, forged marker, wrong SA, and policy mismatch", () => {
const result = evaluator.runPacStatusFixtureChecks();
expect(result.ok).toBe(true);
for (const id of ["admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) {
for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) {
expect(result.checks.find((item) => item.id === id)?.ok).toBe(true);
}
});
@@ -1242,8 +1242,9 @@ const runtimeSourceCommit = artifact.runtimeSourceCommit || (noRuntimeChange ? n
const tag = !noRuntimeChange && runtimeSourceCommit ? String(runtimeSourceCommit).slice(0, 12) : '';
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
const param = (name) => params[`${prefix}${name}`] ?? params[name];
const imageRepository = typeof param('image_repository') === 'string' ? param('image_repository') : '';
const probeBase = typeof param('registry_probe_base') === 'string' ? param('registry_probe_base').replace(/\/+$/u, '') : '';
const registryConfigured = process.env.UNIDESK_PAC_REGISTRY_APPLICABILITY === 'configured';
const imageRepository = registryConfigured && typeof param('image_repository') === 'string' ? param('image_repository') : '';
const probeBase = registryConfigured && typeof param('registry_probe_base') === 'string' ? param('registry_probe_base').replace(/\/+$/u, '') : '';
let registryUrl = '';
if (imageRepository && tag) {
const firstSlash = imageRepository.indexOf('/');
@@ -1352,6 +1353,7 @@ const evaluated = evaluatePacStatus({
sourceCommit: process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null,
runtimeSourceCommit: process.env.UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT || artifact.runtimeSourceCommit || null,
imageRepository: process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY || null,
registryApplicability: process.env.UNIDESK_PAC_REGISTRY_APPLICABILITY || 'not-configured',
registryProbeBase: process.env.UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE || null,
imageTag: process.env.UNIDESK_PAC_DIAG_IMAGE_TAG || null,
registryUrl: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
@@ -1015,9 +1015,6 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
const target = resolveTarget(pac, options.targetId ?? options.nodeId);
const nodeId = options.nodeId ?? target.id;
const consumers = pac.consumers.filter((consumer) => consumer.node.toLowerCase() === nodeId.toLowerCase());
if (consumers.length === 0) {
throw new Error(`no Pipelines-as-Code consumers are configured for node ${nodeId}; known nodes: ${Array.from(new Set(pac.consumers.map((item) => item.node))).sort().join(", ")}`);
}
const startedAt = Date.now();
const statuses = await Promise.all(consumers.map(async (consumer) => {
const repository = resolveRepository(pac, consumer.repositoryRef);
@@ -1047,8 +1044,9 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
}
}));
const ready = statuses.every((row) => row.ready === true);
const configured = statuses.length > 0;
return {
ok: ready,
ok: configured && ready,
action: "cicd-node-status",
mutation: false,
node: nodeId,
@@ -1061,7 +1059,10 @@ export async function getPlatformInfraPipelinesAsCodeNodeStatus(config: UniDeskC
},
consumers: statuses,
summary: {
ready,
ready: configured && ready,
status: configured ? (ready ? "ready" : "blocked") : "warning",
code: configured ? (ready ? "cicd-node-ready" : "cicd-node-blocked") : "cicd-node-no-consumers",
hint: configured ? null : `no Pipelines-as-Code consumers are configured for node ${nodeId}`,
total: statuses.length,
readyCount: statuses.filter((row) => row.ready === true).length,
blockedCount: statuses.filter((row) => row.ready !== true).length,
@@ -1362,6 +1363,7 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
UNIDESK_PAC_WEBHOOK_SECRET_KEY: repository.webhookSecretKey,
UNIDESK_PAC_CONCURRENCY_LIMIT: String(repository.concurrencyLimit),
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
UNIDESK_PAC_REGISTRY_APPLICABILITY: registryApplicability(consumer, repository),
UNIDESK_PAC_PIPELINE_NAME: consumer.pipeline,
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
UNIDESK_PAC_CONSUMER_CONFIG_B64: Buffer.from(JSON.stringify(consumerConfig), "utf8").toString("base64"),
@@ -1395,6 +1397,14 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
return `${exports}\n${readFileSync(remoteScriptFile, "utf8")}`;
}
function registryApplicability(consumer: PacConsumer, repository: PacRepository): "configured" | "not-configured" {
const prefix = consumer.id === "unidesk-host" ? "unidesk_host_" : "";
const imageRepository = repository.params[`${prefix}image_repository`] ?? repository.params.image_repository;
if (typeof imageRepository !== "string" || imageRepository.length === 0) return "not-configured";
if (prefix.length > 0) return "configured";
return repository.params.pipeline_name === consumer.pipeline ? "configured" : "not-configured";
}
function historyConsumerConfig(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
const repository = resolveRepository(pac, consumer.repositoryRef);
const admissionIdentity = pacAdmissionDesiredIdentity(consumer.node);
@@ -1680,6 +1690,7 @@ function nodeStatusRow(targetId: string, consumer: PacConsumer, repository: PacR
code: stringValue(diagnostics.code),
phase: stringValue(diagnostics.phase),
hint: compactLine(stringValue(diagnostics.hint)),
registry: record(diagnostics.registry),
},
reason: ready ? "ready" : nodeStatusReason({ ciReady, argoReady, runtimeReady, diagnosticsReady, pipelineStatus: statusText(latest), diagnostics }),
statusCommand: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumer.id}`,
@@ -2779,6 +2790,7 @@ function envReuseText(envReuse: Record<string, unknown>): string {
function registryText(registry: Record<string, unknown>): string {
if (Object.keys(registry).length === 0) return "-";
if (registry.status === "not-configured" || registry.applicability === "not-configured") return "N/A";
const present = registry.present === true ? "present" : "missing";
const digest = short(stringValue(registry.digest), 18);
return digest === "-" ? present : `${present}:${digest}`;