diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index 3778fa7e..7290b8c2 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -635,6 +635,7 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOptions): Promise> { let k8s = readK8sState(registry, options); + const beforeRefreshStateByFollower = k8s.stateByFollower; const wantsLive = options.live || (!options.noLive && Object.keys(k8s.stateByFollower).length === 0); const refresh = wantsLive && !options.inCluster ? runControllerReconcileJob(registry, options, { dryRun: true, wait: true, recordState: true }) : null; if (refresh !== null) k8s = readK8sState(registry, options); @@ -644,8 +645,9 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti const detailedFollowers = options.followerId !== null || options.full; for (const follower of selected) { const stored = k8s.stateByFollower[follower.id] ?? {}; + const fallbackStored = refresh === null ? {} : beforeRefreshStateByFollower[follower.id] ?? {}; const live = shouldLive && follower.enabled ? await readAdapterStatus(registry, follower, options) : null; - followers.push(mergeFollowerStatus(registry, follower, stored, live, shouldLive, detailedFollowers)); + followers.push(mergeFollowerStatus(registry, follower, stored, live, shouldLive, detailedFollowers, fallbackStored)); } return { ok: k8s.ok && followers.every((item) => item.ok !== false), @@ -666,6 +668,7 @@ async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOpti async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions): Promise> { if (!options.inCluster) { + const before = readK8sState(registry, options); const refresh = runControllerReconcileJob(registry, options, { dryRun: options.dryRun, wait: true, recordState: true }); const k8s = readK8sState(registry, options); const selected = selectFollowers(registry, options, { includeDisabled: false }); @@ -679,7 +682,7 @@ async function runOnce(registry: BranchFollowerRegistry, options: ParsedOptions) execution: "k8s-native-reconcile-job", registry: registrySummary(registry), job: refresh, - followers: selected.map((follower) => mergeFollowerStatus(registry, follower, k8s.stateByFollower[follower.id] ?? {}, null, false)), + followers: selected.map((follower) => mergeFollowerStatus(registry, follower, k8s.stateByFollower[follower.id] ?? {}, null, false, false, before.stateByFollower[follower.id] ?? {})), warnings: refresh.ok ? [] : [`reconcile job failed: ${refresh.message}`], next: { status: "bun scripts/cli.ts cicd branch-follower status", @@ -1943,6 +1946,7 @@ function mergeFollowerStatus( live: AdapterSummary | null, liveRequested: boolean, detailed: boolean, + fallbackStored: Record = {}, ): Record { const storedSource = asOptionalRecord(stored.source); const storedTarget = asOptionalRecord(stored.target); @@ -1956,6 +1960,7 @@ function mergeFollowerStatus( observedSha, livePayload: asOptionalRecord(live?.payload), storedCommand: asOptionalRecord(stored.command), + fallbackStoredCommand: asOptionalRecord(fallbackStored.command), }); const reconcileTimeline = compactReconcileTimeline(asOptionalRecord(stored.command)?.reconcileTimeline, follower.id) ?? { bounded: true, diff --git a/scripts/src/cicd-evidence.ts b/scripts/src/cicd-evidence.ts index 6259e195..9bbe097f 100644 --- a/scripts/src/cicd-evidence.ts +++ b/scripts/src/cicd-evidence.ts @@ -23,14 +23,14 @@ export function followerEvidenceSummary(input: { observedSha: string | null; livePayload: Record | null; storedCommand: Record | null; + fallbackStoredCommand?: Record | null; }): Record | null { const livePayload = input.livePayload; const storedPayload = asOptionalRecord(input.storedCommand?.payload); - const payload = livePayload ?? storedPayload; - if (payload === null) return null; - const tekton = asOptionalRecord(payload.tekton); - const pipeline = asOptionalRecord(payload.pipeline); - const refresh = asOptionalRecord(payload.refreshEvidence); + const fallbackStoredPayload = asOptionalRecord(input.fallbackStoredCommand?.payload); + const tekton = firstRecord(asOptionalRecord(livePayload?.tekton), asOptionalRecord(storedPayload?.tekton), asOptionalRecord(fallbackStoredPayload?.tekton)); + const pipeline = firstRecord(asOptionalRecord(livePayload?.pipeline), asOptionalRecord(storedPayload?.pipeline), asOptionalRecord(fallbackStoredPayload?.pipeline)); + const refresh = firstRecord(asOptionalRecord(storedPayload?.refreshEvidence), asOptionalRecord(fallbackStoredPayload?.refreshEvidence), asOptionalRecord(livePayload?.refreshEvidence)); if (tekton === null && pipeline === null && refresh === null) return null; const pipelineRefName = stringOrNull(tekton?.pipelineRefName); const pipelineName = stringOrNull(asOptionalRecord(pipeline?.metadata)?.name); @@ -39,6 +39,7 @@ export function followerEvidenceSummary(input: { return { pipelineRunRefName: pipelineRefName, pipeline, + refreshBoundedReason: refresh === null ? "missing-from-live-and-stored-evidence" : null, refresh: refresh === null ? null : { @@ -50,6 +51,13 @@ export function followerEvidenceSummary(input: { }; } +function firstRecord(...values: Array | null>): Record | null { + for (const value of values) { + if (value !== null) return value; + } + return null; +} + function asOptionalRecord(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; } diff --git a/scripts/src/cicd-render.ts b/scripts/src/cicd-render.ts index 9dbbcd39..ec63ccf8 100644 --- a/scripts/src/cicd-render.ts +++ b/scripts/src/cicd-render.ts @@ -253,7 +253,15 @@ function evidenceRowsForFollower(item: Record): unknown[][] { ]); } const refresh = asOptionalRecord(evidence.refresh); - if (refresh !== null) rows.push([item.id, "refresh", stringOrNull(refresh.status) ?? "-", `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`, stringOrNull(refresh.pipeline) ?? "-"]); + rows.push([ + item.id, + "refresh", + refresh === null ? "missing" : stringOrNull(refresh.status) ?? "-", + refresh === null + ? stringOrNull(evidence.refreshBoundedReason) ?? "-" + : `${shortSha(stringOrNull(refresh.sourceCommit))}/${boolMatch(refresh.pipelineRefMatches)}/${boolMatch(refresh.pipelineSpecMatches)}`, + refresh === null ? "-" : stringOrNull(refresh.pipeline) ?? "-", + ]); return rows; }