Merge Speed Watch — what slows a change from "done" to "live"

48-hour live observation of every open PR · started Aug 19, runs through the morning of Aug 21 · LIVE — updated as findings land
Last updated: Aug 23, morning · Author: Claude (Fede's merge-speed session)
The one-paragraph summary. The pipeline itself is now fast: the median change goes from "PR opened" to "live in production" in 57 minutes (was 74 a day ago), and once a change is green it merges in about a minute. Nearly all remaining lost time comes from five specific traps, documented below with receipts — and the two biggest (the big-PR test trap and the silent auto-merge disarm) share one root cause: we verify every push instead of verifying the merge. A decision is proposed at the bottom.

Scoreboard — 24h after the first round of fixes vs. the 24h before

MeasureBeforeAfterRead
PR opened → change live in production (median)74 min57 minThe headline number. Target: 15 min.
PR opened → merged (median)54.5 min50.6 minModest — the median PR was already fast.
PRs merged in the day37922.5× throughput, nothing left waiting on a deploy.
Green → merged (90th percentile)18 min9 minWhen auto-merge stays armed, merging is instant.
Review comments per PR (average)5.53.7The founders' nit decision is holding.
Share of PRs taking >3 hours11%23%Mostly good news in disguise: the old stuck backlog (four PRs aged 9–16 days) finally drained and counted its full age on the way out.

Source: the locked pipeline-metrics.ts script (raw open→merge, no filters), snapshots Aug 19 19:37 UTC and Aug 20 19:41 UTC.

The five traps (each observed live, with receipts)

1. The big-PR test trap — decision proposed below

When a PR touches so many files that the "affected tests" selection overflows (200+ test files), the required test check deliberately waits for the full 12-machine suite — but the full suite only runs when a review verdict arrives. Any push after the verdict (rebases, fix-ups) waits forever: nothing re-triggers the suite. Observed: three PRs frozen this way (later rescued by manually re-requesting review — each rescue costing a full review round plus the full suite anyway). The rescue day spent as much on the big test machines as the entire rest of August combined ($40 in a day vs $78 for the month).

2. Silent auto-merge disarm

Any force-push — and even a stale "this PR has conflicts" flag that GitHub later retracts — quietly switches auto-merge OFF, and nothing switches it back on. Green PRs then sit invisible until a person notices. Observed: four separate incidents in 24h, including Fede's own session (the testing-doctrine rule merged the instant it was re-armed — its whole wait was disarm, 0% pipeline). The existing re-arm machinery only fires on the first review verdict, not on round-2+ verdicts.

3. Nobody owns the merge of workflow-file PRs (corrected diagnosis)

Correction (Aug 21): first written up as "review never fired" — that was wrong. The three green, untouched PRs (one 3.7 days old) all change only GitHub workflow files, a class where the bot review skips by design (GitHub forbids it) and auto-merge is policy-barred — so they need a deliberate human merge. The trap: no one owns that step. The PRs were green and mergeable the whole time; the process just has no "someone reads the diff and clicks" assignment. All three were read in full and merged tonight — including, ironically, the fix for the Aug 17 stale-production incident, which sat finished for 3.7 days.

4. Generated-file churn

Four PRs in one hour all touched the same auto-generated eval manifest. Each merge made every sibling's copy stale → red checks on innocent PRs → main itself briefly red → two extra "regenerate" commits to fix. The file is pinned per-branch but describes whole-repo state — it should be regenerated once at merge time, not raced on by every branch.

5. Dropped work in a stacked-branch rebase (caught & recovered)

A fully-reviewed change (4 review rounds — the "final reply must stand alone" fix that stops Clara claiming she sent a link she didn't) had been merged into another PR's branch, then silently thrown away when that branch was rebased. It existed nowhere in production for ~5.5 hours. The reviewer's re-check flagged it; the exact reviewed commit was recovered and re-landed as its own PR — open to merged in 15 minutes, hands-off. Watch item: stacked PRs + rebases can silently delete merged work; the review's "re-verify after rebase" habit is currently the only net.

Where an open PR's time actually goes (756 PR-hours, measured in 10-minute snapshots over 24h)

ShareStateRead
42%Deliberately held (labeled hold-for-review)By choice — mostly Gera's grading-playground stack. Excluded from stall math.
26%Green but idle — nothing running, nothing armed, waiting for a personThe waste. 201 hours of finished work sitting. Top offenders: the ownerless workflow-file PRs (trap 3) and PRs whose authors moved on.
20%Red check, waiting for the author to fixAuthor response gaps — a session/human attention problem, not machinery.
5%Green, armed, in the merge queueHealthy.
4%Checks actually runningThe pipeline computes for 4% of the time. Every speed problem we have is a waiting problem, not a compute problem.
1%Draft

Where the GitHub Actions money goes (August: ~$1,465 gross, ~$800 billed after credits)

ShareWhatCaught in the last 60 pushes
~61%The per-push CI battery (affected tests, type check, lint, six guard jobs, simulate) — re-run on every push, ~6 pushes per PR2 type errors. Everything else: zero real catches — authors (Claude sessions) already verify locally before pushing.
~34%89 small helper workflows — the largest boots a runner on every PR comment just to check if it says "@claude" (1 in 5 of all runs)Glue, not verification. 12,300 runs in 2 days.
~5%The 12-machine full test suite (escalation / nightly / main only)Legit — but manual big-PR rescues doubled its monthly cost in one day.

Decision: how to fix the big-PR trap (and most of the cost)

DECIDED Aug 21 (evening): keep watching — Fede chose not to build any of the options yet. Rationale recorded: cost cut #1's clean-day verdict lands Aug 22, and Gera's review-on-synchronize change (#6053, held for Fede) would already dissolve the disarm trap and un-deadlock big PRs by a different route. The options below stay on file; revisit if the Aug 22+ numbers miss the ≤$80/day target or the traps recur.

Option 1 — Big PRs run the full suite on every push

Simplest possible rule; no stuck states. Rejected by Fede Aug 19 — too expensive (every push on a big PR = a $1.20 full-suite run).

Option 2 — The patch: full suite auto-runs on pushes that land after a verdict

One-line-ish change to the escalation rule. Kills the deadlock exactly where it bites, costs ~22¢ per post-verdict push — cheaper than today's manual rescues (which buy a full review round + the suite anyway). Leaves everything else (cost, disarms, generated-file churn) as-is. Low risk, ships in an hour.

Option 3 — The redesign: verify the merge, not every push RECOMMENDED

Per push: nothing (or a 1-minute incremental type check — the only per-push job that caught anything). At merge: the entire battery, once, on the true merge result — full suite, guard pins, lint, security scan, and regeneration of generated files.

This one change kills traps 1, 2-partially, and 4 outright: no affected-vs-full modes, no escalation machinery, no stuck test checks, no per-branch generated-file races — and cuts the biggest cost bucket ~5–6× (one battery per PR instead of ~6). Honest tradeoff: a genuinely broken PR bounces at the merge queue (~10 min round trip) instead of failing fast on push — at the measured 1–3% failure rate, a rare small delay. Bigger build: a few days, staged.

Option 4 — Both, staged: ship the patch now, build the redesign behind it

The patch un-deadlocks big PRs today; the redesign then deletes the machinery the patch lives in. Right choice if the redesign timeline matters less than never seeing the trap again this week.

Recommendation: Option 3 (or Option 4 if you want the bleeding stopped this week.)

Cost reality-check (verified Aug 21): the easy trigger-level levers are already pulled — every heavy workflow already cancels superseded runs on a new push, and the comment-triggered runner boots were measured at ~$0 each. An earlier draft of this doc claimed a 30–40% cut from those; that was wrong and is retracted. With spend at $145 → $170/day (all grant credit — $0 cash today, but the $10k credit is ~2 months deep at this pace, then it's ~$5k/month real), the only lever that halves the bill is structural: stop paying for the full battery ~6× per PR. That is Option 3. The one surviving no-tradeoff fix: re-arm auto-merge on every verdict, not just round 1 (fixes trap 2; costs nothing).

Trap 3 ownership — RESOLVED Aug 21 (evening): the daily digest is built (PR #6055, workflow-pr-digest.yml). Once a day, #alerts gets one plain-English message listing every finished, green workflow-only PR waiting for a human read-and-merge, with ages; quiet days post nothing. It never merges anything itself. The watch session keeps clearing them opportunistically while it runs; the digest is the durable backstop that survives any machine shutdown.

Live log — added as found

Aug 23, morning — cost verdict: target HIT on the first clean day
Aug 22 closed at $54.45 — under the ≤$80 target and under the ~$63 pre-spike norm. The four-day line: $168.54 → $137.23 → $54.45, a 68% drop from the peak. One honest caveat: Aug 22 was a Saturday, so PR volume was lighter than a weekday — the cut is clearly working, but the confirmation under full load is Monday Aug 25's number. (Aug 23, also a weekend day, is at $29.12 by morning.) Read: cost cut #1 (guard pins post-merge only) plus the quieter board appear sufficient; the shelved "verify the merge, not every push" redesign stays shelved unless weekday spend climbs back above target. Trap-3 digest: first scheduled run fired Aug 22 15:53 UTC and behaved exactly as designed — no finished automation-only PRs were waiting, so it posted nothing.
Aug 22, morning — cost verdict, day 1 after the guards cut
Aug 21 closed at $137.23 — down 19% from the $168.54 baseline, but well short of the predicted ~$115–125. The mid-day projection missed because the evening was unusually busy (the digest workflow's own PR ran three review rounds, several reviews were re-fired on stalled PRs, and Gera pushed a fresh batch) — and Aug 21 wasn't a clean day anyway (machine down all morning suppressed pushes, then the backlog burst in the evening). Aug 22 is the honest test and it has started well: $28.96 by 9am Denver (Aug 21 was already ~$45–50 by this hour on its way to $137). Too early to call ≤$80; the full-day number lands tomorrow morning. Speed: no stalls this morning — the two PRs caught green-with-no-review in overnight sweeps (#5807, #6080) were both handled (one turned out to be a held-for-Fede diff, the other merged after a re-fired review). The trap-3 digest workflow is live on main; first scheduled post 9:47am Denver.
Aug 21, evening — machine restart; watch moves to a lighter cadence
The Mac was shut down mid-day and is back up. The GitHub-side machinery (auto-merge, re-arm, reviews, alerts) ran fine on its own the whole time — nothing got stuck, and no ownerless workflow-file PRs accumulated. The heavy local collectors (10-minute snapshots, webhook feed) are not being restarted: the 48-hour observation is complete and its data is archived. The watch continues on a lighter cadence — a periodic stall sweep plus the daily cost check.

Cost: Aug 20 closed at $168.54 (the "$170" baseline, now exact). Aug 21 is at $98.63 by evening — on track for the predicted ~$115–125 after cost cut #1. Aug 22 is the first clean full day and the real verdict (target ≤$80 gross).

New development that bears on the decision below: Gera opened a change ("review on synchronize", #6053) that makes the review bot automatically re-review after every fix-push, with evidence that 45 of 48 recent watchdog pages trace to reviews never re-firing after a push. If it lands, it largely dissolves trap 2 (every push gets a fresh verdict, which re-arms auto-merge) and un-deadlocks trap 1 by a different route (a verdict on the new head re-triggers the big-PR full suite). It is cost-bounded per its analysis (push bursts cancel their own older runs before any work happens), but it does mean more review/full-suite activity per PR — it leans against the "verify only at merge" redesign's premise, or at least changes its starting point. It is correctly held for Fede (touches security-relevant workflow files, and flips an ADR from deferred to adopted). Read it before picking an option below.
Aug 21, 13:45 UTC — morning update
Cost: first partial day after the guards cut: $72.53 by mid-day, tracking ≈$115–125 vs $170 yesterday — consistent with the predicted ~⅓ cut; the first full clean day (Aug 22) is the real verdict. Guards confirmed skipping on every PR push. Speed: 27 PRs merged overnight; green→merged median holds at ~6 seconds. The trailing 24h median (67 min) is polluted by the backlog drain — the days-old PRs merged during the sweep count their full ages (p90 shows a 6-day-old PR). Trap 3 recurred twice in 12 hours (two new green workflow-only PRs sat ownerless; both read in full and merged — a nightly-harness credentials fix and a stop-paging-over-successful-reviews fix). The class needs a standing owner or digest — proposal added to the decision section.
Aug 21, 04:15 UTC
Cost cut #1 SHIPPED (Fede's directive: halve the bill): guard pins no longer run on PR pushes — post-merge only, where they already ran and already feed the red-main alert. They were 57% of all per-push CI minutes with zero author-bug catches in 60 pushes. Expected effect ≈ one third of total Actions spend ($160 → ~$105-110/day); tomorrow's billing shows the real number. The remaining stretch to half is the Option 3 decision above. Also verified & retracted earlier: superseded-run cancellation was already in place everywhere, so no savings existed there.
Aug 21, 03:30 UTC
Trap 3 re-diagnosed (see corrected section above) and cleared: all three ownerless workflow-file PRs read in full and merged — the Aug-17 stale-prod fix (3.7 days old), the mis-pinned-review repair (which itself fixes a machinery bug from this report), and the touch-budget alert-fatigue fix. Phase-decomposition of the full 24h snapshot data added: checks run 4% of the time; green-idle is 26%.
Aug 21, 01:50 UTC
Gera's PRs brought into active watch (Fede's request). Found trap #3 (reviews never fired: #5975, #5967, #5827 — re-fired tonight); #5981 waits on Gera's answer to review suggestions; two PRs have real merge conflicts only he can resolve; nine are deliberately held (grading-playground stack) and excluded from stall math.
Aug 20, 14:13 UTC
The application-link PR merged (Fede's explicit click — correctly held by the permissions-file hard floor). Full app-link family now live.
Aug 20, 03:40 UTC
Trap #5 recovery: the dropped "final reply must stand alone" work re-landed (15 min, hands-off).
Aug 20, 03:30 UTC
The two deadlocked big PRs (label-quality sweep, topic-labeler corpus) merged after rescue. Main briefly red from trap #4; healed by peer sessions within the hour.

Data sources: locked pipeline-metrics.ts · 10-minute lifecycle snapshots of every open PR (48h) · GitHub billing usage API · per-job analysis of the last 60 CI runs · incident receipts in the session log. Overnight collection continues; morning update lands ~7:30am Denver.

PropFlow Docs