fix: require PaC runtime artifact alignment
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user