fix: require PaC runtime artifact alignment

This commit is contained in:
Codex
2026-07-10 04:27:06 +02:00
parent 9f5e9fd5a6
commit 60e1fdf3fb
2 changed files with 94 additions and 38 deletions
@@ -921,36 +921,42 @@ const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
const registryPresent = process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true';
const sourceCommit = process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null;
const registryDigest = process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null;
const expectedDigest = registryDigest || artifact.digest || null;
const artifactDigest = artifact.digest || null;
const expectedDigest = registryDigest || artifactDigest;
const runtimeImage = runtime.image || null;
const runtimeMatches = !expectedDigest || (typeof runtimeImage === 'string' && runtimeImage.includes(expectedDigest));
const runtimeDigest = runtime.digest || (typeof runtimeImage === 'string' && runtimeImage.includes('@') ? runtimeImage.split('@').slice(1).join('@') : null);
const runtimeMatches = !expectedDigest || runtimeDigest === expectedDigest;
const registryMatchesArtifact = !registryDigest || !artifactDigest || registryDigest === artifactDigest;
const healthUrl = process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null;
const healthStatus = process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null;
const healthReady = !healthUrl || healthStatus === '200';
const gitopsReady = Boolean(artifact.gitopsCommit || argo.revision);
const gitopsRevisionMatches = !artifact.gitopsCommit || artifact.gitopsCommit === argo.revision;
const deliveryDisabled = artifact.imageStatus === 'disabled';
let code = 'pac-diagnostics-not-applicable';
let phase = 'not-applicable';
let ok = true;
let hint = 'consumer has no sentinel image_repository diagnostic config';
if (process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY) {
if (deliveryDisabled) {
code = 'pac-delivery-disabled'; phase = 'disabled'; hint = 'delivery is disabled by YAML; registry, GitOps and runtime artifacts are intentionally absent';
} else if (!sourceCommit) {
ok = false; code = 'sentinel-pac-source-unknown'; phase = 'source-unknown'; hint = 'latest PaC PipelineRun did not expose a source commit';
} else if (!registryPresent) {
ok = false; code = 'sentinel-pac-registry-missing'; phase = 'source-ready-registry-missing'; hint = 'PaC source commit is known but the configured registry tag is missing';
} else if (!gitopsReady) {
ok = false; code = 'sentinel-pac-gitops-missing'; phase = 'registry-ready-gitops-missing'; hint = 'registry tag exists but GitOps revision is not visible in PaC artifact or Argo status';
} else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') {
ok = false; code = 'sentinel-pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy';
} else if (!runtimeMatches) {
ok = false; code = 'sentinel-pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image does not match the registry digest observed for the source commit';
} else if (!healthReady) {
ok = false; code = 'pac-health-not-ready'; phase = 'runtime-ready-health-pending'; hint = 'runtime image is aligned but the configured health endpoint is not ready';
} else {
code = 'sentinel-pac-ready'; phase = 'ready'; hint = 'source, registry tag, GitOps, Argo and runtime are aligned';
}
let hint = 'consumer did not expose artifact or runtime alignment evidence';
if (deliveryDisabled) {
code = 'pac-delivery-disabled'; phase = 'disabled'; hint = 'delivery is disabled by YAML; registry, GitOps and runtime artifacts are intentionally absent';
} else if (!sourceCommit) {
ok = false; code = 'pac-source-unknown'; phase = 'source-unknown'; hint = 'latest PaC PipelineRun did not expose a source commit';
} else if (process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY && !registryPresent) {
ok = false; code = 'pac-registry-missing'; phase = 'source-ready-registry-missing'; hint = 'PaC source commit is known but the configured registry tag is missing';
} else if (!registryMatchesArtifact) {
ok = false; code = 'pac-artifact-registry-mismatch'; phase = 'artifact-registry-mismatch'; hint = 'PipelineRun artifact digest does not match the configured registry digest';
} else if (!gitopsReady) {
ok = false; code = 'pac-gitops-missing'; phase = 'artifact-ready-gitops-missing'; hint = 'PipelineRun completed but its GitOps commit is not visible';
} else if (argo.sync !== 'Synced' || argo.health !== 'Healthy') {
ok = false; code = 'pac-argo-not-ready'; phase = 'gitops-ready-argo-pending'; hint = 'GitOps exists but Argo is not Synced/Healthy';
} else if (!gitopsRevisionMatches) {
ok = false; code = 'pac-argo-revision-not-aligned'; phase = 'gitops-ready-argo-revision-mismatch'; hint = 'Argo has not observed the GitOps commit produced by the selected PipelineRun';
} else if (!runtimeMatches) {
ok = false; code = 'pac-runtime-not-aligned'; phase = 'argo-ready-runtime-mismatch'; hint = 'runtime image digest does not match the artifact digest produced by the selected PipelineRun';
} else if (!healthReady) {
ok = false; code = 'pac-health-not-ready'; phase = 'runtime-ready-health-pending'; hint = 'runtime image is aligned but the configured health endpoint is not ready';
} else if (artifactDigest || process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY) {
code = 'pac-ready'; phase = 'ready'; hint = 'source, artifact, GitOps, Argo and runtime are aligned';
}
process.stdout.write(JSON.stringify({
ok,
@@ -972,7 +978,7 @@ process.stdout.write(JSON.stringify({
commit: artifact.gitopsCommit || argo.revision || null,
},
argo: { sync: argo.sync || null, health: argo.health || null, revision: argo.revision || null },
runtime: { image: runtimeImage, digest: runtime.digest || null, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null },
runtime: { image: runtimeImage, digest: runtimeDigest, readyReplicas: runtime.readyReplicas ?? null, replicas: runtime.replicas ?? null },
health: { url: healthUrl, status: healthStatus, ready: healthReady },
pipelineRun: latest.name || null,
deliveryDisabled,
+66 -16
View File
@@ -547,27 +547,49 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
const consumer = resolveConsumer(pac, options.consumerId);
const startedAt = Date.now();
const waitMs = Math.max(1, pac.release.waitTimeoutSeconds) * 1000;
let attempts = 0;
let ciAttempts = 0;
let runtimeAttempts = 0;
let current = await status(config, options);
attempts += 1;
while (options.wait && !closeoutReady(current, options.sourceCommit) && Date.now() - startedAt < waitMs) {
ciAttempts += 1;
while (options.wait && !closeoutCiReady(current, options.sourceCommit) && Date.now() - startedAt < waitMs) {
await sleep(3000);
current = await status(config, options);
attempts += 1;
ciAttempts += 1;
}
const summary = record(current.summary);
const latest = record(summary.latestPipelineRun);
const diagnostics = record(summary.diagnostics);
const observedSourceCommit = stringValue(latest.sourceCommit) !== "-" ? stringValue(latest.sourceCommit) : stringValue(diagnostics.sourceCommit);
const sourceMatched = options.sourceCommit === null || observedSourceCommit === options.sourceCommit;
const baseReady = current.ok === true && sourceMatched;
const gitOpsMirrorFlush = await runCloseoutGitOpsMirrorFlush(config, pac, consumer, latest, baseReady, observedSourceCommit);
let summary = record(current.summary);
let latest = record(summary.latestPipelineRun);
let diagnostics = record(summary.diagnostics);
let observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
let sourceMatched = options.sourceCommit === null || observedSourceCommit === options.sourceCommit;
const ciReady = closeoutCiReady(current, options.sourceCommit);
let gitOpsMirrorFlush = await runCloseoutGitOpsMirrorFlush(config, pac, consumer, latest, ciReady, observedSourceCommit);
const gitOpsMirrorFlushReady = record(gitOpsMirrorFlush).ok !== false;
const runtimeStartedAt = Date.now();
if (ciReady && gitOpsMirrorFlushReady) {
current = await status(config, options);
runtimeAttempts += 1;
while (options.wait && !closeoutReady(current, options.sourceCommit) && Date.now() - runtimeStartedAt < waitMs) {
await sleep(3000);
current = await status(config, options);
runtimeAttempts += 1;
}
}
summary = record(current.summary);
latest = record(summary.latestPipelineRun);
diagnostics = record(summary.diagnostics);
observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
sourceMatched = options.sourceCommit === null || observedSourceCommit === options.sourceCommit;
const baseReady = current.ok === true && sourceMatched;
gitOpsMirrorFlush = {
...record(gitOpsMirrorFlush),
afterSummary: summary,
valuesPrinted: false,
};
const ready = baseReady && gitOpsMirrorFlushReady;
return {
ok: ready,
action: "platform-infra-pipelines-as-code-closeout",
mutation: false,
mutation: record(gitOpsMirrorFlush).executed === true,
target: targetSummary(target),
consumer,
sourceCommit: options.sourceCommit,
@@ -576,9 +598,12 @@ async function closeout(config: UniDeskConfig, options: CloseoutOptions): Promis
ready,
wait: {
requested: options.wait,
attempts,
attempts: ciAttempts + runtimeAttempts,
ciAttempts,
runtimeAttempts,
elapsedMs: Date.now() - startedAt,
budgetSeconds: pac.release.waitTimeoutSeconds,
budgetScope: "per-phase",
source: `${configLabel}.release.waitTimeoutSeconds`,
valuesPrinted: false,
},
@@ -1154,7 +1179,15 @@ function renderCloseout(result: Record<string, unknown>): RenderedCliResult {
...table(["TARGET", "CONSUMER", "READY", "SOURCE_MATCHED", "MUTATION"], [[stringValue(target.id), stringValue(consumer.id), boolText(result.ready), boolText(result.sourceMatched), boolText(result.mutation)]]),
"",
"WAIT",
...table(["REQUESTED", "ATTEMPTS", "ELAPSED_MS", "BUDGET_S", "SOURCE"], [[stringValue(wait.requested), stringValue(wait.attempts), stringValue(wait.elapsedMs), stringValue(wait.budgetSeconds), stringValue(wait.source)]]),
...table(["REQUESTED", "CI_ATTEMPTS", "RUNTIME_ATTEMPTS", "ELAPSED_MS", "BUDGET_S", "SCOPE", "SOURCE"], [[
stringValue(wait.requested),
stringValue(wait.ciAttempts),
stringValue(wait.runtimeAttempts),
stringValue(wait.elapsedMs),
stringValue(wait.budgetSeconds),
stringValue(wait.budgetScope),
stringValue(wait.source),
]]),
"",
"SOURCE / PIPELINERUN",
...table(["EXPECTED", "OBSERVED", "PIPELINERUN", "STATUS", "DURATION_S"], [[short(stringValue(result.sourceCommit), 16), short(stringValue(result.observedSourceCommit), 16), stringValue(latest.name), stringValue(latest.reason ?? latest.status), stringValue(latest.durationSeconds)]]),
@@ -1358,14 +1391,31 @@ function parseCommonOptions(args: string[]): CommonOptions {
function closeoutReady(value: Record<string, unknown>, sourceCommit: string | null): boolean {
if (value.ok !== true) return false;
if (sourceCommit === null) return true;
const summary = record(value.summary);
const latest = record(summary.latestPipelineRun);
const diagnostics = record(summary.diagnostics);
const observedSourceCommit = stringValue(latest.sourceCommit) !== "-" ? stringValue(latest.sourceCommit) : stringValue(diagnostics.sourceCommit);
const observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
if (sourceCommit === null) return true;
return observedSourceCommit === sourceCommit;
}
function closeoutCiReady(value: Record<string, unknown>, sourceCommit: string | null): boolean {
const summary = record(value.summary);
const latest = record(summary.latestPipelineRun);
const diagnostics = record(summary.diagnostics);
const pipelineGate = record(summary.pipelineRunGate);
if (pipelineGate.ok !== true) return false;
const observedSourceCommit = observedCloseoutSourceCommit(latest, diagnostics);
if (sourceCommit !== null && observedSourceCommit !== sourceCommit) return false;
const artifact = record(summary.artifact);
if (stringValue(artifact.imageStatus) === "disabled") return true;
return stringValue(artifact.gitopsCommit) !== "-";
}
function observedCloseoutSourceCommit(latest: Record<string, unknown>, diagnostics: Record<string, unknown>): string {
return stringValue(latest.sourceCommit) !== "-" ? stringValue(latest.sourceCommit) : stringValue(diagnostics.sourceCommit);
}
function closeoutBlocker(value: Record<string, unknown>, expected: string | null, observed: string, sourceMatched: boolean, gitOpsMirrorFlush: unknown): Record<string, unknown> {
if (!sourceMatched) {
return {