diff --git a/.agents/skills/unidesk-cicd/references/branch-follower.md b/.agents/skills/unidesk-cicd/references/branch-follower.md index 31fd30a3..19f8a147 100644 --- a/.agents/skills/unidesk-cicd/references/branch-follower.md +++ b/.agents/skills/unidesk-cicd/references/branch-follower.md @@ -60,7 +60,7 @@ The automatic controller loop is non-blocking, so closeout acceleration cannot l The same rule applies to git-mirror post-flush. If native status shows runtime/Argo are aligned but GitOps mirror is still pending flush, the automatic controller loop must run the bounded target-side git-mirror flush instead of leaving a follower in `ClosingOut` until a manual wait/closeout path is used. -After an automatic closeout accelerator runs, the same reconcile must do one bounded native status re-read and write the resulting state when it is already aligned. Do not defer the final `Noop` write to the next controller loop; loop interval plus another status-read can add enough idle time to exceed the 120s end-to-end budget even when PipelineRun, Argo and runtime are already ready. The re-read timeout must come from YAML follower budgets. +After an automatic closeout accelerator runs, the same reconcile must do a bounded native status re-read/poll and write the resulting state when it is already aligned. Do not defer the final `Noop` write to the next controller loop; loop interval plus another status-read can add enough idle time to exceed the 120s end-to-end budget even when PipelineRun, Argo and runtime are already ready. The re-read timeout must come from YAML follower budgets, and the short poll interval must come from YAML controller budgets. A single immediate re-read is insufficient when Argo accepts refresh first and updates operation/runtime state a few seconds later. Stage timing rows must not label optional gates as `not-ready` when they are not part of that follower's closeout contract. For sentinel-like followers without a GitOps branch flush gate, git-mirror source snapshot readiness should render as source-ready/ready, while missing GitOps `githubInSync` remains `-`/not-applicable instead of a failure-looking state. diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index c03620d3..f24af72e 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -875,7 +875,7 @@ async function decideAndMaybeTrigger( else if (flush !== null) automaticCloseoutAccelerated = true; } if (automaticCloseoutAccelerated && observedSha !== null) { - const reread = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: follower.budgets.statusSeconds }); + const reread = await readAdapterStatusAfterCloseoutAcceleration(registry, follower, observedSha, options); if (!reread.ok) { warnings.push(`post-closeout status re-read failed: ${redactText(tailText(reread.message, 300))}`); } else if (reread.observedSha === observedSha) { @@ -955,6 +955,32 @@ function shouldRefreshAutomaticCloseout( return !argoReady || !runtimeAligned || live.targetSha !== observedSha; } +async function readAdapterStatusAfterCloseoutAcceleration( + registry: BranchFollowerRegistry, + follower: FollowerSpec, + observedSha: string, + options: ParsedOptions, +): Promise { + const timeoutSeconds = follower.budgets.statusSeconds; + const startedAt = Date.now(); + const deadline = startedAt + Math.max(1, timeoutSeconds) * 1000; + let latest = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: Math.min(timeoutSeconds, remainingSeconds(startedAt, timeoutSeconds)) }); + while ( + latest.ok + && latest.observedSha === observedSha + && latest.aligned !== true + && Date.now() < deadline + ) { + const remaining = Math.max(0, deadline - Date.now()); + const pollSeconds = Math.min(registry.controller.budgets.nativePollIntervalSeconds, Math.ceil(remaining / 1000)); + if (pollSeconds <= 0) break; + runCommand(["sleep", String(pollSeconds)], repoRoot, { timeoutMs: (pollSeconds + 1) * 1000 }); + if (Date.now() >= deadline) break; + latest = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: remainingSeconds(startedAt, timeoutSeconds) }); + } + return latest; +} + function shouldFlushAutomaticCloseout( follower: FollowerSpec, observedSha: string | null,