fix(cicd): preserve refresh evidence in live status

This commit is contained in:
Codex
2026-07-04 02:51:58 +00:00
parent 3886bbbb2c
commit 8281797e56
3 changed files with 29 additions and 8 deletions
+7 -2
View File
@@ -635,6 +635,7 @@ async function applyController(registry: BranchFollowerRegistry, options: Parsed
async function buildStatus(registry: BranchFollowerRegistry, options: ParsedOptions): Promise<Record<string, unknown>> {
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<Record<string, unknown>> {
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<string, unknown> = {},
): Record<string, unknown> {
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,
+13 -5
View File
@@ -23,14 +23,14 @@ export function followerEvidenceSummary(input: {
observedSha: string | null;
livePayload: Record<string, unknown> | null;
storedCommand: Record<string, unknown> | null;
fallbackStoredCommand?: Record<string, unknown> | null;
}): Record<string, unknown> | 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<Record<string, unknown> | null>): Record<string, unknown> | null {
for (const value of values) {
if (value !== null) return value;
}
return null;
}
function asOptionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
+9 -1
View File
@@ -253,7 +253,15 @@ function evidenceRowsForFollower(item: Record<string, unknown>): 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;
}