diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 0c52c0ad..e657a263 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -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, diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 31e406c5..09f81c19 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -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): 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, 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, 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, diagnostics: Record): string { + return stringValue(latest.sourceCommit) !== "-" ? stringValue(latest.sourceCommit) : stringValue(diagnostics.sourceCommit); +} + function closeoutBlocker(value: Record, expected: string | null, observed: string, sourceMatched: boolean, gitOpsMirrorFlush: unknown): Record { if (!sourceMatched) { return {