GitHub Actions spend — Blacksmith trial and the credit runway

Where the CI bill goes, what Blacksmith measured on a real run, and three options for making the $9k GitHub credit last longer. For Fede and Gera.

2026-09-09 · Decided: Option A (Fede, 2026-09-09), PR #7437 closed; cuts pending approval · Trello qw8iq00P · PR #7437 held, not merged

Blacksmith does not lower the bill, it moves it from credit to cash. Its 4-vCPU machines cost the same per minute as GitHub's standard runners, both round every job up to a whole minute, and about 4,400 billed jobs a day are the problem, not the hardware. Recommendation: stay on the credit, close the PR, and cut job count.

Newest: Did small PRs cause the CI jam? · 30-day CI waste audit · CI fleet inspection: what is left to save · One-workflow fold: runs per PR push · CI capacity: AWS practices and how AI-scale teams cope · What others do about CI for fleets of coding agents · Job-by-job audit, September 29 · Run inspection, September 29: are we running too many checks? · Root cause, September 28

CI capacity: AWS practices and how AI-scale teams cope (2026-09-29)

On 2026-09-29 the CI fleet hit the 320 on-demand vCPU cap. The cause: the spot setting "lowest-price" found no ARM capacity, so every job fell back to on-demand. Fix #13974 merged at 19:23 MT and moved every runner back to spot within a minute. Two research passes follow: AWS and RunsOn practices, and how teams with AI-scale CI volume cope.

Night of 2026-09-29: what shipped and the proof

The fleet was capped at 320 on-demand vCPU and 512 spot vCPU. Root cause of the 3-hour stall: after a spot-quota burst error, RunsOn forces on-demand for 5 minutes, that bucket fills, and launches fail (force_on_demand_reason = spot_quota_snoozed in the worker logs). Quotas are not raised; the cure is lower peak demand and faster machines.

Before → after (RunsOn job logs, 30-min buckets, MT)

Measure18:00–19:0019:30–20:20
Launches failing on a capacity cap, per 30 min~1,400240–320
Machines on spot15%42–55%
Wait for a job to start, typical (p50)61–75 s39 s
Wait, slow case (p90)11–14 min1.5–1.8 min
Machine-minutes per 30 min10,6872,716
Production lag3+ h behind mainpromoted 19:55 (cc20dd843)

Merged live tonight

PRWhatExpected effect
#13861/#13869/#13870/#13872/#13881/#13902/#13970The 8 job cuts (guard pins only what a push affects, self-review cut, etc.)Fewer jobs per push
#13974Spot allocation price-capacity-optimized + t4g familySpot 0% → 42–55%
#13978Wider cpu/ram/family ranges on runner labelsMore spot pools
#140088cpu label needs 32 GB RAM floorFixes exit-137 (OOM) unit shards caused by #13978
#14000Auto-merge guard skips wakes from empty review runs~800 fewer machine jobs/day
#13984One-workflow piece 1: Plan + PR Verdict shadow jobs (dark)Measurement scaffold for the fold
RunsOn stackv3.3.1 → v3.3.2, spot circuit breaker 2/15/30 → 20/15/10, AppSize small → mediumFewer forced on-demand periods
CloudWatchAlarms ci-ondemand-vcpu-80pct / ci-spot-vcpu-80pct, posted to #alerts by a 2-min pollerEarly warning

Where the machines go (2 h sample, 19:00–21:00)

BucketShare of machine-minutes
CI63% (full suite 45% of CI, guard pins 31%, affected 13%, checks 8%)
Auto-merge guard21%
12 other workflows16%

About 58,000 jobs/day.

In flight (open PRs / builds)

ItemWhat
#14014Review bot blocks only on correctness defects, 2-round cap, 🟡 says "do not push". From the 30-day inspection: median PR merges in 40 min; slowest 10% hold 77% of hours; 82% of hours are review loops; 43% of 🔴 were real bugs; 78% of 🟡 were followed by a push.
#14024One-workflow piece 2 (5 drift checks folded)
Graviton4-only poolsGuard shard 615 s on t4g → 326 s on m8g; −26% CI machine-minutes
Delta test laneAccepts any green ancestor (−12–15%)

22:30 proof (CloudWatch, one query for all windows; Denver time)

WindowJobsSlow-case wait (p90)Spot shareOn-demand cap hitsMachine-minutes
Incident 18:30–19:304,805231 s7%6,3195,565
After first fixes 19:30–20:205,54461 s23%1,9365,154
Last hour 21:30–22:309,025116 s16%6,9646,891
Last 30 min 22:00–22:304,52598 s16%3,1843,612

Waits met the target (slow case under 5 minutes). Cap hits did not fall because job demand nearly doubled versus the incident hour; every cut so far was absorbed by more pushes. The remaining lever is fewer jobs per push, which is what the one-workflow fold delivers.

Merged 22:00–23:00

Open, armed

AWS and RunsOn practices

Items marked (inferred) or [INF] are reasoning, not stated by a source.

PracticeSourcePropFlow todayGapFix + effort
Use price-capacity-optimized; AWS calls lowest-price "not recommended... highest risk of interruption"https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-allocation-strategy.html2cpu label spot=lowest-pricePicks cheapest pool even when nearly emptyspot=pco / price-capacity-optimized (in the open PR). RunsOn's own default is already PCO (https://runs-on.com/configuration/spot-instances/). 15 min
Be flexible across ≥10 instance types and all AZshttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-best-practices.html7 families, all Graviton, one size (2 vCPU/8 GB)Few pools; all compete for the same Graviton spot supplyAdd c6g/c7g/c8g/m6gd..., t4g, and allow a range cpu=2+4 / ram=4+16 (https://runs-on.com/configuration/job-labels/). 1 h
Attribute-based selection (vCPU/RAM ranges, not named types)https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-attribute-based-instance-type-selection.htmlFixed ram=8Excludes 4/16 GB sizesRunsOn ranges cpu=2+4, ram=4+16; best-practices page: "use ranges" (https://runs-on.com/guides/best-practices/). 30 min
Don't treat on-demand as the spot failover: "failing over to On-Demand... can inadvertently drive interruptions" and OD in that pool may also be scarcespot-best-practices (above)RunsOn always falls back spot->OD (https://runs-on.com/configuration/spot-instances/)Fallback funnelled the whole fleet into the 320 OD bucketWiden spot pools so fallback rarely fires; cap OD use (see §2).
Watch vCPU quotas with CloudWatch alarmshttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-on-demand-instances.htmlNo alarm known [INF]2 h stall found by humansService Quotas usage alarm at 80% of 320 OD and 512 spot. 1 h, AWS-side (read/alarm only, no request)
Quota buckets: Standard = A,C,D,H,I,M,R,T,Z, counted in vCPUs regardless of type; separate OD and Spot bucketsec2-on-demand-instances (above); https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-limits.htmlGraviton + t4g all StandardAdding x86 (m7i, c7a) or t4g does not add quota; it only adds spot *pools*Use x86 to widen spot supply, not quota.
Spot placement score before choosing region/AZ/type mix (needs ≥3 types)https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-placement-score.htmlNot used [INF]Mix chosen blindOne-off aws ec2 get-spot-placement-scores for the proposed arm64 and x64 sets, per AZ. 2 h, free
Handle interruptions (rebalance / 2-min notice, retry)spot-best-practices; RunsOn v3.3.0/3.3.2 auto-retry of interrupted jobs (https://runs-on.com/changelog/v3.3.0/)retry= unknown; stack version unknownPossible manual rerunsUpgrade stack ≥ v3.3.2; retry=when-interrupted. 2 h
Circuit breaker trips to OD after N interruptionsSpotCircuitBreaker default 2/15/30 (https://runs-on.com/docs/maintenance/configuration/flex/cloudformation/)Default [INF]At 36k runs/day, 2 interruptions in 15 min trips the whole stack to OD for 30 min, a quota-exhaustion accelerator [INF]Raise to e.g. 20/15/10. 15 min
Surface launch errors instead of silent queuesv3.3.0: "Repeated EC2 capacity failures could leave a Flex job queued for hours... Flex now reserves its final launch attempt for the fallback error runner"Matches incident symptomIf stack < 3.3.0, jobs hang instead of failing fastUpgrade (same as above)
Spread OD across AZsv3.3.0: OD runners "could all land in one AZ... now vary subnet preference, including on-demand fallback"UnknownOD fallback may have hit one AZ's ICEUpgrade
Control-plane sizing for launch rateAppSize default small (CFN page above)Unknown36k runs/day likely exceeds "small" launch-rate assumptions [INF]Set AppSize medium/large. 15 min
Warm poolshttps://runs-on.com/runners/pools/None [INF]Pools are on-demand only and count against the 320 OD quotaOnly a small pool for latency-critical jobs; not a capacity fix
Quota auto-increaseEC2 "automatically increases your On-Demand Instance quotas based on your usage"; Service Quotas: "Smaller increases are usually automatically approved" (https://docs.aws.amazon.com/servicequotas/latest/userguide/request-quota-increase.html)Rule: no requestsFact onlynone

Top 5 actions

  1. Ship PCO + wide Graviton family + t4g + cpu/ram ranges on 2cpu and 8cpu labels. 1–2 h. Repo only.
  2. Upgrade RunsOn stack to ≥ v3.3.2 (fail-fast on capacity errors, OD spread across AZs, interrupted-job retry). 2–4 h. Vendor release, our CFN update.
  3. Retune SpotCircuitBreaker (default 2/15/30 is too twitchy at this volume) and check AppSize. 30 min. CFN parameter.
  4. Add an x64 spot twin label for arch-neutral jobs to widen spot pools. 1 day incl. migrating workflows. Repo only.
  5. Quota + spot-health alarms and a one-off spot placement score check for both mixes, per AZ. 2–3 h. AWS reads/alarms only, no quota request.

How teams with AI-scale CI volume cope

No CI-cost or capacity posts were found from Cursor, Cognition/Devin, Factory, Replit, Vercel, Datadog, Canva or Airbnb. Shopify, Google, Meta, Uber and Chromium sources predate agents but are the primary literature. Items marked (inferred) are reasoning, not sourced.

PracticeWho (source)Published effectPropFlow?
Test impact analysis (listener records results, selector picks tests per PR)Anthropic (claude.com)CI jobs up 25x in 6 months, tests 10x; the *selector service* fell over first (3 patches lasted 70d, 29d, <1d); redesigned stateless in 3 weeksPartly: have affected-test selection; lesson = selection infra must scale 10-20x
ML predictive test selectionMeta (engineering.fb.com, arXiv)Runs ~1/3 of dependent tests, catches >99.9% faulty changes, testing cost halvedPartly: later step after deterministic selection
Speculative merge queue with success-probability thresholdUber SubmitQueue (arXiv, ICSE 2025, repo)CI resources -53%, CPU -44%, P95 wait -37%Partly: GitHub queue has no prediction; the "speculate only on likely-green" idea applies
Merge queue with dynamic batch sizeShopify (blog); Mergify (docs); GitHub (docs)Shopify: >90% of core PRs via queue; Mergify example batch 3 x 2 parallel: 7 PRs in 10 min vs 1 h. Graphite publishes no numbers (blog)Yes: built into GitHub, setting only
Batch post-submit ("milestones"), presubmit limited to affected projectsGoogle TAP (Memon et al.)Milestone every ~45 min, up to 4.2M tests; each target runs only at its latest affecting changeYes: full suite once per batch, not per PR
Two-tier CI: fast checks gate queue entry, exhaustive at mergeMergify (docs); Chromium CQ dry run vs Quick Run (docs)QualitativePartly done (affected on push, full at approval)
Retry failed shard; ignore if also fails on tip of treeChromium CQ (docs)QualitativeYes
Auto flaky detection + suppressionSlack (blog); Trunk (site)Test job failures 57% -> <5%; main stability 20% -> 96%; 550 PRs/wkYes: no flaky quarantine today
Local fast checks before push; cap CI roundsStripe Minions (stripe.dev)Local lint <5 s; max 2 CI rounds per run; 1,000+ agent PRs merged/week; auto-applied test autofixesYes: agent-specific
Agents self-review locally before opening PROpenAI Codex (openai.com)QualitativeYes (review:self exists; extend to tests)
Agent as CI first responderAnthropic (claude.com)First sitrep in ~15 min; 56% fewer alerts after root causes fixedPartly
Remote build/test cacheNx Cloud: Caseware 93% hit (nx.dev); Hetzner 85% hit, 45 -> 6 min (nx.dev)Vendor-publishedPartly: pays off only if tasks are cacheable (typecheck/lint/build)
Managed/faster runnersBlacksmith: Ashby -75%, VEED -70% cost (customers)Vendor-publishedPartly: PropFlow already self-hosts; capacity, not price, is the cap
Per-agent throttlesGitHub Copilot agent: exponential backoff 1h->48h for non-admins (gh-aw); one agent job per engineQualitativeYes: agent-specific

Top 5 PropFlow is not doing (ranked)

  1. Collapse 43 workflows into one orchestrating workflow with a path-based job graph. Effect: removes ~43 runner pickups/checkouts per PR event; plausibly 60-80% fewer workflow runs (inferred from 36k runs / 450 PRs = 80 runs/PR). Effort: 3-5 days, done in pieces. Dependency: none (update branch-protection required checks).
  2. GitHub merge queue with batching (min 1 / max ~8, build concurrency capped) and full suite only on merge_group. Effect: full suite runs once per batch instead of per PR; Uber-style queues cut CI ~53%. Effort: 1 day. Dependency: GitHub setting plus merge_group trigger.
  3. Per-agent CI budget: max 2 CI rounds per PR, then local-only until green; per-session concurrency cap. Effect: bounds the pushes-per-PR tail, which the median hides. Stripe model. Effort: 1-2 days (a hook in the fleet plus a counter check). Dependency: none.
  4. Automatic flaky quarantine (detect by pass-on-retry, auto-skip + auto-issue). Effect: Slack took failed test jobs from 57% to <5%; for PropFlow, fewer agent fix-loops and reruns. Effort: 2-3 days in-house, or hours with Trunk. Dependency: none or vendor.
  5. Remote cache for typecheck/lint/build outputs (Turborepo/Nx remote cache on S3). Effect: 85-93% hit rates published for named customers; highest value on per-push guard jobs where most inputs are unchanged. Effort: 2-4 days. Dependency: vendor or S3 self-host.

Also worth copying: treat the test selector as critical infra and load-test it at 10-20x (Anthropic).

Facts checked tonight

What others do about CI for fleets of coding agents (research, September 29)

Fede asked whether we are the first to hit this. We are not, but nobody has published a before-and-after CI bill for an agent fleet. The closest is Linear, whose test suite quadrupled after agents arrived; they moved off GitHub runners, sharded more and roughly halved runner time per test. Large monorepos (Google, Uber, Stripe, Shopify) solved the same shape at bigger scale and those lessons transfer.

The local numbers that matter

FactNumberSource
PRs opened on Sept 28462GitHub search
Workflow runs per dayabout 36,000Sept 28 census, 2-hour sample scaled
Workflows started by every PR push43 (about 32 after today's cuts)workflow files on main
Commits per merged PR, median1last 40 merged PRs

Most PRs are one push. One push fans out into 40-plus workflows, and a PR fires again on the review verdict, body edits and labels: roughly 75 workflow runs per PR. Boot, checkout and install overhead on 36,000 runs a day is the structural cost.

What transfers, ranked

  1. One PR workflow, not forty. One job graph that skips what the diff cannot affect. Largest lever not yet pulled; it is the redesign held back on Sept 29 and should come next.
  2. Affected tests per push, everything once per merge. Stripe runs ~5% of tests per build for under a tenth of the compute (source); Shopify cut p95 from 45 to 18 min (source). Done for tripwires in #13861; the approval-time full suite is the remaining candidate.
  3. Batch the merges. GitHub merge queue tests a batch of approved PRs together and bisects on red. Google reports 72% fewer machines at batch size four (source); Uber cut CI resources 53% (source). At ~400 merges a day the approval-time full suite would run per batch, not per PR. Needs required-check rewiring (branch protection: Fede).
  4. No CI on non-code events. Body edits, labels and review comments start runs today; only pushes and the final verdict should.
  5. The agent's machine is the first gate. Codex and Copilot coding agent run tests in their sandbox before opening a PR. Here: affected tests only, serialized through one lock, never the full suite locally (two concurrent full suites pushed the Mac mini to load 110 on Sept 29).
  6. Content-hash result caching for typecheck, lint and build (Turborepo/Nx remote cache; Mercari reports ~50% CI reduction, source). Fits our build steps, not vitest itself.
  7. Runner settings that cut refusals without a quota request: spot allocation by price-and-capacity instead of lowest price, a small warm pool in working hours, dependencies prebaked into the image (RunsOn, AWS). Vendor percentage claims unverified; effect unmeasured until tried.
  8. Quarantine flaky tests. Google found 84% of pass-to-fail transitions are flakes (source); each flake here costs a full re-run round.

Recommendation

Today's six cuts remove about half of machine minutes. The next half is items 1, 3 and 4, in that order: the one-workflow redesign first because it is inside the repo, the merge queue second because it touches branch protection. Not proposed: raising AWS limits (Fede: never) and capping concurrent agents (an owner decision, not engineering). Status: proposed, pending Fede's go on item 1.

Research agents' full notes: merge queues and batching, selective testing, agent-fleet practices, spot runner settings (four web-research passes, Sept 29). Other sources: Linear write-up; Meta predictive test selection; Google TAP; Uber SubmitQueue; Mergify on merge queues and agents.

Job-by-job audit, September 29: what stays, what goes

Fede: "each job manually inspected: is it adding value, is it necessary; if not we're cutting to the bare bone." Measured from every job in a two-hour weekday window (Sept 28, 14:00–16:00 UTC: 3,558 runs, 9,750 jobs), scaled to a day. Session 004, 2026-09-29.

Where the minutes go (machine-minutes per day, measured)

JobMin/dayShareVerdictSaves/dayRisk accepted
Guard pins (ci.yml, 6 shards, every push)36,10046%Run only when the push touches what it pins, plus once at approval and on main≈25,000a pin break on an unrelated push shows at approval instead of at push (approval still blocks merge)
Unit tests, full 4-shard re-run at approval6,3008%Keep; move 4×8-core to one 32-core spot box≈1,200none on coverage; one box to boot
Unit tests, affected lane (per push)6,1008%Keep (this is the core)0—
Type check + lint + build gate4,4006%Keep (core). Note: the required "Build" check is an echo on PRs; Vercel is the real build0—
self-review.yml (per push)3,3004%Cut: the pre-push hook and the Lint job already run the same checks3,300body-shape feedback comes from the reviewer instead of a separate check
Advisory harnesses on every PR (turnover, lambda bundles, portfolio, quality-replay, one-call, sync, intake, lease draft, AppFolio loop)3,4004%Fold into one "harnesses" workflow that runs on main after merge, not per push≈2,400a harness break is found after merge and fixed forward; they fail 4–8% of runs today, so some catches move later
Reviewer bot machine time (not the model)2,9004%Keep; drop the 90-second idle "settle" sleep≈900none
Semgrep scan (every PR)2,2003%Scan only auth, payments, secrets, middleware paths per PR; full scan weekly≈2,000a finding outside those paths waits up to a week
Three tiny PR gates (size, collisions, description)3,1004%Done today (#13816): one machine instead of three≈1,500none
Verdict and plumbing jobs inside ci.yml (preflight, manifest, decision, seed cache, gate, canary, 4 claim reports)≈2,0003%Fold into the main job; require one "CI" check instead of four names≈1,500needs a branch-protection edit (Fede)
Auto-merge arm/disarm (10,000 runs/day)1,5002%Drop duplicate wake triggers≈600arming a few minutes later in rare cases
Review babysitting (10 workflows: recheck, override, repin, infra retry, watchdog, dispatch guard…)≈7001%Replace with one review workflow that owns the status plus one 15-minute sweeper≈500none; removes today's races between them
Deploys (Lambdas, renewal worker, Vercel promote)2,7003%Keep; Lambdas: skip lanes whose bundle is unchanged≈500hash check must be exact
All nightly and scheduled runs together≈2000.3%Overnight SMS/email/voice traffic ends today (#13827). The rest goes weekly: nightly.yml has failed 81 of its last 100 nights and nobody acted≈150model or prompt drift seen up to 7 days late
Total78,500100%≈39,000 (50%)

Sources: job records for the window above (scratch file census.md, session 004); ci.yml, per-PR and scheduled workflow inspections (three reports, same session). Minutes at GitHub's ARM rate would be about $11,800 a month; the September bill is higher because GitHub rounds every job up to a whole minute and thousands of jobs run under 20 seconds. Since Sept 29 the same minutes run on our own AWS spot machines at roughly a tenth of that rate, so cutting minutes now matters most for staying under the 512-vCPU spot ceiling and for the on-demand overflow, which costs 5–10× spot.

Decisions for Fede (yes / hold, one line each)

  1. Guard pins only when the push touches what it pins, plus at approval and on main. Biggest single cut, about a third of everything. Recommended: yes.
  2. Cut self-review.yml. Duplicate of the pre-push hook and the Lint job. Recommended: yes.
  3. Advisory harnesses run once on main after merge, not per push. Recommended: yes, with the red-main alert kept so a break pages the owner.
  4. Semgrep on sensitive paths per PR, full scan weekly. Recommended: yes.
  5. Require one "CI" check instead of Build, Type Check, Unit Tests, Lint. Lets the plumbing jobs fold away. Needs the branch-protection edit. Recommended: yes.
  6. Nightlies weekly. Recommended: yes.
  7. Redesign for AI-volume (bigger, later): one fast job per push (types, lint, affected tests, ≈8 min on one box) and the heavy set (guards, full suite) once per merge batch through a merge queue with a minimum batch of 4. Cuts the heavy tier another 60–75%. The July merge queue failed because batches of 1 and flaky tests were never quarantined; both must be fixed first. Recommended: hold until items 1–6 land and are measured for a week.

Items 1–6 are each one PR, dark for nothing (CI only, no customer effect), and reversible by revert. Together they remove roughly half of today's machine time and about 15 machine boots per push.

One-workflow fold: runs per PR push (measured 2026-09-30 02:45 MDT)

We are folding the PR-triggered workflows into a single CI run. Five of 13 pieces were merged by 02:30 (#14062, #14089, #14107, with #14111 armed and #14123 stacked; #14116 widens the runner pool). Measured, not estimated: for each PR head SHA, every workflow run with that head_sha across all events.

Samplen pushesmedianp90mean runs per push
Before (Sept 28 12:00Z – Sept 29 00:00Z, every 3rd PR)25334532.1
After (pushes since piece 4 merged, Sept 30 07:16Z–08:06Z)1823.53524.8

≈29% fewer runs per push so far; target is 60–80% once the wake list (piece 9), the docs fold (piece 7) and the regression gate (piece 8) land.

WorkflowBefore avg/pushAfter avg/push
Auto-merge on green (all PRs)4.243.72
CI (the folded lanes now run inside CI, one run per event type)1.842.61
Claude Code2.682.06
Docs-only fence2.201.72
PR body gate2.00folded into PR gates (1.67)
Self-review2.00folded
Review execution re-pin1.761.50
Claude Review Check Override1.761.50
Branch Preview Domain—1.28

Caveats: pushes are approximated by PR commit time; machine-minutes per push are not measurable via the timing API (self-hosted runners report 0 billable ms).

Measured 2026-09-30 after pieces 7, 8, 9 and 12

A PR push now starts a median of 14 workflow runs instead of 33, measured on 20 real pushes after the fold against 25 before it.

MetricBeforeAfter
Median3314
p904521
Mean32.116.1
Remaining runs per pushAvg/push
Auto-merge guard3.2
CI2.6
Claude Code1.75
Branch Preview1.55
Review re-pin1.4
Review override1.4
CI dispatch guard1.05
Claude Code Review1.05

Merged: piece 7 #14142, piece 8 #14123, piece 9 #14196, piece 12 #14127. Next: cut the guard's wake-ups (piece 13).

CI fleet inspection, September 30: what is left to save

Measured 2026-09-30. Findings only, no changes made. Each item was checked twice.

The 10 biggest items

#Item$/monthHours/monthActionSizeStatus
1Put more CI machines on spot: open the x86 type to more instance families, check weekly where AWS has spare spot$100 (up to $300)~2tuneSPR 2 below
2One of six guard-test groups takes 318s, the others ~170s. Every PR waits for it. Rebalance by measured runtime$0~90 (up to ~1,000) waitingtuneS-MPR 1 below
38-core machines ran 1,451 hours on-demand in September, though the settings ask for spot. Find out why~$200 (estimate)~0rethinkMRefuted as written. Diagnosis running, no PR
4Stop the author pushing again after a yellow-only review~$150~45-60rethinkSRefuted as written, part held. Proposed
5Full-history git checkouts cost ~27s extra per job. Fetch file contents only on demand, keep history$5 (up to ~$120)5-25 machinetuneSPR 3 below
6Faster disk for restoring saved node_modules (~15s per restore)$3 (up to $15)~3tuneSProposed
7Merge the two start-up jobs (Plan and Preflight), save one machine boot$1-2 (up to ~$12)~4tuneS-MProposed
8Full-suite groups are uneven (slowest 594s, average 368s). Re-measure after the cut from 12 groups to 4, then rebalance$0up to ~3.8 min per full runtuneSRe-measure first
9CI machines run in only 2 of 6 zones. No stack setting adds zones, so this needs a network redesign$0 as proposed0rethinkM+Proposed
10Keep a few machines warm so short jobs start faster-$75 to -$15010-25 queuetuneSSkip costs more than it saves

Rows 3 and 4 are not counted in the totals. A higher run volume could make rows 5 and 7 several times bigger (see caveats).

The next 3 PRs

  1. Rebalance the guard-test groups by measured runtime. Include a planted control: drop one file and the check must go red. First confirm the slow group is not one giant file. A fix, no switch, live everywhere.
  2. Widen spot on the x86 machine type and add a weekly spot-capacity check. Never narrow the ARM choices. A third zone is a separate network design, not part of this PR.
  3. Switch full-history checkouts to fetch file contents on demand. Full history stays, so finding the common base commit still works. Prove it against the planted controls.

Two structural findings

FindingWhat happensStatus
Spot-allowance snooze loopA burst of jobs hits the 512-core spot cap. RunsOn then turns spot off for 5 minutes. Every job in that window runs at full price, which fills the 320-core on-demand cap. Launches then fail and jobs stall.Sep 29 fix moved every runner back to spot. Rows 1 and 3 and PR 2 target the cause. Quota increases are ruled out.
Auto-merge guard crashThe guard passes its data to itself as one blob. The blob went over the 128 KB environment limit. The guard exited with code 126 and PRs were disarmed. It fails closed: nothing merged wrongly, PRs just stopped auto-merging.Status not recorded in the inspection source. Owner to confirm.

Measured today

MeasureValue
Workflow runs per push (median)33 before, 14 after (p90 45 to 21)
Machines started per push23
Machine jobs under 60 seconds (boot share)51%
EC2 cost, Sep 29 (ARM spot capacity ran out)$243
Normal day of CI machines$8 to $20
Fleet mix (sample of 163 machines)62% spot, 37% on-demand; 108 in zone 1a, 55 in zone 1b

Methods and caveats

30-day CI waste audit: where time and money go

Audit — 2026-09-30, partial coverage. Read-only, no changes made.

Coverage

This is a sample, not the full month. We read 210,500 runs out of an estimated 700,000 to 750,000, and 2,909 merged PRs out of an estimated 4,000 to 4,500. We have no per-machine minutes, because the GitHub API budget ran out. Dollars come from the AWS bill.

Where a PR's time goes

StepMedian (hours)Slowest 10% (hours)
Open to first review verdict0.100.27
First verdict to approval0.311.58
Approval to merged0.070.31
Open to merged0.654.01
Merged to live1.132.46

Based on 80 random merged PRs. For the median PR, getting live takes longer than getting merged.

Outcomes of runs: 38% were skipped with no work done, 6% were cancelled, and cancelled runs used 23% of CI time.

The 10 biggest waste items

#ItemWhat$/month (est.)Action
1On-demand fallback8-core and 2-core machines fall back to full price when spot is not available1,500-2,000Rethink
2CI reruns on approval5,634 CI runs started by a review approval, 45% of CI runs, on code already tested700-900Delete
3Cancelled CI23% of CI time is cancelled work500-700Rethink
4Checks on every pushSecurity scan, quality-gate POC, voice, turnover and other suites run with no path filter400-600Limit
5Review bot failures20% of review runs fail and 7.5% are cancelled300-500 plus model costRethink
6Renewal worker deployRuns after every main build, 1,686 runs200-300Limit
7Helper-job fan-outAbout 40% of all runs are small jobs reacting to other jobs (auto-merge, re-pin, override, recheck, reruns)200-400Rethink
8Five separate PR gatesBody, self-review, size, collisions and dispatch guard each boot a machine150-250Rethink
9Always-skipped workflowsAbout 40,000 runs that almost never do anythingunder 100Delete
10Deploy delayMerge to live is 1.1 hours at the mediantime, not dollarsRethink

Code review waste

Corrections

The approval-time rerun already skips when the PR's unit tests concluded (G5 in scripts/ci/decide-test-lane.sh), so the 45% figure is a run count. The machine-time share is being measured separately.

Did small PRs cause the CI jam? — 2026-09-30

Brief — 2026-09-30. Read-only, no changes made. Figures marked estimate are estimates.

Verdict

Partly, and only a little. The jam comes from how many PRs our parallel sessions open at the same moment, times a big fixed cost every push pays no matter its size. The size rule adds maybe a few percent on top (estimate).

Options

OptionCI minutes/weekTime to mergeDefectsEvidence
A (recommended)Keep small PRs. Cut the fixed cost per push. Finish folding the side checks into one job, stop the reruns on approval, cancel a run as soon as a newer push arrives, and add a pacer that queues new PR runs past ~40 an hour instead of booting more machinesEstimate -30 to -50% (side checks ~12 of ~55 min per push; reruns on approval were 45% of CI runs before the Sep 29 fold)Same or faster (less queueing in bursts)No changeAudit and fleet inspection, plus this sample
BRaise the guidance to ~600 lines and stack changes locally before openingEstimate -5 to -10%. One 900-line PR instead of 3x300 saves ~2 pushes (~110 machine-min), but only a small share of PRs are splits (unknown how many)Unknown. 601-1,000-line PRs took a median of 0.6 h, the same as small onesWorse. 601-1,000 lines: 5.9 review rounds and 0.9 blocking findings per PR. 101-300 lines: 1.3 rounds and 080-PR review sample (only 10 PRs sized 601-1,000, so weak)
CKeep small PRs, but a session opens its chain as one PR train and CI runs only on the last PREstimate -10 to -20% on chains (unknown what share of PRs are in chains)Slower for the first PR in each chain. One failure blocks the whole trainNeutral. Review still sees small piecesNo data on chain share

Recommendation: A. Keep the size rule. It is the cheapest defect filter we have. Put the effort into making each push cheaper and into pacing bursts. A and C can stack later.

Per PR by size

Size (lines)PRs (review sample)Pushes, meanWorkflow runs, meanReview rounds, meanPRs with a blocking findingOpen to merged, median hWall-min per 1,000 merged lines
100 or less81.3261.000.151,082
101-300211.5361.300.72330
301-600281.8473.020.56187
601-1,000102.2595.930.61119
Over 1,000133.3908.671.2743

Based on an 80-PR review sample plus a 1,303-PR join to the sampled run list. The run list keeps only the newest 1,000 runs per 3 hours, so push and run counts are undercounts; the ratios between sizes hold.

Measured: runs per push, before → after

PeriodPRs measuredWorkflow runs per push (median)
Before (Sep 22-26)12 merged32
After (today)12 opened today15
Change-53%

Machine-minutes are not comparable yet: the after figure (about 62 per push) was measured job by job, but the before figure was not, because the API budget ran out.

Side findings

Run inspection, September 29: are we running too many checks?

Documented, no changes made to the checks yet. Decisions pending Fede. Session 004, 2026-09-29.

Six changes merged today, every run followed

MeasureValueSource
Job records1,495GitHub run and job records for the six changes
Never started a machine861 (58%)
Machines started634
Machine starts under 60 seconds323
Machine time in the first, useful run47%
Machine time on a version later replaced50%
Machine time repeated on the same version2%
Size of one changesee line below

A one-push change used about 25 runs and 40 machines. A change with 3-4 pushes used about 100 runs and up to 165 machines.

Decisions needed from Fede

  1. Approve the order: narrow triggers first (A), combine tiny gates second (B), fewer pushes per change third (C). Recommended: yes.
  2. Retire checks that have not failed in the sample (D)? Recommended: not yet, one check at a time.
  3. Act now, or wait for the first full day on our own AWS machines (2026-09-30)? Recommended: start A now, since it does not depend on the measurement.

Next three actions

  1. Gera's swarm (owner of the check settings) narrows what wakes the helper jobs (A).
  2. Session 004 combines the tiny per-change gates into one job (B).
  3. Read the first full AWS cost day on 2026-09-30 and report before and after.
Why small jobs stopped saving money

Small separate jobs were cheap on GitHub's machines. Since 2026-09-29 almost all jobs run on our own AWS machines, where every job boots its own machine. Many tiny jobs now each pay boot time. That is the mechanism; the dollar size is not measured yet (first full AWS day is 2026-09-30).

Of 51 per-change helper workflows in the inventory (repo workflow files and one GitHub run count each, 2026-09-29): 16 keep, 2 combine into one job, 13 fire on events they do not need, 13 are retire candidates (advisory and zero failures in the sample: a screen, not a verdict), 7 unclear. 22 of them ran at least 2,500 times in 7 days. The API caps the count, so those are floors.

What is already done today
ItemBeforeAfterSource
Job settings pointing at AWS6163+repo workflow settings, 2026-09-29
Job settings on GitHub's machines1614, kept on purposesame; the four are two recovery jobs, the nightly voice test, and one job needing a tool the AWS image lacks
Price per machine hour$0.30 GitHub$0.03-$0.056 AWS spot, 8 GBGitHub price list ($0.005/min ARM); AWS spot price history 2026-09-29
GitHub bill per day, before creditsaverage $633 (Sept 20-28), about $19,000 a monthn/aGitHub billing usage API
Monthly estimate after the moven/aroughly $3,000 (mostly spot) to $6,500 (mostly full price)Estimate, not a bill.
Measured so far on AWSn/a$55 for the partial dayAWS Cost Explorer, 10:03 local

Four changes: three merged (09:24, 09:47, 10:12 local). One safety follow-up is open. It keeps six production-writing jobs on machines AWS cannot reclaim.

Limits we must live inside
Options, with a recommendation
OptionExpected effectRisk
A Recommended first Narrow what wakes the helper jobs. Stop restarting on description edits, labels, reviews and comments where not needed. Let a new push cancel the older review and re-pin runs.Removes most chain reactions and skipped runs.Low. Touches the merge guard, so it needs an attended merge.
B Combine the tiny per-change gates into one job on one machine.Fewer machine boots.Low.
C Fewer pushes per change: run the local pre-check before pushing and batch fix-ups.Pushes are the biggest driver of machine time.None technical. A working-habit change for the agent fleet.
D Retire advisory checks with no failures in the sample, one by one, after confirming each is covered elsewhere.Fewer runs.Medium. Needs a per-check decision.
E Do nothing more until one full AWS day is measured (2026-09-30).Better numbers.Spend and queueing continue meanwhile.

Recommended order: A, then B, then C, with D decided from the inventory. Owner of the check settings: Gera's swarm. Session 004 can take B.

What we could not measure
For engineers

Changes: #13737, #13747, #13771, #13780. Rollback: set repo variable CI_RUNS_ON to 0. Audit was read-only, run on Sonnet 5.5.

Per-change table (jobs classified once each; minutes are job durations on machines)

PRlinespushesrunsjobsmachinesminUSEFULDUPSUPERSEDEDNOOP(<60s, inferred)other-cancelled
1376636125864072.785%5%02%8%
13768311248439103.393%5%02%0
13759189391320118229.843%1%54%1%0
137613143102338144337.438%2%57%1%2%
13737333298282128308.848%0%49%1%2%
137143814109378165361.134%1%64%1%0

Pattern: about 24 runs for 1 push, 25-35 runs per extra push. 323 of 634 machine jobs (51%) ran under 60 s (84 min total). Totals: 634 starts, 1,413 min; useful-first 214 starts (657 min); superseded 345 (701 min); duplicate on same version 20 (24 min). Source: GitHub run and job records.

Top over-firing workflows, cause and smallest fix

  1. ci.yml: 582 no-machine job rows plus 216 wasted-or-superseded jobs. Fires on pull_request (opened/synchronize/reopened/labeled/ready_for_review) AND pull_request_review submitted; review events run a full preflight, mostly skipped. Fix: drop pull_request_review from `on:`, or gate at workflow level.
  2. auto-merge-all.yml: 206 runs in the 1,000 (67 skipped/cancelled). on has pull_request x6 types incl. edited/labeled/unlabeled, review x2, repository_dispatch x2, workflow_run from 5 workflows; cancel-in-progress false. Fix: remove edited/unlabeled, cancel-in-progress true for pull_request events.
  3. claude.yml: 96 of 99 skipped. Fires on every issue_comment, review comment, review, issue. Fix: filter on the trigger phrase earlier, or drop pull_request_review.
  4. review-execution-repin.yml: 57 workflow_run + review runs; 11 of 18 in my PRs had no machine. Triggers on CI completed (every CI run) and every review; cancel-in-progress false. Fix: workflow_run only, and cancel-in-progress true.
  5. rerun-on-runner-shutdown.yml: 79 runs, 75 no machine. workflow_run on CI and Deploy Lambdas completed, job `if: conclusion == 'failure'`. Fix: none needed for machines; the run count is API/queue noise. Add `branches:` is not possible for workflow_run success, so accept it or use a check_run-failure trigger.
  6. self-review.yml: 4 cancelled after 5+ min. types include edited; cancel-in-progress true but runs long. Fix: remove `edited`.
  7. claude-code-review.yml: superseded runs finish (cancel-in-progress false), 15 jobs / 31 min. Fix: cancel-in-progress true.
  8. quality-gate-poc.yml: 27 superseded jobs / 31 min. `pull_request` with no paths filter (intentional, required check). cancel-in-progress is already true; the waste is jobs finishing before the next push. Fix: none cheap.

Small per-change workflows: class table

ClassMeaningWorkflows
AKeep16
BMerge into one per-change job2
CNarrow the trigger13
DRetire candidate (screen, not a verdict)13
EUnclear7

Cost-motivated workflows

Full inventory (51 workflows)

Runs in 7 days is capped at 2,500 by the API, so those rows are lower bounds. Fail, skipped and median come from the first 100 runs returned, a recent sample, not 7 days. Class D is a screen, not a verdict.

workflowpurpose (plain)origincost-motivated?blocks merge?triggersinstalls deps?runs/7dfail %skipped %median minclassnote
branch-preview-domain.ymlCreates/removes a preview URL for PRs titled [preview]Gera approved 2026-08-04 (wildcard CNAME)NADVISORY (feature, not a gate)pull_request(opened,reopened,synchronize,closed); schedule; workflow_dispatch; no paths filter; wf concurrency, no cancel; 3 job(s)setup-node only>=2500 (API cap)000.6CBoots a machine on every PR push just to read the commit message for the [preview] token; header explains it cannot move to a job-level if
ci-dispatch-guard.ymlSays so when GitHub never started a PR's pull_request checks#7061, 2026-09-04: "Three consecutive pushes produced ZERO pull_request workflow runs"NADVISORYpull_request_target(opened,synchronize,reopened,ready_for_review); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)100.3AMust stay on pull_request_target by design, so it cannot be folded into a pull_request job; fail rate 1% (real signal); no checkout, ~20 s
ci-nested.ymlBuilds nested packages on their own lockfiles; posts "Nested Gate"2026-07-29 CI-cost sweep: "ONE job, not six"Y (already consolidated)BLOCKS (Nested Gate, repo ruleset "Require CI")pull_request; no paths filter; wf concurrency, cancel; 1 job(s)yes (npm ci)>=2500 (API cap)000.5AAlready collapsed from 6 jobs to 1; npm ci only for touched packages
claude-code-review.ymlOpus reviewer; writes the required `review` check"THE CONTRACT: every push to a non-draft, reviewable PR gets a verdict"NBLOCKS (`review`)pull_request(opened,ready_for_review,reopened,synchronize); workflow_dispatch; no paths filter; wf concurrency, no cancel; 2 job(s)yes (npm ci)>=2500 (API cap)1324.9A13% fail (infra); ~4.9 min median
claude-review-check-override.ymlTurns the `review` check green when the bot APPROVES despite a yellow summaryPR #977 round-5 incident, 2026-05-16; "proper fix lives in PR #1008"NBLOCKS (can write `review`)pull_request_review(submitted); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)0590.2CFires on every review submitted; 59% skipped by job-level if. Possibly superseded if #1008 landed (not verified)
claude-review-recheck.ymlCarries last review verdict to a new head SHA without re-running the LLM2026-08-21; "existed to close that gap instantly and for free"YBLOCKS (writes `review` on new SHA)pull_request(synchronize); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)030.4A~0.4 min vs ~4.9 min for a full review
claude.ymlResponds to @claude mentions on comments/issuesno headerNADVISORYissue_comment(created); pull_request_review_comment(created); issues(opened,assigned); pull_request_review(submitted); no paths filter; job-level concurrency; 1 job(s)no>=2500 (API cap)0985.0C98% skipped: fires on every comment/review event; skipped jobs are unbilled, so this is run-list noise more than cost
dependabot-review-gate.ymlPosts a passing `review` status on Dependabot/Renovate PRs2026-07-13, first hit PR #3145NBLOCKS for bot PRs (writes `review`)pull_request_target(opened,synchronize,reopened); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)0100-A100% skipped in sample (no bot PRs); skipped = $0
docs-only-fence.ymlFails/disarms a PR that claims docs-only but is not, or is on a stale base#3521, 2026-07-10 (squash reverted prod fix)NADVISORY (but applies hold-for-review + disables auto-merge)pull_request(opened,labeled,synchronize,reopened,edited); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)0100-C100% skipped in sample; header says job-level if is free, saving would be run-list noise only
lambda-bundles-check.ymlBuilds every Lambda bundle once per PR (no AWS)"CI cost guard (added 2026-05-31)": replaced ~7x fan-out of deploy previewsYADVISORYpull_request; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)>=2500 (API cap)001.6D0 failures in sample; it is itself the cost guard. Whether ci.yml Build already bundles Lambdas not verified
main-lane-red-alert.ymlPages #alerts when a harness lane goes red on main2026-09-14: Portfolio harness "stayed red for eleven hours"NADVISORYworkflow_run[Portfolio harness,CI (docs),CI,Application-intake identity harness,AppFolio loop harness,New-lease draft harness,One call, one email,Sync idempotence (identity harness)]; workflow_dispatch; no paths filter; wf concurrency, no cancel; 1 job(s)no>=2500 (API cap)0980.6Cworkflow_run on 8 workflows incl. PR runs; 98% skipped
pr-body-gate.ymlAdvisory comment when PR body misses required sectionsPR #8141 rounds 1-4; audit Part D #1Y (avoids a ~7-min Opus round)ADVISORY (always exits 0)pull_request(opened,edited,synchronize); no paths filter; wf concurrency, cancel; 1 job(s)setup-node only>=2500 (API cap)001.6DSuperseded by self-review.yml step 6/6 which runs the same body gate; 45% cancelled
pr-file-collisions.ymlLists other open PRs rewriting the same files#7995/#7996, 2026-09-12NADVISORYpull_request(opened,synchronize,reopened); no paths filter; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)000.4B100% success; checkout + gh API only; ~0.4 min
pr-size-tier.ymlComment when PR exceeds 300 changed linesaudit Part D #5, "PRs >= 1,000 substantive lines average 9.83 bot review invocations"NADVISORYpull_request(opened,synchronize,reopened); no paths filter; wf concurrency, cancel; 1 job(s)setup-node only>=2500 (API cap)100.5BNode setup only; ~0.5 min; overlaps the review:self size ceiling (#13432), not verified identical
quality-gate-poc.ymlLeasing regression gate (required) + two advisory lanesRestructured 2026-08-21 (Fede decision)NBLOCKS (`Regression Gate - Leasing (required)`)pull_request; no paths filter; wf concurrency, cancel; 3 job(s)yes (npm ci)>=2500 (API cap)000.8ARuns on every PR (no paths filter); 2 advisory lanes also boot+checkout on every PR (C candidate)
rerun-on-runner-shutdown.ymlRe-runs jobs killed by ARM runner reclaimMeasured 2026-09-10: "~0.5% of ubuntu-24.04-arm jobs die mid-run"Y (side effect of cost cut)ADVISORYworkflow_run[CI,Deploy Lambdas]; no paths filter; wf concurrency, no cancel; 1 job(s)no>=2500 (API cap)0950.6Cworkflow_run on CI and Deploy Lambdas; 95% skipped. CI_RUNS_ON=1 since 2026-09-28: whether reclaim still applies not determined
rerun-review-on-infra-failure.ymlRe-runs the review check after infra failure2026-09-13: "failed 70 of 200 runs today (35%)"NADVISORY (protects required `review`)workflow_run[Claude Code Review]; no paths filter; wf concurrency, no cancel; 1 job(s)no>=2500 (API cap)0870.5Cworkflow_run on review; 87% skipped
review-execution-repin.ymlWithdraws a bot approval if tests later fail on that head"30 of the 49 pull requests ... were approved before any executed Unit Tests"NADVISORY (dismisses approvals)workflow_run[CI]; pull_request_review(submitted); workflow_dispatch; no paths filter; wf concurrency, no cancel; 1 job(s)no>=2500 (API cap)0290.8CTwo triggers (workflow_run CI + review submitted); 29% skipped, 6% cancelled
self-review.ymlRuns review:self (ratchets, body shape, lint verdict) in CIaudit Part D #2; saves a wait on "a full Opus round"Y (avoidance)ADVISORYpull_request(opened,edited,synchronize,reopened); no paths filter; wf concurrency, cancel; 1 job(s)yes (npm ci)>=2500 (API cap)404.1C63% cancelled (edited/synchronize re-runs); 4 of 37 concluded failed; npm ci each time; ~4.1 min
semgrep.ymlSAST scan of PR diff2026-07-14 COST: "~$400/mo of Actions spend for zero marginal coverage"YADVISORYpull_request; schedule; workflow_dispatch; paths-ignore; wf concurrency, cancel; 1 job(s)no>=2500 (API cap)002.1E0 failures; unknown whether it fails on findings; RPX assessment cites it as SAST coverage
turnover-harness.ymlTurnover intake regression harnessturnover-intake-regression-harness.md section 4NADVISORY ("MUST pass on every PR" per header; not required)pull_request; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)>=2500 (API cap)001.8D100 of 100 success; volume at API cap (2,500+)
deploy-lambdas.ymlBuilds and deploys all Lambdas on main; PR preview for lambda dirs2026-07-19 CI-cost consolidation of 11 lanesYADVISORY (deploy)pull_request; workflow_run[CI]; workflow_dispatch; paths; wf concurrency, cancel; 24 job(s)yes (npm ci)21231804.5Cworkflow_run on every CI run; 80% skipped; 24 jobs
clara-behavior-evals.ymlUmbrella behaviour-eval sweep for any change to Clara behaviour"Tier-1 eval umbrella gate (Replication / Morpheus Gate)"NADVISORYpull_request; workflow_dispatch; paths; wf concurrency, cancel; 2 job(s)yes (npm ci)2074100.3E2,074 runs/7d but 0.3 min median with a 19-glob paths filter: most runs appear to exit at the budget job; per-job data not sampled
portfolio-harness.yml22 reproductions of the one-channel=one-property assumption (inverted suite)2026-09-14 red-main incident #8518 referencedNADVISORYpull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)2060803.8AFails 8%; runs on GitHub-hosted ubuntu-24.04-arm, not the RunsOn label; cancelled 5%
deploy-announce.ymlPosts one Slack digest per day of deploys"744 standalone posts in 14 days, zero human replies"NADVISORYworkflow_run[CI]; no paths filter; wf concurrency, no cancel; 1 job(s)no20110880.3Cworkflow_run on every CI run (incl. PR runs); 88% skipped
deploy-renewal-worker.ymlBuilds/pushes renewal worker image, rolls ECS2026-07-29 CI-cost: replaced a polling gate that "billed a runner 3-45 min per merge"YADVISORY (deploy)workflow_run[CI]; workflow_dispatch; no paths filter; wf concurrency, no cancel; 2 job(s)yes (npm ci)201116112.2Cworkflow_run on every CI run; 61% skipped, 28% cancelled; 12 min median when it runs
quality-replay-typecheck.ymlTypechecks quality-replay/ which root tsconfig excludesPR #6043 round 1NADVISORYpull_request; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)1538601.9AFails 6%
wo-creation-e2e.ymlProd end-to-end work-order creation harness2026-06-24 TEST-101 regressionNADVISORYworkflow_dispatch; pull_request(labeled); no paths filter; wf concurrency, no cancel; 1 job(s)yes (npm ci)1114099-C99% skipped: fires on every label event
ci-docs.ymlSupplies the required Build/Type Check/Unit Tests/Lint names for docs-only PRs#8645 "Native required checks belong to this workflow's suite"NBLOCKS (produces required contexts on docs-only diffs)push; pull_request; paths; wf concurrency, cancel; 2 job(s)no1024001.0ACheap; mixed PRs wait for ci.yml
pipeline-lab-evals.ymlRuns smoke preset corpus against the PR preview"Pipeline Lab preset regression gate"NADVISORYworkflow_dispatch; pull_request; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)955000.2D100% success at 0.2 min median (looks like mostly no-ops); 955 runs
appfolio-loop-harness.ymlRuns the whole AppFolio message loop against a stub"WHY IT RUNS ON EVERY PR (and BLOCKS, no continue-on-error)"NADVISORY to branch protection (header says blocks; not in required set)pull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)893502.3AFails 5%; red check but not a required context
one-call-one-email.ymlCounts every email/text one call produces2026-08-31: one call produced three notificationsNADVISORYpull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)629402.8AFails 4%
claude-type-sync.ymlComment-only: warns when Lambda types.ts mirror may be stale"PR #~408 era ... broke the Lambda esbuild bundle"NADVISORYpull_request(opened,synchronize,ready_for_review,reopened); paths; wf concurrency, cancel; 1 job(s)no609001.6DSuperseded in effect by lambda-bundles-check + ci-nested (both build the Lambda bundle); 0 failures; ~1.6 min median
require-eval-diff.ymlFails a prompt-file PR that has no eval changerepo rule "every prompt change ships with an eval"Y (made path-filtered; ~300 billed min/day removed)ADVISORY ("NOT a required context")pull_request(opened,synchronize,reopened,edited); paths; wf concurrency, cancel; 1 job(s)no3841100.8AFails 11%; 34% cancelled
voice-comprehensive.ymlLayered Camellia voice regression via ElevenLabs simulateno incident citedNADVISORY ("SOFT (report-only)")schedule; pull_request; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)381105.0E1% failed; report-only at ~5 min; not determined if reports are read; cancelled 7%
app-intake-harness.ymlReplays 11 application-intake scenarios against real writers, no network"makes NO model calls, so there is no spend"NADVISORYpull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)368302.4APath-filtered; ARM swap 2026-09-09 was a cost cut
sync-specialists.ymlSyncs voice-agent tools to ElevenLabs (PR = drift check)"CANONICAL voice-sync workflow"NADVISORYpull_request; push; paths; wf concurrency, no cancel; 2 job(s)yes (npm ci)358002.5D358 runs, 0 failures; the push half is a deploy and should stay
sync-idempotence.ymlSync tick writes zero new claims; one card = one inquiry#8922, #9731 (31 of 145 persons had >1 inquiry)NADVISORYpull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)317002.6D317 runs, 0 failures; own lane because unit lane selects by diff
renewal-replay.yml30 simulated days of renewal scan over seeded book2026-09-03 renewal change shipped three faultsNADVISORYpull_request; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)185002.1D185 runs, 0 failures in sample
renewal-evals-scoping.ymlGuards renewal thread -> work-order tool leak2026-07-04 Aria Sablan incidentNADVISORYpull_request; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)1034301.9E43% failed: real catches or noise? not determined
turnover-capture-gauntlet.ymlOffline vitest of persona builder (live half is manual)no incident citedNADVISORYpull_request; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)90001.7DHeader: "~5 seconds" of test on a booted machine with npm ci
new-lease-draft-harness.ymlReplays approve-to-lease-draft failures that really happened"Every scenario is a failure that actually happened"NADVISORYpull_request; push; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)83102.7A1 failure in 83 (1.2%, below 2% bar, non-zero)
intent-evals.ymlHaiku-backed intent-extraction evals when extractor files changeno incident citedNADVISORYpull_request; schedule; workflow_dispatch; paths; wf concurrency, cancel; 2 job(s)yes (npm ci)666703.7E67% failed (44/66): real catches, flaky, or ignored? not determined
renewal-evals.ymlVoice renewal tool-call evals (+ Monday gauntlet)no incident citedNADVISORYpull_request(opened,synchronize,reopened,labeled); schedule; workflow_dispatch; paths; wf concurrency, cancel; 4 job(s)yes (npm ci)58003.0D58 runs, 0 failures (PR side); nightly job elsewhere covers drift
renewal-evals-sms.ymlSMS renewal tool-call evalsno incident citedNADVISORYpull_request; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)49002.6D49 runs, 0 failures
schedule-drift.ymlChecks live schedules match config/automations.tomlADR-0123NADVISORYschedule; pull_request; workflow_dispatch; paths; wf concurrency, cancel; 2 job(s)yes (npm ci)291001.7AFails 10% (3/29)
sync-phone-numbers.ymlSyncs phone->agent assignments to ElevenLabs (PR = dry check)config-as-code, Camellia launchNADVISORYpull_request; push; schedule; workflow_dispatch; paths; wf concurrency, no cancel; 3 job(s)yes (npm ci)15701.7A1 failure in 15
phone-address-dump-drift.ymlChecks generated phone-address dump matches config filesDock row v6-config-dumpsNADVISORYpull_request; schedule; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)13001.7D13 runs, 0 failures; low volume
sync-turnover-intake.ymlSyncs turnover-intake voice agent tools2026-05-27 dead-URL incidentNADVISORYpull_request; push; paths; wf concurrency, no cancel; 2 job(s)yes (npm ci)5002.7D5 runs, 0 failures
closer-eval-freshness.ymlRe-runs closer-predicate evidence to check committed numbers are fresh"the eval-diff gate"NADVISORYpull_request; workflow_dispatch; paths; wf concurrency, cancel; 1 job(s)yes (npm ci)310002.0E3 of 3 sampled runs failed: unclear if that is a legitimate nag or a stuck check
provision-western-slope-proto.ymlDeploys the Western Slope prototype voice agent on merge"The ONLY door to the Western Slope prototype voice agent"NADVISORYpull_request; push; paths; wf concurrency, no cancel; 2 job(s)yes (npm ci)0---E0 runs in 7 days; no data

Why this came up

At the 2026-09-08 standup Fede said GitHub Actions is costing thousands a month. Aldo (ex-colleague, at Zillow) said he went from about $300 to $80 a month on Blacksmith and that it is faster. Fede ran Blacksmith's migration wizard on 2026-09-09; it opened PR #7437 swapping the runner label in all 66 workflow files. The goal Fede stated in session: about $9,000 of GitHub credit remains, it expires in twelve months, the run rate is over $3,000 a month, and the aim is to make the credit last longer, not to hit twelve months. Cash spend on a vendor is only worth it if speed matters somewhere.

Prior work on the same bill: a self-hosted runner on the Mac mini was tried and reverted on 2026-07-31 (Fede: no self-hosted runners). The August 9 to 11 sessions targeted a 50% cut and shipped push debouncing, incremental re-review and guard consolidation; the target was not reached. On 2026-08-18 the full test suite moved to GitHub's 8-core ARM pool (PR #5856), measured then at 7 min to 2.3 min wall and $0.35 to $0.22 per run. Blacksmith itself had never been evaluated before today.

The bill, measured

Source. Every job GitHub ran between 2026-09-08 23:28 UTC and 2026-09-09 17:04 UTC, pulled from the Actions jobs API: 2,982 workflow runs, 10,752 jobs, of which 4,395 actually executed (the rest were skipped by an if: and are not billed). Priced at GitHub's published private-repo list rates per runner label, each job rounded up to a whole minute as GitHub bills, then scaled to 30 days. These are modeled figures, not the invoice; the org billing endpoint needs an admin scope this session's login lacks.

ItemPer month (modeled)ShareWhat it is
Full unit-test suite, 12 shards on the 8-core ARM pool$64723%About 57 full-suite runs a day. 54% of the shards are triggered by review verdicts on hub-module PRs, 27% by pushes to main, 17% by PR pushes that escalated to full mode.
Type check, build, lint on every PR push$43015%Three jobs per push, roughly 120 to 180 of each a day.
Affected unit tests on PR pushes$2579%One shard per push, median 56 s.
AI code review runner, parked while the model reviews$1676%Median 5.1 min, p90 8.6 min per review, on a billed machine that does nothing but wait.
Guard pins$1134%Post-merge only since 2026-08-20; still 229 jobs a day.
Everything else, about 60 small jobs$1,22543%Gates, drift checks, preview mint and remove, harnesses, evals, deploys. Most finish in under 30 s and bill one minute each.
Total$2,839100%Matches Fede's "over $3k" once storage and the review bot's own spend are added.

What Blacksmith measured on PR #7437

Source. The PR's own checks ran on Blacksmith machines because a workflow edit takes effect inside its pull request. First run at 2026-09-09 17:44 UTC, a rerun of five workflows about an hour later after the first run had warmed what it could. GitHub-side numbers are the median of the same job over the sample above, and step timings come from one representative GitHub PR run (34379781500) against the Blacksmith rerun (34384721571).

Step or jobGitHubBlacksmithRead
Type check, the CPU-bound step alone79 s35 s2.3x faster Twice the cores show up where the work is pure compute.
Lint, the CPU-bound step alone5 s7 sSame.
Hermetic harness jobs (AppFolio loop, turnover intake, app intake, portfolio), whole job116 to 174 s68 to 79 s40 to 58% faster One run each on Blacksmith; consistent direction.
Semgrep scan, whole job77 s47 s39% faster
Installing dependencies29 s cache restore90 s full npm install3x slower, every job Main seeds the node_modules cache on GitHub's cache backend. Blacksmith redirects the cache action to its own backend, so nothing seeded on GitHub is visible there. This persists until the seed job itself runs on Blacksmith, which means a full move, not a partial one.
Git checkout5 to 25 s13 to 288 sUnstable One checkout took nearly five minutes; most took 13 to 26 s.
Lint, whole job, first run49 s629 sCold lint cache plus cold dependencies. Warm rerun: 141 s, still 3x slower because of the install.
Affected unit tests, whole job109 s74 s, then 533 s on rerunInconclusive The rerun selected a different set of tests and hit the slow checkout.
Small gate jobs (nested gate, quality gate, preview mint)14 to 17 s55 to 105 sSlower These jobs are almost all install time. On GitHub the install is a 29 s restore; on Blacksmith it is a full install.

Every check that matters passed on Blacksmith: build, lint, type check, all unit test shards, the leasing regression gate, the escape gate, the preview deploys, the harnesses. The one red check, the renewal conversation scoping eval, fails identically on main today (dispatched on main at 17:56 UTC to confirm) and is a prompt problem unrelated to runners. It is not a required check.

Pricing, side by side

Source. blacksmith.sh/pricing and docs.blacksmith.sh runner overview, fetched 2026-09-09; GitHub rates from the 2026-08-18 billing pull recorded in the CI workflow file and the August sessions.

MachineGitHubBlacksmithNote
Standard x64 (GitHub 2-core vs Blacksmith 4-vCPU, the wizard's mapping)$0.008/min$0.008/minSame price, twice the cores. Blacksmith bills per vCPU: $0.004 per 2-vCPU minute.
Standard ARM (GitHub 4-core ARM vs Blacksmith 4-vCPU ARM)$0.005/min$0.005/minSame. ARM is 0.625x the x64 rate on both sides.
8-core ARM (the test-suite pool)$0.014/min$0.010/min29% cheaper per minute. This is the only line where Blacksmith's rate is lower.
Free tierTeam plan minutes, not applicable to larger runners3,000 2-vCPU x64 minutes a monthAbout $12 of value at our volume.
RoundingUp to the minuteUp to the minuteThe $800 a month of round-up moves with the jobs.

Aldo's $300 to $80 result is consistent with a small team whose jobs are compute-bound and whose GitHub bill was mostly larger-runner minutes. Our bill is thousands of one-minute jobs, where the rate is identical.

Options

OptionGitHub credit burnCashSpeedVerdict
A. Stay on GitHub, close the PR, cut job count$2,840 today, falling with each cut below$0UnchangedRecommended Every dollar stays on the credit; the cuts are the only thing that actually extends runway.
B. Full move to Blacksmith (the PR as written)$0about $2,600 modeled at the same job mix and durationsCompute steps about 2x faster; installs and checkouts slower or unstable; net wall-clock unprovenNo Trades credit for cash with no bill reduction. Also moves the cache backend, so every job re-seeds until main has run there.
C. Partial: only the 8-core test shards (narrow the PR to that one label)minus $647about $460 modeled at today's shard durationsShards likely faster once the cache is seeded on Blacksmith; unmeasured, since full mode did not run on the PROnly if speed matters Cash for credit again, and the node_modules seed would have to run on Blacksmith too or every shard pays the 90 s install.

Cuts that extend the credit (Option A)

Ranked by modeled monthly saving from the same sample. Arithmetic: jobs in the 17.6 h sample x 1.364 for a day x 30 x billed minutes x rate. All are estimates until shipped and re-measured.

CutPer monthShape
Fold the auto-merge arm-or-disarm job into a step of an already-running job$99412 one-minute jobs a day that do 8 s of work. Mechanical.
Fold the two unit-test reporter jobs into the last shard's step$112465 one-minute jobs a day, 3 to 9 s of work each. Mechanical.
Gate or retire the Quality Gate POC's three jobs$97402 one-minute jobs a day. Needs an owner's call on whether the POC is still an experiment.
Fold preview-domain mint and remove into steps$62259 one-minute jobs a day.
Stop parking a runner for the AI reviewup to $167Re-architecture: the review runs inline on a billed machine for 5 to 9 min. The August 9 session already proposed an async executor on Lambda as "Decision 3", not committed.
Full suite on review verdictsup to $35054% of full-suite shards come from review verdicts on hub PRs, by design since PR #5518 so hub changes get full coverage before merge. Cutting this is a coverage decision, not a cleanup.
Remaining sub-30-second jobs$125 to $165 realisticAbout 1,270 more tiny jobs a day across 40 workflows; maybe a third can fold or path-filter. Low confidence.

The mechanical cuts alone are about $370 a month, 13% of the bill, and take the credit from roughly three months of runway toward three and a half. Getting to a real stretch needs the two design decisions above (review parking, full suite on verdicts), which together are another $500 or so and would push runway toward five months.

Decided 2026-09-09 (Fede)

Shipped 2026-09-09

Measured after the cuts (2026-09-10)

WhatBefore the cutsAfter the cutsChange
Cost of one PR's CI run (median)$0.070$0.055-21% fewer jobs run per push (18 → 15, median), from moving the full suite off the PR path
Cost of one merge to main (median)$0.498$0.519+4%, flat the ARM savings got eaten by more of that work landing on the pricier 8-core runner (32% → 41% of merge minutes)
Jobs running per PR push (median)1815-17%
Share of all CI minutes on the old x64 machines36%12%real, clean win this is the ARM move working as intended, and it doesn't depend on how many runs happened that day
Time from a PR push to CI finishing (median, real runs only)21s24sflat
Time from a PR push to CI finishing (slow 10%)419s549sup — the PR lane's remaining work (affected tests + the merged lint/type/build job) is now a bigger share of what's left
Main went red after a merge8 of 28 pushes58 of 104 pusheslooks much worse raw, but see below

On that last row — the red-main count needs unpacking, not just quoting. Grouping the 58 red pushes into distinct incidents (not counting the same broken thing twice while it's being fixed forward) gives 14 separate episodes, not 58. Together they left main red for about 9.2 hours total across the 15.3-hour window (the longest single stretch was 1.9 hours, right after the cut shipped) — that's the time production promotes were held.

PropFlow Docs