0045 — An urgent fix ships at once, unless a commit riding along with it has a red heavy run

The problem

release is what production ships, and the heavy lane fast-forwards it only to a commit it proved green. The hotfix path skips that wait: after a green push:main CI run whose subject is [urgent] or Revert ", urgent-advance fast-forwards release to that commit. A fast-forward carries every commit between release and the urgent one.

2026-10-07 01:20Z: release had been parked at 42eb581f9 for about 2.5 h while main's heavy runs were red. #17285 [urgent] went green on push:main, and urgent-advance moved release to 9d0f47837 (Deploy Web run 37556553287). That carried 34 non-urgent commits to the web and the Lambdas. Four of them, 2162883bd, 17b92b2a0, ddaafb881 and 21f991354, had red heavy runs, and the heavy run on 9d0f47837 itself went red minutes later. Nothing broke in production, because all the failures were test-only. That was luck, not design. release-advance.mjs checked only that the move was a fast-forward.

Options considered

The decision

  1. Before the CI-triggered urgent fast-forward, release-advance.mjs urgent-check --sha <urgent> decides. It reads compare/release...<urgent> (the commits that would ride along) and the failed post-merge heavy runs (by event, status=failure). It holds (exit 10) when any commit in the range has a failed heavy run. The pure decision is decideUrgentRange, tested in scripts/__tests__/release-advance.test.ts. Pinned as a test (decideUrgentRange › "replay of 10-07 01:20Z" in scripts/__tests__/release-advance.test.ts, synthetic SHAs); replayed with live data on last night's range (42eb581f9…9d0f47837), it holds: red heavy run on 2162883bd, 17b92b2a0, ddaafb881, 21f991354, 9d0f47837.
  2. On hold: release is left alone, the step prints a ::warning, and a plain message goes to #alerts. The message names the commit and the override command. The post is fail-soft, like the deploy-failed alert. The job stays green, because a hold is a decision, not a failure. The urgent commit then ships in one of two ways: the heavy lane records green on a later main tip and fast-forwards release as usual, or a human runs promote-now.
  3. Commits with no heavy run yet ride along. Superseded, paced and still-running runs all count as "no run yet". Those commits passed a merge-queue train. Last night most of the 34 were in this state, not red. Requiring a heavy pass on them would turn option 2 into option 3, i.e. B.
  4. promote-now (workflow_dispatch with sha) skips the check. It is the human override, and the reason for skipping is logged by promote-now itself.
  5. Fail closed. An unreadable range or run list holds, and so does a range longer than the 250 commits the compare API lists. A check that crashed (any non-zero exit) holds too. A wrong hold costs one promote-now; a wrong advance ships unproven code to prod.
  6. Revert " commits are held the same way. This is a choice. The cure for a red range is the revert landing and the heavy lane going green on it, which moves release normally. That costs one heavy run: 4–12 min when the train proved the tree, 18–21 min otherwise (measured 10-07). During a production incident, promote-now ships the revert at once. The #alerts message prints the exact command. This consequence is stated in #17378's description and in the merge close-out on the bridge thread, so Fede sees it when he answers.

Why a second reading of "red"

main-red-hold.sh defines main as red by one signal (#14893): an open heavy-lane-red culprit issue, and "no merge to blame" does not count. That signal decides whether pushes to main re-run and deploy. This ADR reads a different thing: whether this specific range contains a commit the heavy lane saw fail. The two disagree in exactly last night's case. #17288 was "no merge to blame", so the culprit count was 0 and the deploy hold did not apply, while four commits in the urgent range had red heavy runs. Shipping a specific set of commits to prod should be judged on those commits' own evidence, not on whether triage could blame one merge. The culprit-issue definition is unchanged for everything it already governs.

What is not changed

Stated gaps

Rollback

The change is one guard clause in one step plus one script mode. Reverting the PR restores the plain fast-forward. There is no state to clean up, no ref is rewritten, and no branch protection is involved.

Relation to other ADRs

Build

Part Repo What State
1 propflowai urgent-check in release-advance.mjs + the hold in deploy-web.yml urgent-advance + control tests #17378 merged 2026-10-07T17:50:12Z (565f31b11)