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
| Measure | Yesterday | Today | Goal |
|---|---|---|---|
| Time to merge (PR opened → merged, median) | 2.1 h | ~50 min | < 30 min Oct 2 afternoon, < 15 min this week |
| Main red | Overnight, 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 cancelled | Rare |
| Merge-queue runs since 4:49 AM MT | n/a | 60 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 cost | Sep 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 push | 13.3 | 18.3 (worse; see One machine per push · One run per PR push below) | 2 |
By theme
| Theme | Status | Done | In progress | Not 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 fixes | Moving the two queue-only checks onto every PR; stopping the guard-pin kinds file going stale when two PRs are batched | Open 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 completions | Dangerous-diff hold enforced by a required status (then the old helper is deleted); queue-branch filter for follow-on workflows | One runner spec (163 copies down to 1) |
| Cost | 🔄 | Spot quota raised 512 to 1,024 vCPU | Reviews no longer re-run unit tests unless owed (~400 fewer 8-core runs a day); a costed plan to reach < $65/day | Single-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 machines | Queue-only checks moved onto PRs (Fable, PR pending) | Flaky-test quarantine |
| Deploys | 🔄 | Worker and Lambda deploys target the main tip | Renewal 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 needed | Delete the old auto-merge helper once the required status is live | Rest 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
| What | PR |
|---|---|
| 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
| What | PR |
|---|---|
| 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
| Idea | Status | Proof | Effect |
|---|---|---|---|
| Done (77) — newest first | |||
| Last x64 jobs moved to the arm pool | ✅ Done merged Oct 1 | #15001 | x64 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 | #15018 | Pre-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 | #14902 | Deploy 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 | #14888 | 29 of 33 pool labels now arm (commit 5d4449ab7) |
| One-machine shadow run (measurement only, advisory) live | ✅ Done merged Oct 1 | #14880 | Removal 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 | #14774 | Handles already-running review runs without failing |
| Auto-merge guard fires once per CI completion | ✅ Done merged Oct 1, 7:58 AM MT | #14821 | Claude 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 open | Fewer low-value tests to run. |
| Test prune | ✅ Done merged Oct 1, 7:46 AM MT | #14420 | Test prune: dead and duplicate test files removed |
| CI followups: review re-fire | ✅ Done merged Oct 1, 6:46 AM MT | #14799 | review 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 | #14785 | rerun-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 | #14784 | Every 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 | #14781 | Test 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 | #14779 | The 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 | #14735 | One 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 | #14724 | RunsOn 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 | #14718 | Four 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 | #14710 | The 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 | #14712 | Removed 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 | #14696 | PR 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 | #14536 | Wider 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 | #14536 | 166 label sites |
| Spot placement | ✅ Done merged Oct 1, 12:10 AM MT | #14570 | Done (merged a93b837c8): every runner label now uses capacity-optimized spot, no lowest-price anywhere. |
| Spot strategy | ✅ Done merged Oct 1, 12:10 AM MT | new | Last 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 | #14535 | PRs 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 | #14535 | A 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 | #14649 | Guard-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 | #14583 | Renewal 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 | #14625 | All 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 | #14582 | Guard pins classified fence vs bookkeeping (metadata only). Done. |
| Guard-pin classification | ✅ Done merged Sep 30, 10:38 PM MT | new | Every pinned guard test tagged fence vs bookkeeping (metadata only) for the check diet |
| New runner fleet | ✅ Done merged Sep 30, 10:23 PM MT | #14612 | Canary 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 | #14538 | Terraform 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 | #14377 | Catches reviews that never post a verdict. |
| Guard-pin shards on bigger machines | ✅ Done merged Sep 30, 9:42 PM MT | #14540 | Guard-pin test shards move to 4-core machines (≈30–40% shorter) |
| us-west-2 runner fleet | ✅ Done merged Sep 30, 9:20 PM MT | #14563 | Done. |
| US West canary | ✅ Done merged Sep 30, 9:20 PM MT | new | Dispatch-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 | new | Stack 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 | #14586 | Heavy 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 | #14534 | Alarm note in the fleet automation config |
| Red main fix | ✅ Done merged Sep 30, 8:45 PM MT | #14569 | Red 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 | #14558 | Three 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 | #14552 | Two 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 | #14548 | Two 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 | new | Cancelled shards stop showing as failures. |
| Cancelled test shards on a PR | ✅ Done merged Sep 30, 7:35 PM MT | #14530 | Cancelled 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 | #14487 | A dismissed review gets a fresh one. |
| Merge-queue batch checks run on non-spot machines | ✅ Done merged Sep 30, 7:27 PM MT | #14477 merged | The 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 | #14477 | One machine loss no longer rebuilds a batch. |
| One-workflow piece 17 (override/repin fold) | ✅ Done merged Sep 30, 7:21 PM MT | #14436 | Merged |
| Review-check override folded into CI | ✅ Done merged Sep 30, 7:21 PM MT | #14436 | One 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 | new | Fix goes forward; nothing is reverted. |
| Red main fix-forward | ✅ Done merged Sep 30, 6:59 PM MT | #14527 | Red 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 | #14455 | Saves machine time on every merge. |
| PRs never wait on the heavy test lane after approval | ✅ Done merged Sep 30, 6:32 PM MT | #14491 | Approved 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 | #14282 | Fewer CI runs per PR. |
| Main deploys were starving | ✅ Done merged Sep 30, 6:21 PM MT | #14505 | A 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, armed | Frees 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, #14446 | main 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 | #14440 | Merge 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, #14423 | A 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 | #14422 | The harnesses image builds again. |
| Heavy guard pins run only after approval / on main | ✅ Done merged Sep 30, 3:03 PM MT | #14409 | The 30-minute guard lane no longer runs on every push. |
| Advisory checks don't block merges | ✅ Done merged Sep 30, 3:01 PM MT | #14414 | A 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, #14354 | Every job uses one of two machine sizes. |
| Deploy main tip for lambdas/worker | ✅ Done merged Sep 30, 2:53 PM MT | #14289 | Deploys no longer pin to the triggering commit. |
| Drift tests on every PR | ✅ Done merged Sep 30, 12:18 PM MT | #14323 | Drift breaks are caught before merge. |
| x86 runner pools | ✅ Done merged Sep 30, 11:55 AM MT | #14317 | Avoids the Graviton capacity stalls. |
| Auto-merge guard workflow: paused today for catch-up | ✅ Done | workflow active again (checked Oct 1, ~9 AM MT) | Temporary, to drain the backlog. |
| Repo-admin bypass on the "Require CI" ruleset | ✅ Done | bypass list empty (checked Oct 1, ~9 AM MT) | Temporary, to drain the backlog. |
| Spot vCPU quota 512 → 1,024 | ✅ Done | AWS approved | ✅ 1,024 live at 4:10 PM; EC2 still enforcing the old count for ~30 min after |
| Third runner fleet | ✅ Done | runs-on-flex | New 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 | ✅ Done | new | Spot vCPU alarm threshold raised 410 → 820 (80% of the 1,024 quota) |
| Push throttle | ✅ Done | new | Builders skip the push pacer tonight (documented skip, Fede wants speed) |
| Ownership split | ✅ Done | new | CI/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 started | issue #14474 | Auto-merge resumes by itself after a freeze. |
| Vercel preview / UI verify only after review verdict | ⬜ Not started | proposed | Skips preview builds on PRs that get revised. |
| Flaky-test quarantine | ⬜ Not started | ranking done, list in session | Flaky tests stop blocking unrelated PRs. |
| Follow-ups from today's review rounds (12 small CI issues) | ⬜ Not started | issues #14463–#14479 | Rerun 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 | #14520 | Main runs stop dying to spot loss. Dropped: reverted by #14777 (merged without Fede's cost yes) |
| Short 2-vCPU jobs on guaranteed machines | ✖ Dropped | #14543 | On cost hold (≈ +$1,800/mo estimate), yes/hold with Fede Dropped: closed as not needed |
| Main-branch heavy test shards on guaranteed machines | ✖ Dropped | #14520 | On 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
- AWS gross CI spend was ~$347 on 09-29 and ~$223 on 09-30 (partial day), vs a ~$55/day baseline (Cost Explorer, before credits, measured by CI/CD Opus).
- The driver is guaranteed (on-demand) machines at ~2.7x spot price, reached automatically when spot capacity fails (117 failures in the last hour) or the spot circuit breaker trips (~1,100 interruption log lines/hour).
- Fix path: wider machine families, West region, least-interrupted pools, fewer cancelled jobs.
- The two PRs adding more guaranteed machines are on hold.
- 9:00 PM: #14543 and #14520 stay unarmed pending Fede's yes/hold; the spot-placement change (#14570) attacks the on-demand fallback at its cause.
Baseline before the one-run flip (measured 9:45 PM MT, 447 PRs merged in the last 24 h)
| Measure | Value |
|---|---|
| Open to merge | median 124 min, p90 729 min; 67% of it before the final push |
| Single-push PRs | merge in a median 27 min |
| Final push to all six required checks green | median 14.3 min, p90 52 min |
| Check that finishes last | Unit Tests, in 353 of 420 PRs |
| Runner queue wait per job | median 0.6 min (not the bottleneck) |
| Unit Tests decision waits on its chain | median 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 approval | median 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
| Measure | Today | Target (est.) |
|---|---|---|
| Merge → prod | web median 18 min, p90 49; worker/lambdas hours on bursts | ~20 min, every lane |
| Workflow runs per push | 32 → 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 failing | 20% (202 of 1,000) | under 3% |
| Merges/day | 181 main commits/day; 341 PRs Sep 29 | ~800/day capacity |
| Test files | 8,236 | ~7,500 |
Top 5 mistakes
- No PR is tested against latest main before merging.
- Review 🔴 blocks on non-bugs (57%); pushes continue after 🟡.
- 22 runs per push; about 12 do nothing.
- Deploys pin old commits; nothing checks prod behavior.
- 59% of runner spend is on-demand fallback.
Top 5 opportunities
- Merge queue; delete the rerun on main.
- 🔴 only for real defects; one verdict per commit.
- One CI workflow, one review workflow, one runner spec.
- Deploy main tip, canary, auto-rollback.
- Quarantine flaky tests; prune 899 low-value test files.
Next 3 actions
- Deploy the main tip for lambdas and worker; alarm at 45 min.
- Always run the drift tests on every PR.
- Flaky-test quarantine with a 7-day fix-or-delete.
1 · The pipeline today, gate by gate
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
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
34 failed main CI runs, logs read; one run can have several causes.
| Cause | Mechanism that removes it |
|---|---|
| Drift pin: two green PRs each add a route, the shared inventory breaks | Merge queue tests PR + latest main; drift set never skipped |
| One test failing over and over, no owner | Fail-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
Every test file classified (heuristic script plus spot reads); 899 files to merge or delete.
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
1,557 bot verdicts across 600 PRs.
114 red verdicts in 40 multi-round PRs: fewer than half were bugs.
| Finding | Number |
|---|---|
| PRs with 3+ rounds | 38% of PRs, 68% of open hours |
| Rounds after a 🟢 | 310 |
| PRs over 1,000 lines | 40% of all 🔴 |
| Review runs per day | 530 (Sep 22) → 1,266 (Sep 29) |
| Review token cost | not 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.
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)
- in flight: #14289 armed
- retry live (watcher + rerun workflow); one runner spec not started (163 copies)
- done: #14323 merged 2026-09-30
- not started
- yes from Fede, after catch-up; not started
- in flight: pieces 14 (#14267), 16 (#14354) open; 17 built
- not started
- not started
Green row: reliability first, each step reversible. Blue row: flow changes, each deletes gates.
8 · Research: what good teams do
| Principle | Us today | Source |
|---|---|---|
| Keep main always green; verify before merge, auto-revert after | 20% of main runs fail | Trunk-Based Dev |
| Merge queue tests each PR with latest main | not used | GitHub docs |
| Uber's submit queue took mainline from green 52% of the time to always green | same failure class | Uber, EuroSys 2019 |
| Flake rate above ~1% ruins test value; quarantine, don't blind-retry | no quarantine | Google Testing |
| Small, fast, one-behavior tests; no brittle over-specification | mock-echo tests exist | SWE at Google ch.11 |
| Fast checks first; no duplicate coverage across layers | full suite run twice | Fowler |
| Mutation testing on changed lines only | controls by hand | Google Research |
| Elite: under 1 day lead time, ~5% change failure, recovery under 1 h | lead time elite; failure and recovery unmeasured | DORA 2024 summary |
| AI adoption tracks lower delivery stability (−7.2%) | agent-written code at scale | DORA 2024 |
| AI code raises duplication, cuts refactoring (vendor study) | 899 prunable test files | GitClear 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).
| Option | Cost today / month | After 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 license | cheapest at our size; cost is ops time (capacity stalls, spot reclaims, quotas) |
| GitHub-hosted | ~$10.3k (Sep measured $10,549, spend page) | ~$5k | zero ops; ruled out by Fede 2026-09-29 |
| Buildkite (hosted) | ~$13.6k + $900 seat minimum | ~$6.8k + $900 | different CI system: ~37 workflows to convert; arm64 needs enterprise plan; GitHub Actions compatibility is a preview |
| Blacksmith | ~$6.8k list | ~$3.4k | Sep 9 page found same $/min as GitHub for equal machines; tried and removed 2026-09-30 |
| Depot | ~$10.2k | ~$5.1k | not 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 push | Proposed |
|---|---|
| Plan, manifest, 2 verdict jobs, result aggregator, gates, repin, docs gate: ~25 jobs on the 2-core pool, each billed 75 s warm + boot | Steps 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 shards | Same 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 jobs | One chokepoint step posts all six as check-runs from one verdict file |
What breaks, and the answer.
- 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. - 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.
- 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.
- 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%).
| Today | One 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 push | 1,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.
| Step | PR |
|---|---|
| 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 done | 11.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 bot | done 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 |
| Reruns | 14% 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 PR | 28.7 min median, 51.9 p90 |
| One batch run | 12.4 min (success), 12.6 (failure; no early exit) |
| Slot wait (queue time minus one batch, never-kicked PRs) | ~13 min median |
| Batches that failed | 38% overall, 49% in the 16:00Z hour |
| PRs kicked out of the queue at least once | 62% (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 batch | CI Checks 9.0 min (vs 3.4 on PR runs), then Guard pins shards ~4 min each |
What this says.
- The fleet is not the bottleneck: machine pickup is seconds, and the review bot finishes first.
- The felt slowness is the merge queue: a PR waits about half an hour, half of it just waiting for a batch slot.
- Three levers, by owner: queue settings (more batches built in parallel) — CI/CD Opus; CI Checks taking 9 min inside a batch when it takes 3.4 on a PR — CI/CD Fable, cause being traced now; drift pins that only run in the queue — a re-check against fresh main right before enqueue, so a stale-green PR never enters the queue (proposed to Opus).
- Not in scope here: the slowest-tests audit, which Gera is doing himself.
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.
| Workflow | Runs / 3 h | Trigger | Plan |
|---|---|---|---|
| CI | 124 | pull_request | Stays. This is the one run. |
| Auto-merge on green | 421 (124 pull_request, 136 workflow_run, 125 repository_dispatch, 36 review) | four triggers | One 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 Review | 101 | pull_request | Becomes 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 tests | 131 + 108 | workflow_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 alert | 108 + 28 + 43 | workflow_run | Same 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).
| Before | After | |
|---|---|---|
| PR pushes | 183 | 32 |
| Workflow runs | 2,433 | 587 |
| Runs per push | 13.3 | 18.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.
| Path | Before/push | After/push | Next |
|---|---|---|---|
| auto-merge-all (guard) | 4.2 | 5.9 | #14821 drops review triggers; deleted outright after #14830 lands |
| ci.yml | 2.5 | 3.3 | includes 14 merge_group runs (queue on); proposal to Opus: drop the review-event full suite, the queue already runs it |
| ci-followups (one file) | 0 | 4.8 | #14835 merged 9:10 AM MT: ignores merge-queue branch completions; expect ~1 per PR CI completion (re-measure ~10 AM) |
| five deleted helpers | 3.7 | 0 | done |
| merge-hold review listener | 0 | 1.5 | fires on bot reviews with the job skipped; accepted for now |
| ci-dispatch-guard, review, deploys | 2.4 | 2.4 | unchanged |
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.