CI/CD architecture: from opening a PR to proven in prod

2026-09-30 · Target is Proposed — pending Fede review · Data: GitHub REST, AWS Cost Explorer, Vercel, 30 days · "est." marks every estimate

Speed is already good; stability is not. A median PR merges in 22 minutes, but 1 in 5 runs on main fails, mostly because two PRs that were each green break main together.

Fix: test each PR against the latest main before it merges (a merge queue), then deploy that exact result with a small canary and automatic rollback.

Same quality bar, fewer machines, far fewer red builds.

New (2026-10-03): CI rethink: stop testing the same change seven times — the first-principles review and five proposed moves.

Where we are now (Oct 1, ~9 AM MT)

Stability bar (Fede, Oct 1): "smooth for 24 hours — nothing failing, or very rare". Not met yet.

Today vs goal
MeasureYesterdayTodayGoal
Time to merge (PR opened → merged, median)2.1 h~50 min< 30 min Oct 2 afternoon, < 15 min this week
Main redOvernight, before the merge queue: 172 of 187 main CI runs failed (Oct 1, before 4:49 AM MT)Since the queue: 6 failed, 7 passed, 12 cancelledRare
Merge-queue runs since 4:49 AM MTn/a60 passed, 41 failed. Most failures come from two checks that only run in the queue (guard-pin kinds file, workflow grep markers). Fix in flight: run them on every PR, and stop the kinds file going stale when two PRs are batched.Rare
CI costSep 30 ≈ $310/day for CI (on-demand $180 because the spot quota ran out; quota raised 512 → 1,024 vCPU)Oct 1 estimate lower, not yet in billing< $65/day, CI only (the app's own AWS costs excluded). A costed plan to reach it is being built.
Runs per PR push13.318.3 (worse; see One machine per push · One run per PR push below)2
By theme
ThemeStatusDoneIn progressNot started
Keeping main green🔄Merge queue on since 4:49 AM MT; every required check now reports in the queue; automatic revert after a bad merge; the red-main fixesMoving the two queue-only checks onto every PR; stopping the guard-pin kinds file going stale when two PRs are batchedOpen question: should the after-merge lane open revert PRs at all (with Fede)
Speed to merge🔄CI starts ~2 minutes sooner; guard pins split into 6 shards; the merge helper only reacts to CI completionsDangerous-diff hold enforced by a required status (then the old helper is deleted); queue-branch filter for follow-on workflowsOne runner spec (163 copies down to 1)
Cost🔄Spot quota raised 512 to 1,024 vCPUReviews no longer re-run unit tests unless owed (~400 fewer 8-core runs a day); a costed plan to reach < $65/daySingle-machine builds (one machine per PR push instead of ~13+), being priced now; fewer runs per push (13.3 to 18.3 today, goal 2)
Pipeline reliability🔄Watchdog restarts runs frozen by lost machinesQueue-only checks moved onto PRs (Fable, PR pending)Flaky-test quarantine
Deploys🔄Worker and Lambda deploys target the main tipRenewal worker deploys only forward of what is running (a stale worker re-sent ~134 application emails 2:33-3:25 and 5:33-7:14 AM CT)Canary + automatic rollback for deploys
Cleanup🔄Test prune part 1 (41 files); several helper workflows folded into one; short-jobs-on-demand idea closed as not neededDelete the old auto-merge helper once the required status is liveRest of the test prune

Open question for Fede: should the after-merge lane open revert PRs at all? Yes / hold.

Every PR, for engineers — where each idea stands (updated 2026-10-01 9:00 AM MT) — ✅ done · 🔄 in progress · ⬜ not started · ✖ dropped

Shipped today

WhatPR
Merge queue on (4:49 AM MT)n/a
Guard pins run as 6 shards#14755
All 7 required checks report in the queue#14765
After-merge auto-revert#14790
Auto-merge helper trimmed to CI completions#14788, #14821
CI starts ~2 min sooner#14806
Watchdog restarts runs frozen by lost machines#14811
Test prune part 1 (41 files)#14420
Queue-branch filter for follow-on workflows#14835 merged Oct 1, 9:10 AM MT
Queue-only checks (test list, grep markers) now block on PR runs too#14845 merged Oct 1, 9:12 AM MT
Generated test list never fails a merge batch (regenerated in CI; missing entry is a warning)#14855 merged Oct 1, 9:35 AM MT
Short jobs on on-demand machines: closed, not needed#14543 closed

In flight

WhatPR
Reviews no longer re-run unit tests unless owed#14836
Renewal worker deploys only forward of what is running#14825
#15043 — guard-pins jobs on on-demand arm64 inside the merge queue only (spot reclaims killed 6 of 16 failed batch jobs on 2026-10-01; retry=when-interrupted re-runs after the workflow ends, too late for a batch); measured premium ≈ +$4.7/day; companion to Opus's #15019.#15043
auto-merge-all.yml deletion: split into PR1 (dead tests, 550 lines) and PR2 (workflow + fence pin), stacked behind #14992 (symbol sweep fix); decide-guard-abort.sh deletion waits for the sweep to ignore shell variables.—
#15052 — pre-queue check's own shards on on-demand arm64 (the gate must not be spot-killed; first proof run died on a spot reclaim and RunsOn's retry never fired, run_attempt stayed 1); armed.#15052
fence workflow off the x64 pool (last pool=ci-2cpu-x64 references, 3 lines in dangerous-diff-fence.yml) → then retire the x64 pool in runs-on.yml; PR being cut.—

All ideas, by status

IdeaStatusProofEffect
Done (77) — newest first
Last x64 jobs moved to the arm pool✅ Done
merged Oct 1
#15001x64 pool jobs moved to the arm pool; merged 2026-10-01 as d01bbdb08 (kicked once from the queue by a spot reclaim, re-added).
Pre-queue check env-leak fix✅ Done
merged Oct 1
#15018Pre-queue check: GUARD_PIN_KINDS scoped to the resolve step (the job-level env leaked into vitest and failed every PR); merged 2026-10-01 21:0x UTC as 21e7d2b44; workflow re-enabled; live proof passed: run 36921785512 on #15001, status pre-enqueue=success (2026-10-01 21:0x UTC); becomes a required queue check when the queue is turned back on.
Shared dangerous-diff fence workflow✅ Done
merged Oct 1
#14830#14830 — Shared dangerous-diff fence workflow (one label + hold job instead of per-workflow copies); merged 2026-10-01 (e3586c554), backfill over open PRs green. Next: the old auto-merge-all workflow is deleted by the CI/CD Opus lane.
Pre-enqueue check✅ Done
merged Oct 1
#14948#14948 — Pre-enqueue check merged 2026-10-01 (a525c9003). First live runs exposed an env-leak bug (job-level GUARD_PIN_KINDS reached the vitest step, coverage drift test failed on every PR); the workflow is disabled until fix #15018 lands, then a live proof, then it becomes a required queue check together with the CI/CD Opus lane.
Deploy adoption job runs on the arm pool✅ Done
merged Oct 1
#14902Deploy adoption job moved to the arm pool (commit e57bec753)
Glue jobs (non-test helper jobs in the PR workflow) run on the arm pool✅ Done
merged Oct 1
#1488829 of 33 pool labels now arm (commit 5d4449ab7)
One-machine shadow run (measurement only, advisory) live✅ Done
merged Oct 1
#14880Removal PR due 2026-10-03 ~17:00Z after comparing its efficiency against the per-job lane (commit 31fb83624)
"Rerun review on infra failure" tolerates 403 race✅ Done
merged Oct 1, 8:19 AM MT
#14774Handles already-running review runs without failing
Auto-merge guard fires once per CI completion✅ Done
merged Oct 1, 7:58 AM MT
#14821Claude Code Review dropped from its triggers; review events only for human reviewers
Test-file prune (~900 shallow files)✅ Done
merged Oct 1, 7:46 AM MT
#14420 first deletion batch openFewer low-value tests to run.
Test prune✅ Done
merged Oct 1, 7:46 AM MT
#14420Test prune: dead and duplicate test files removed
CI followups: review re-fire✅ Done
merged Oct 1, 6:46 AM MT
#14799review re-fire (CI-completed leg), rerun-review-on-infra-failure and main-lane-red-alert folded into the same ci-followups.yml; main-lane-red-alert.yml deleted; 5 jobs watch 6 workflows (part 2 of one-run-per-push)
CI followups: rerun-shutdown + adopt-tests fold✅ Done
merged Oct 1, 6:09 AM MT
#14785rerun-on-runner-shutdown + adopt-unit-tests folded into ci-followups.yml (2 → 1 runs per CI completion, part 1 of the one-run-per-push plan)
Drift tests: every ElevenLabs read retries✅ Done
merged Oct 1, 6:08 AM MT
#14784Every live ElevenLabs read in drift tests retries upstream 5xx via one shared helper (8 scripts, 6 tests)
smoke-wo-bugs test mock logging name✅ Done
merged Oct 1, 5:32 AM MT
#14781Test mock gains the logging name it was missing (fixes red main shard 1/2)
import-line probe syntax-tree ratchet skip✅ Done
merged Oct 1, 5:23 AM MT
#14779The ratchet no longer flags import-line probe in human-trigger test (was reddening every main run and queue batch)
Live ElevenLabs drift test retries upstream errors✅ Done
merged Oct 1, 3:53 AM MT
#14735One unit test fetches live ElevenLabs agents; a single upstream 500 reddened main at aae1b4684 (1 of 60,328 tests). The fetch now retries 3 times with backoff on 5xx/429/network errors; 4xx still fail at once. Unit test asserts the exact backoff schedule.
Drop every gen-5 runner token✅ Done
merged Oct 1, 3:00 AM MT
#14724RunsOn prefix-matches family tokens, so m5/c5/r5/m5n/c5n/r5n still admitted m5ad and m5n. All 17 families on x86 labels are now gen 6/7; a test forbids all nine gen-5 tokens. Verified: zero gen-5 launches in the following 20 min.
Heavy drift tests go async✅ Done
merged Oct 1, 2:52 AM MT
#14718Four tests that blocked the vitest worker with sync child processes (Lambda builds, grep sweeps) now await them; fixes "Timeout calling onTaskUpdate" exit-1 on shards where every file passed.
Lint truth-table test in-process✅ Done
merged Oct 1, 2:28 AM MT
#14710The 8,192-combination lint verdict test runs in one bash process instead of 8,192 spawns: 22 s → 2 s, and no more 60 s worker timeouts on guard shards.
Drop first-gen AMD runner families✅ Done
merged Oct 1, 2:20 AM MT
#14712Removed m5a/c5a/r5a from every x86 runner label; those tokens let m5ad.xlarge in, which ran guard shards 2x slower and took every spot interruption tonight.
Fence-only PR guard pins✅ Done
merged Oct 1, 1:48 AM MT
#14696PR runs execute only the 698 fence guard pins (1,034 → 699 files); bookkeeping pins stay on every main push and in the half-hourly heavy lane. First concrete cut toward one run per push.
Runner instance pool✅ Done
merged Oct 1, 12:48 AM MT
#14536Wider x86 instance-family pool on every runner label (m5/c5/r5 and newer families added). Done (merged 0c4045331).
Wider x86 machine family pool✅ Done
merged Oct 1, 12:48 AM MT
#14536166 label sites
Spot placement✅ Done
merged Oct 1, 12:10 AM MT
#14570Done (merged a93b837c8): every runner label now uses capacity-optimized spot, no lowest-price anywhere.
Spot strategy✅ Done
merged Oct 1, 12:10 AM MT
newLast three 'lowest-price' labels become 'price-capacity-optimized' (fewer interruptions, fewer fallbacks to guaranteed machines)
Stacked PRs left unarmed✅ Done
merged Sep 30, 11:27 PM MT
#14535PRs based on another PR's branch are left unarmed until the base merges, daily sweep finds the lost ones
Auto-merge guard✅ Done
merged Sep 30, 11:27 PM MT
#14535A PR whose base is not main never arms; the stacked-base sweep is declared in the automations config. Done.
Guard pins✅ Done
merged Sep 30, 11:14 PM MT
#14649Guard-pin kinds sidecar regenerated; drift is now advisory so two pin PRs in a row can't redden main. Done (merged 710bd6a73).
Renewal worker deploy✅ Done
merged Sep 30, 10:45 PM MT
#14583Renewal worker deploy waits for the ECS service to be stable instead of a fixed sleep. Done.
Small jobs moved✅ Done
merged Sep 30, 10:39 PM MT
#14625All 32 small x86 CI jobs moved to the 6-zone fleet. Merged 5d37b22d0 at 10:41 PM MT. Rollback is a revert.
Guard pins✅ Done
merged Sep 30, 10:38 PM MT
#14582Guard pins classified fence vs bookkeeping (metadata only). Done.
Guard-pin classification✅ Done
merged Sep 30, 10:38 PM MT
newEvery pinned guard test tagged fence vs bookkeeping (metadata only) for the check diet
New runner fleet✅ Done
merged Sep 30, 10:23 PM MT
#14612Canary for the new 6-zone runner fleet in us-east-1 (flex). Merged 1a2ce86a8. Proof: run 36814907882; the flex-labelled job ran on a machine owned by the new fleet, the control job stayed on the old fleet.
Runner network Terraform✅ Done
merged Sep 30, 10:19 PM MT
#14538Terraform for a 6-zone runner network, code only, nothing applied (on hold pending study)
Review-verdict watchdog✅ Done
merged Sep 30, 9:49 PM MT
#14377 (🔴 fixed, re-armed)Catches reviews that never post a verdict.
Review-verdict watchdog✅ Done
merged Sep 30, 9:49 PM MT
#14377Catches reviews that never post a verdict.
Guard-pin shards on bigger machines✅ Done
merged Sep 30, 9:42 PM MT
#14540Guard-pin test shards move to 4-core machines (≈30–40% shorter)
us-west-2 runner fleet✅ Done
merged Sep 30, 9:20 PM MT
#14563Done.
US West canary✅ Done
merged Sep 30, 9:20 PM MT
newDispatch-only canary workflow + label branch for the 8-core arm jobs; waits on the GitHub App click
US West runner fleet✅ Done
merged Sep 30, 9:17 PM MT
newStack runs-on-usw2 created in us-west-2 (first attempt rolled back: AWS budget names are account-global); needs the GitHub App click, ask posted to #agent-trinity
us-west-2 runner fleet✅ Done
merged Sep 30, 9:17 PM MT
#14586Heavy arm test jobs routed to the second runner fleet in us-west-2. Merged.
Fleet automation alarm note✅ Done
merged Sep 30, 9:02 PM MT
#14534Alarm note in the fleet automation config
Red main fix✅ Done
merged Sep 30, 8:45 PM MT
#14569Red main fixed: two test fixtures named a real customer's property id; anonymized. Merged 8:45 PM MT (admin merge under the red-main claim). Main was red 7:00–8:45 PM MT.
Red main fix✅ Done
merged Sep 30, 8:19 PM MT
#14558Three guard tests tripped by product PRs (one allowlist row, one rename, one export pin)
Red main fix✅ Done
merged Sep 30, 8:16 PM MT
#14552Two cron workflows registered in the schedule manifest (Schedule Drift was red on every PR)
Red main fix✅ Done
merged Sep 30, 8:05 PM MT
#14548Two CI jobs carried the old machine label shape
A test shard cancelled by a newer run counts as no verdict, not a red (#14149 false red)✅ Done
merged Sep 30, 7:35 PM MT
newCancelled shards stop showing as failures.
Cancelled test shards on a PR✅ Done
merged Sep 30, 7:35 PM MT
#14530Cancelled test shards on a PR no longer count as a failure (push to main still fails closed) — merged
Review re-fires after a dismissed verdict✅ Done
merged Sep 30, 7:32 PM MT
#14487A dismissed review gets a fresh one.
Merge-queue batch checks run on non-spot machines✅ Done
merged Sep 30, 7:27 PM MT
#14477 mergedThe first queue batch died to a spot reclaim; batches of 10 PRs must not be lost to one machine.
Merge-group runs on steady (non-spot) machines✅ Done
merged Sep 30, 7:27 PM MT
#14477One machine loss no longer rebuilds a batch.
One-workflow piece 17 (override/repin fold)✅ Done
merged Sep 30, 7:21 PM MT
#14436Merged
Review-check override folded into CI✅ Done
merged Sep 30, 7:21 PM MT
#14436One fewer separate workflow.
Red main 6:37 PM: tour-wording test vs #13907, fix-forward PR, no revert (Fede: no reverts)✅ Done
merged Sep 30, 6:59 PM MT
newFix goes forward; nothing is reverted.
Red main fix-forward✅ Done
merged Sep 30, 6:59 PM MT
#14527Red main fixed forward: test updated to the new tour-window wording
Post-merge main run skips test lanes a merge-group batch already validated✅ Done
merged Sep 30, 6:36 PM MT
#14455 open, armed; one review fix in flight (must not skip while main is known-red)saves up to ~63 machine-minutes per merge (7-day measurement)
Main skips lanes a merge group already validated✅ Done
merged Sep 30, 6:36 PM MT
#14455Saves machine time on every merge.
PRs never wait on the heavy test lane after approval✅ Done
merged Sep 30, 6:32 PM MT
#14491Approved PRs no longer sit behind the heavy lane.
Push pacer (fewer CI runs per PR)✅ Done
merged Sep 30, 6:27 PM MT
#14282 (merged up with main, armed)Fewer CI runs per PR.
Push pacer✅ Done
merged Sep 30, 6:27 PM MT
#14282Fewer CI runs per PR.
Main deploys were starving✅ Done
merged Sep 30, 6:21 PM MT
#14505A newer push cancelled the older run before anything deployed; 30-min guard added.
Unit Tests decision job waits without holding a machine✅ Done
merged Sep 30, 5:57 PM MT
#14460 open, armedFrees a machine while the job waits.
Red-main test fixes (tests lagging today's workflow edits)✅ Done
merged Sep 30, 4:28 PM MT
#14434, #14444, #14449, #14446main green on checks; three test-only reds fixed within ~40 min each
Merge queue (batch PRs, one CI run per batch)✅ Done
merged Sep 30, 4:17 PM MT
#14440Merge queue turned OFF at 5:38 PM MT on Fede's call (one spot-machine loss rebuilt the whole batch and merges stopped for 75 min). Separate initiative; revisit only after the merge-group runs use steady machines (#14477). Update: back on since Oct 1, 4:49 AM MT.
Unit tests: 2 shards on PRs, full suite deferred to main✅ Done
merged Sep 30, 3:06 PM MT
#14402, #14423A PR runs about 2 minutes of unit tests instead of the full suite.
Harnesses image fix (ripgrep install)✅ Done
merged Sep 30, 3:05 PM MT
#14422The harnesses image builds again.
Heavy guard pins run only after approval / on main✅ Done
merged Sep 30, 3:03 PM MT
#14409The 30-minute guard lane no longer runs on every push.
Advisory checks don't block merges✅ Done
merged Sep 30, 3:01 PM MT
#14414A non-required check can no longer hold a merge.
One CI workflow, runner spec pinned once (was pasted in 163 places)✅ Done
merged Sep 30, 3:01 PM MT
#14267, #14354Every job uses one of two machine sizes.
Deploy main tip for lambdas/worker✅ Done
merged Sep 30, 2:53 PM MT
#14289Deploys no longer pin to the triggering commit.
Drift tests on every PR✅ Done
merged Sep 30, 12:18 PM MT
#14323Drift breaks are caught before merge.
x86 runner pools✅ Done
merged Sep 30, 11:55 AM MT
#14317Avoids the Graviton capacity stalls.
Auto-merge guard workflow: paused today for catch-up✅ Doneworkflow active again (checked Oct 1, ~9 AM MT)Temporary, to drain the backlog.
Repo-admin bypass on the "Require CI" ruleset✅ Donebypass list empty (checked Oct 1, ~9 AM MT)Temporary, to drain the backlog.
Spot vCPU quota 512 → 1,024✅ DoneAWS approved✅ 1,024 live at 4:10 PM; EC2 still enforcing the old count for ~30 min after
Third runner fleet✅ Doneruns-on-flexNew RunsOn fleet in us-east-1 across six availability zones (10.3.0.0/16), built with Terraform, state in S3. GitHub App installed by Fede.
Spot alarm✅ DonenewSpot vCPU alarm threshold raised 410 → 820 (80% of the 1,024 quota)
Push throttle✅ DonenewBuilders skip the push pacer tonight (documented skip, Fede wants speed)
Ownership split✅ DonenewCI/CD Opus: PR backlog, merge loop, red main and deploy health. CI/CD Fable: runner fleet, CI code.
In progress (1) — see also "In flight" above
Worker deploy waiter🔄 In progress(no number yet)Worker deploy: 'wait for service stable' now tolerates one ECS task replacement (was timing out 71 s short). Building.
Not started (4)
Guard re-arms on freeze clear⬜ Not startedissue #14474Auto-merge resumes by itself after a freeze.
Vercel preview / UI verify only after review verdict⬜ Not startedproposedSkips preview builds on PRs that get revised.
Flaky-test quarantine⬜ Not startedranking done, list in sessionFlaky tests stop blocking unrelated PRs.
Follow-ups from today's review rounds (12 small CI issues)⬜ Not startedissues #14463–#14479Rerun shards after a red-main fix, keep approval on unchanged diff, guard hand-disarm, missing-verdict refire, worker deploy retry, and more.
Dropped (3)
Main's heavy shards on steady machines✖ Dropped#14520Main runs stop dying to spot loss.
Dropped: reverted by #14777 (merged without Fede's cost yes)
Short 2-vCPU jobs on guaranteed machines✖ Dropped#14543On cost hold (≈ +$1,800/mo estimate), yes/hold with Fede
Dropped: closed as not needed
Main-branch heavy test shards on guaranteed machines✖ Dropped#14520On cost hold, same reason
Dropped: reverted by #14777 (merged without Fede's cost yes)

Today's numbers: queue peaked at 414 runs waiting (1:20 PM), 158–232 through the afternoon; 41 PRs merged 2:30–5:00 PM; main green since 5:04 PM; at 5:25 PM the merge queue held 20 PRs (18 Gera's) with the first 10 running as a batch; fleet 170 machines. 5:30–6:10 PM: 97 PRs merged after the queue came off (vs 4 merges 4–5 PM). 6:35 PM: Fede's 37 open PRs: 10 armed green/awaiting review, 26 red or conflicting, builders assigned; 9 of his merged in the following hour. Production web still at the 5:01 PM build at 7 PM because every main run since failed the one tour-wording test; deploys fire only on a green main. 7:50 PM: Fede's open PRs 24 (from 38 at 6 PM); 41 PRs merged in the previous hour; production on the 7:00 PM build; 108 spot machines running, 142 jobs queued; 90 spot no-capacity errors in the last hour. 8:35 PM: main red 7:00–8:22 PM on guard tests, fixed forward with three PRs; production still on the 7:00 PM build; 22 PRs merged in the previous hour; Fede's open PRs 26; 133 machines running, 160 queued; ~17 orphan machines at any time (job cancelled before pickup, killed at 10 min, not tunable).

Cost finding, 8:30 PM

Baseline before the one-run flip (measured 9:45 PM MT, 447 PRs merged in the last 24 h)

MeasureValue
Open to mergemedian 124 min, p90 729 min; 67% of it before the final push
Single-push PRsmerge in a median 27 min
Final push to all six required checks greenmedian 14.3 min, p90 52 min
Check that finishes lastUnit Tests, in 353 of 420 PRs
Runner queue wait per jobmedian 0.6 min (not the bottleneck)
Unit Tests decision waits on its chainmedian 12.6 min, p90 48 min (about 6 serial machine boots)
Pushes that are just "merge main"520, 34% of all pushes, each re-running full CI
Review bot push to approvalmedian 6.7 min

Target after the flip and the chain-flattening PR: final push to required green at or under 8 minutes median; measured on real PRs, not inferred.

Source: GitHub Actions and PR API, 24 h window ending 9:45 PM MT 2026-09-30 (CI/CD Opus session).

Fleet before the flex cutover (10:30 PM MT, 30 minutes)

Old fleet: 1,172 launches, 36 launch errors, 4 spot interruptions. Flex fleet: 1 launch, 0 errors. The 30-minute comparison after the cutover is logged in ~/.claude/logs/ci-decisions.log and will be added here.

Fleet 30 minutes after the cutover (10:41–11:11 PM MT)

Old fleet: 875 launches, 51 launch errors (5.8%), 9 spot interruptions. Flex fleet: 578 launches, 19 launch errors (3.3%), 1 spot interruption. Cutover kept.

Ghost queue runs

90 workflow runs dated Aug 3–Sep 30 sit in GitHub's "queued" state with zero jobs and cannot be cancelled (cancel and force-cancel both return 409). Every counter (watcher, push pacer, queue watch) now counts only runs created in the last two hours. A GitHub support ticket is on the morning list.

Before → after

MeasureTodayTarget (est.)
Merge → prodweb median 18 min, p90 49; worker/lambdas hours on bursts~20 min, every lane
Workflow runs per push32 → 15 (today's fold)~3
Machine-min per PR~176 (55/push × 3.2 pushes)~75
Runner $/month$219/day on Sep 29 (59% on-demand)$1.5–2.5k/mo
Main runs failing20% (202 of 1,000)under 3%
Merges/day181 main commits/day; 341 PRs Sep 29~800/day capacity
Test files8,236~7,500

Top 5 mistakes

  1. No PR is tested against latest main before merging.
  2. Review 🔴 blocks on non-bugs (57%); pushes continue after 🟡.
  3. 22 runs per push; about 12 do nothing.
  4. Deploys pin old commits; nothing checks prod behavior.
  5. 59% of runner spend is on-demand fallback.

Top 5 opportunities

  1. Merge queue; delete the rerun on main.
  2. 🔴 only for real defects; one verdict per commit.
  3. One CI workflow, one review workflow, one runner spec.
  4. Deploy main tip, canary, auto-rollback.
  5. Quarantine flaky tests; prune 899 low-value test files.

Next 3 actions

  1. Deploy the main tip for lambdas and worker; alarm at 45 min.
  2. Always run the drift tests on every PR.
  3. Flaky-test quarantine with a 7-day fix-or-delete.
1 · The pipeline today, gate by gate
1 · BEFORE PUSH (laptop)2 · EVERY PR PUSH (22 runs)3 · MERGE4 · AFTER MERGE (main)5 · PROD CHECKreview:self + typechecklocal, ~6 min wallbody, size, ratchetspre-push unit suitelocal, ~6 minsame tests CI rerunsCI (50 jobs)43 machine-min, 11 min wall2.3 runs/push · requiredClaude review (Opus)4-8 min · 1.1 runs/push🔴 = red required checkNested Gate0.5 min · required by rulesetReview helpers (3)override, re-pin, recheck4.3 runs/push, ~75% skippedAuto-merge on green3.9 runs/push, 60% skip/cancelGuard + previewdispatch guard, preview domain2.4 runs/push, not merge gatesNo-op satellites (3)@claude, docs fence, dependabot3.5 runs/push, 100% skippedEvals + harnessesSemgrep too; path-filtered2-7 min eachAuto-merge guard3,516-line script, rules 0-9decides when to arm mergedangerous-diff holdPR never meets maintwo green PRs canbreak main togetherFull CI again on main~181 runs/day20% fail · 60% cancelledtests after the fact30-min promote windowweb waits for a batchurgent/revert bypassDeploy lambdas + workerpins the SHA, not main tipworker ran ~2 h behindon Sep 30Vercel READY + page warmcatches build errors onlyFreshness alarmpages at 120 min behindtoo loose (2 h lag missed)Rollback by handneeds a person + Fede goweb instant; 10 lambdasand worker: noneNo behavior checkSep 16 calls outagefound by a humankeepfold into one jobadvisory / nightlydelete or replace

Every gate from laptop to prod, colored by what we should do with it. Numbers: 25 pushes, 551 runs, fetched today.

Seven checks are required to merge (Build, Type Check, Unit Tests, Lint, Leasing regression, review, Nested Gate). Everything else is extra. Already removed from the push path today: PR body gate, file collisions, size tier, docs fence, Semgrep, Quality Gate POC, Dependabot gate, lambda bundle check.

2 · Where the PR hours go
PRs with 3+ review rounds: 1,571 h (68%)1-2 rounds: 732 h (32%)600 merged PRs, 2,303 open hours. 38% of PRs had 3+ rounds and took 68% of the hours.wait for CI 0.35 hbot review 0.08approval 0.04merge 0.03merge → prod ~0.4 h (est.)Median PR (n=70): open → merged 0.36 h, then ~0.4 h to prod (web; worker and lambdas can lag hours).

Review loops, not CI speed, eat the hours. Top bar: 600-PR sample. Bottom: median PR timeline.

3 · Why main goes red, and what removes each cause 🟡 In progress merge queue ✅ Done drift tests
drift pin (route inventory, scope registry, prompts)25 of 34 failed runsone repeat test (maintenance-comms-activities)17 of 34marketing-home tests3 of 34runner never started / lost (vCPU cap, spot)3-5 of 34unread / other3 of 34

34 failed main CI runs, logs read; one run can have several causes.

CauseMechanism that removes it
Drift pin: two green PRs each add a route, the shared inventory breaksMerge queue tests PR + latest main; drift set never skipped
One test failing over and over, no ownerFail-then-pass on same commit → quarantine + owner issue, 7 days to fix or delete
Runner never started (320-vCPU cap, spot)Retry launch failures only, max 2; wider spot pools
Behavior bug CI cannot see (Sep 16 calls outage)10% canary + probe, automatic rollback
4 · Test suite audit (8,236 files, ~110k tests) 🟡 In progress test prune ⬜ Proposed quarantine
keep7,349 filesmerge into table-driven720 filesdelete179 files

Every test file classified (heuristic script plus spot reads); 899 files to merge or delete.

keep, guards a fixed regression300keep, states a rule107merge into one table-driven suite137delete2

The 546 drift tests: most guard a regression that really happened, so they stay.

The rule: a test earns its place if it checks behavior with real inputs, has caught a real defect, or guards a stated rule. Delete tests of run-once scripts, duplicates, and ones that only echo mock calls. The check: each new test carries a mutation control (plant the bug, see it go red), and a nightly report lists tests with zero catches in 90 days, flaky ones, and test-to-code line growth.

5 · Code review: rounds and verdict quality 🟡 In progress verdict watchdog
🟢 clean783 (50%)🟡 suggestions445 (29%) · 78% still got another push🔴 blocking329 (21%)

1,557 bot verdicts across 600 PRs.

real correctness bug49 (43%)test gap28repeat of earlier finding17other9style7gate / rule text4

114 red verdicts in 40 multi-round PRs: fewer than half were bugs.

FindingNumber
PRs with 3+ rounds38% of PRs, 68% of open hours
Rounds after a 🟢310
PRs over 1,000 lines40% of all 🔴
Review runs per day530 (Sep 22) → 1,266 (Sep 29)
Review token costnot logged anywhere
6 · Target pipeline 🟡 In progress merge queue Decided 2026-09-30: merge queue yes (after the backlog is caught up); test prune yes, one PR. Ladder in progress, see 7.
1 · BEFORE PUSH2 · PR LANE (1 workflow)3 · MERGE QUEUE4 · MAIN = DEPLOY5 · PROD CHECKreview:self + testslocal, 2-6 minPR Gate (one check name)lint, types, buildaffected unit testsdrift set ALWAYS runs~8-11 min wall (est.)Review (one workflow)🔴 only for a real defectone verdict per headtoken + $ footerQueue Gate (batch 2-4)PR + latest main, togetherfull suite once, ~11 minred PR is ejected, not maindangerous-diff hold staysLaunch-fail retryvCPU/launch errors onlymax 2, never test failuresDeploy main tipevery lane, no second CIno 30-min windowweb, lambdas, workerNightly full runbackstop for 2 weeks,then evals + Semgrep onlyCanary 10% for 5 minSentry error rate ANDsynthetic probe at Willowsboth must fail to tripAuto-rollback + revert PRall lanes, no human waitFreshness alarm 45 minRuns per push ~3 (CI, review, queue) instead of 22 · Flaky test → quarantine lane + owner issue, fix or delete in 7 days

Winning design: reliability-first (2 of 2 judges), plus grafts from the other two proposals.

Kept: the dangerous-diff hold, the required check names (published on both PR and queue events), the Willows-only probe. Not adopted: a cheaper review model on quality paths, or moving drift tests to nightly.

2026-10-01 20:27 UTC: merge queue turned OFF on Fede's emergency go (depth 71, "blocking all of us"). Before the flip, kick-red-out (#14934) had 27 of 32 batches passing and 0 failing; the 20:03 UTC ruleset drop that landed it had flushed and disarmed every queued PR. PRs now merge one by one with plain auto-merge; main-red collisions are the risk, the deploy hold is the backstop. Turning it back on is Fede's call, after #15019/#15043/#15052 land and the pre-queue check is required.

7 · Migration ladder: 8 small PRs 🟡 In progress one workflow (piece 17)
1Deploy main tip, all lanesfreshness 120→45 mindeletes: pinned-SHA deploys2Retry + one runner spec163 copies → 1deletes: vCPU false reds3Drift set always runs on PRsnever skipped by 'affected'cuts: #1 red-main cause4Flake quarantinefail→pass same SHA = quarantinecuts: repeat-test reds5Merge queue onguard in shadow ~1 weekfolds: auto-merge rules6Delete rerun + windowfold 5 review helpers, 7 no-ops22 → ~3 runs/push7Canary 10% + probeVercel, lambda aliases, ECS breakeradds: prod check8Auto-rollback + revert PRboth signals must faildeletes: manual rollback
  1. in flight: #14289 armed
  2. retry live (watcher + rerun workflow); one runner spec not started (163 copies)
  3. done: #14323 merged 2026-09-30
  4. not started
  5. yes from Fede, after catch-up; not started
  6. in flight: pieces 14 (#14267), 16 (#14354) open; 17 built
  7. not started
  8. not started

Green row: reliability first, each step reversible. Blue row: flow changes, each deletes gates.

8 · Research: what good teams do
PrincipleUs todaySource
Keep main always green; verify before merge, auto-revert after20% of main runs failTrunk-Based Dev
Merge queue tests each PR with latest mainnot usedGitHub docs
Uber's submit queue took mainline from green 52% of the time to always greensame failure classUber, EuroSys 2019
Flake rate above ~1% ruins test value; quarantine, don't blind-retryno quarantineGoogle Testing
Small, fast, one-behavior tests; no brittle over-specificationmock-echo tests existSWE at Google ch.11
Fast checks first; no duplicate coverage across layersfull suite run twiceFowler
Mutation testing on changed lines onlycontrols by handGoogle Research
Elite: under 1 day lead time, ~5% change failure, recovery under 1 hlead time elite; failure and recovery unmeasuredDORA 2024 summary
AI adoption tracks lower delivery stability (−7.2%)agent-written code at scaleDORA 2024
AI code raises duplication, cuts refactoring (vendor study)899 prunable test filesGitClear 2025
9 · Detail files

before-after-runs-per-push-2026-09-30.md · ci-gates-inventory.md · ci-waste-audit-30d.md · deploy-post-merge.md · judge-0.md · judge-1.md · pr-lifecycle.md · proposal-0.md · proposal-1.md · proposal-2.md · research-best-practices.md · review-process.md

Disputed numbers: the $6.6k/month runner figure is one expensive day (Sep 29) times 30; capacity and red-main targets are queue math, not measurements. Not measured yet: change failure rate, recovery time, review token spend.

10 · Runner vendors vs our own fleet (research, 2026-09-30)

Fede asked, 2026-09-30: Gera posted a Buildkite link in the meeting chat ("our high volume has broken the build machines" / "and our wallet" / buildkite.com). Would a vendor be cheaper than maintaining our own fleet? Base: ~1.7M machine-minutes in September (spend page).

OptionCost today / monthAfter cleanup halves minutes (estimate)Notes
Our fleet on AWS + RunsOn~$7.3k (Sep 29 pace $243/day, AWS cost RCA page)$1.5–2.5k + ~$300 licensecheapest at our size; cost is ops time (capacity stalls, spot reclaims, quotas)
GitHub-hosted~$10.3k (Sep measured $10,549, spend page)~$5kzero ops; ruled out by Fede 2026-09-29
Buildkite (hosted)~$13.6k + $900 seat minimum~$6.8k + $900different CI system: ~37 workflows to convert; arm64 needs enterprise plan; GitHub Actions compatibility is a preview
Blacksmith~$6.8k list~$3.4kSep 9 page found same $/min as GitHub for equal machines; tried and removed 2026-09-30
Depot~$10.2k~$5.1knot evaluated beyond pricing

Recommendation: keep the fleet, finish the CI cleanup (the savings are in minutes, not the vendor), revisit only if stalls keep costing hours. Every vendor cell is list price × our minutes (assumes 1 machine-minute = 1 billed 2-vCPU minute); "after cleanup" assumes the team's own 50% target, not a measurement.

Sources: buildkite.com/pricing, buildkite.com/docs/agent/buildkite-hosted/linux, buildkite.com/docs/pipelines/migration/run-github-actions-workflows, blacksmith.sh/pricing, depot.dev/pricing, runs-on.com/pricing, docs.github.com/en/billing/reference/actions-runner-pricing, github.blog changelog 2025-12-16 (simpler Actions pricing). Transcript: #transcripts 2026-09-30 chat log and call.

One machine per PR push (Proposed — pending review, Oct 1, 9:40 AM MT)

Answer. Run everything a PR push needs on one 8-core arm spot machine as steps of one job, and have that job post the required check names itself. Today a push starts about 25 short glue jobs on the always-on pool plus 3–4 heavy machines; the glue jobs and the machine boots cost more than the work. Cost model (Opus, Oct 1): CI runs about $335/day against Fede's target of under $65; this piece is about −$68/day and takes push-to-green from 9.8 to about 8 minutes.

Today, per PR pushProposed
Plan, manifest, 2 verdict jobs, result aggregator, gates, repin, docs gate: ~25 jobs on the 2-core pool, each billed 75 s warm + bootSteps of one job; no boot, no pool
"Unit Tests (decision)" idles ~7 min polling the API ($13.57/day)Gone: the verdict is written at the end of the same job
CI Checks (lint, types, build) and affected tests on two 8-core machines; guard pins on 4-core shardsSame work, parallel steps on one 8-core arm spot box
Required names (Build, Type Check, Lint, Unit Tests, Regression Gate, Nested Gate) come from 6 separate jobsOne chokepoint step posts all six as check-runs from one verdict file

What breaks, and the answer.

  1. Required check names. Branch protection accepts a check-run of that name from the Actions app; the job needs checks: write. The rule "execution jobs post nothing" (one chokepoint) is kept: a single final step posts, and the existing chokepoint mutation controls stay green.
  2. Re-running one check. One job means one rerun (~8 min) instead of one shard. Accepted: reruns are for machine kills, and the spot-kill rerun already restarts the whole run.
  3. Follow-ons keyed on job names. The review watchdog and the merge guard read the job "Unit Tests" and its "CI verdict:" step; the guard is deleted after #14830, the watchdog moves to reading the check-run. Phase 2, before any required name moves.
  4. 145 tests reference ci.yml; 15 pin names and the job graph. The shadow adds a new job under new names and touches none of them; the cutover PR updates them with the controls.

Plan. (1) Shadow, dark: job "One machine (shadow)" runs on 1 in 4 pushes (head sha starts with 0–3), posts its results as shadow: check-runs nobody requires, records step timings; a dated removal PR 48 h after merge. (2) Compare on the sampled pushes: shadow wall time vs the real lane's push-to-green, red/green agreement per name. (3) Cutover: the shadow posts the required names, the old jobs go, tests and controls updated, follow-ons re-keyed. (4) Glue that stays outside the push (reviews, merge-queue verdicts) moves to two always-on 2-core arm machines (~−$18/day, removes the warm pool).

Cost arithmetic (Opus, model3.py → one-machine.md, measured 12:00–14:45Z Oct 1 after the queue went on, scaled to a day, ±25%).

TodayOne machine
PR-event CI per day$93 ($71 pushes + $22 reviews); $193 over the full 24 h$26 ($23 counting cancelled pushes as half)
Per push1,413 pushes/day, 379 cancelled by a newer push; ~1,923 CPU-seconds of work each$0.0186: one m8g.2xlarge spot, 343 s compute at an assumed 70% efficiency + 90 s setup + 30 s boot ≈ 7.7 min
Saving$67/day on machines, $76 with IP and disk. x86 8-core would be $31–32/day; 4-core x86 $28/day but ~13.4 min push-to-green vs 7.7 on arm and 9.8 today

Unmeasured, and what the shadow measures: the 70% CPU efficiency and the 90 s setup. Open: GitHub expressions have no arithmetic, so sampling uses the head sha's first character. Status rows below as PRs land.

StepPR
Shadow job, dark, 1-in-4 sample, measurement only#14880 redesigned as its own workflow after review, merged
Glue pool on arm: pool config (org RunsOn config).github-private #2 merged Oct 1, 9:55 AM MT
29 glue jobs move to the arm pool; 3 stay on x86 (cache key, Lambda bundle)#14888 merged; its own CI run is the arm proof

Where the minutes go (measured 2026-10-01)

Gera's message on 10-01: CI still feels slow. We measured where a PR's minutes actually go, on the PR lane and in the merge queue, before choosing the next lever.

Measured 2026-10-01 10:20–11:40 MT by the CI/CD Fable session; samples: 40 PR runs, 286 batch runs, 12 failure logs, 8 batch critical paths, 39 PRs.

PR lane (last 3 h, 40 runs, 14:20–17:20Z)

Push → all checks done11.1 min median, 17.7 p90
Waiting for a machine on the critical path~30 s on spot labels, ~0 s on the flex pool (5% of the wall)
Review botdone 6.3 min before CI on 27 of 28 pushes (not on the critical path)
Longest jobs (median / p90)Semgrep 3.6 / 9.0 min (advisory); CI Checks 3.4 / 6.8; Unit Tests (affected) 3.0 / 6.1
Reruns14% of runs needed a second attempt; each adds a full ~5 min cycle

Merge queue (since it turned on 10:49Z; 286 batch runs, 39 merged PRs sampled)

Time in queue per PR28.7 min median, 51.9 p90
One batch run12.4 min (success), 12.6 (failure; no early exit)
Slot wait (queue time minus one batch, never-kicked PRs)~13 min median
Batches that failed38% overall, 49% in the 16:00Z hour
PRs kicked out of the queue at least once62% (kicked PRs +5.7 min median)
Why batches failed (12 sampled)8 guard-pin drift/ratchet tests that pass on the PR but fail after main's guard files moved; 4 the DDB Escape Gate on one test file
Critical path inside a batchCI Checks 9.0 min (vs 3.4 on PR runs), then Guard pins shards ~4 min each

What this says.

One run per PR push: baseline and plan (Oct 1, 5:30 AM MT)

Measured on real PRs, last 3 hours (98 pushes). Each PR push fires 3 workflows directly and about 5 more when CI finishes, roughly 9 runs per push.

WorkflowRuns / 3 hTriggerPlan
CI124pull_requestStays. This is the one run.
Auto-merge on green421 (124 pull_request, 136 workflow_run, 125 repository_dispatch, 36 review)four triggersOne trigger path: #14788 merged 6:5x AM MT (dropped pull_request + repository_dispatch); #14821 merged 8:1x AM MT (CI completions only, human reviews only). Last blocker to deleting the helper: moving the dangerous-diff label into a small job after CI's plan step, scoped in #14821's body.
Claude Code Review101pull_requestBecomes a job inside CI. Needs design: the review secrets and the verdict encoder move with it. Proposed, pending review.
Rerun on runner shutdown + Adopt unit tests131 + 108workflow_run (CI)Folded into one ci-followups.yml: #14785 merged 6:15 AM MT.
Review re-fire + Rerun review on infra failure + Main-lane red alert108 + 28 + 43workflow_runSame file: #14799 merged 6:47 AM MT. Five helper workflows are now one ci-followups.yml.

Target: 1 direct run per push and 1 follow-up run per CI completion (from ~9 to 2), measured the same way after each step. Machine-minutes change little in parts 1 and 2 (same jobs, fewer runs); the review-as-job step is where the pickup waits disappear.

Before → after, measured the same way (2026-10-01)

Method: every workflow run in the window (events pull_request, workflow_run, repository_dispatch, pull_request_review, pull_request_target) divided by distinct PR pushes. Before = 02:00–05:00Z (merge queue OFF). After = 12:50–14:50Z (merge queue ON).

BeforeAfter
PR pushes18332
Workflow runs2,433587
Runs per push13.318.3 → 16 at 10:00 AM

Not a win yet, and why. The five CI-completion helpers are gone (3.7 runs/push → 0) and replaced by one file (4.8/push). The direct runs fell 3.6 → 2.8. But two things grew: the auto-merge guard now fires on every CI completion (5.9/push, was 4.2) and the follow-ups file fires on every CI completion too. The queue was off in the before window; with it on, every merge-queue batch's CI completion fires both workflows with every job skipped, and the after window had fewer pushes per queue batch, so the ratio rose. The leg ci.yml runs on every review (approval-time full suite, 1.5/push, 46 runs) and the merge-hold review listener (1.5/push, job skipped on bot reviews) are new in the count because the before tally missed them.

PathBefore/pushAfter/pushNext
auto-merge-all (guard)4.25.9#14821 drops review triggers; deleted outright after #14830 lands
ci.yml2.53.3includes 14 merge_group runs (queue on); proposal to Opus: drop the review-event full suite, the queue already runs it
ci-followups (one file)04.8#14835 merged 9:10 AM MT: ignores merge-queue branch completions; expect ~1 per PR CI completion (re-measure ~10 AM)
five deleted helpers3.70done
merge-hold review listener01.5fires on bot reviews with the job skipped; accepted for now
ci-dispatch-guard, review, deploys2.42.4unchanged

Re-measured 10:00 AM MT (45 pushes since the queue-branch filter merged at 9:10): 16 runs per push, down from 18.3. Where they come from now, per push: follow-ups file 4.3 (it wakes on every completion of CI, the review, harnesses and advisory tests, almost always with every job skipped); the auto-merge guard 3.2 on completions plus 1.4 on reviews; merge-hold listener 1.4; ci.yml 1.3 on reviews (the full suite at approval) and 1.3 on pushes; the review itself 1.0; dispatch guard 1.1; deploys after merges 0.3. The queue-branch filter worked: zero follow-ups now fire on merge-queue batches.

What this says. Every listener that wakes on a completion is one run per completion, no matter how little it does; filters only trim which completions. Deleting the guard (after the fence PR) takes ~4.6 off and the review-event suite cut takes ~1.3, landing near 10. Getting to 2 needs the producer to do its own follow-up work: the review as a job inside the push's run and the follow-ups as its final steps, which is what the one-machine-per-push design above delivers. Target stays 2; re-measured the same way after each step.

PropFlow Docs