fix: 修正 PaC registry 状态投影
This commit is contained in:
@@ -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`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user