improvement(ci): bound docker layer caches and right-size ten runners
Summary
Our Blacksmith docker layer caches had no eviction policy at all. setup-docker-builder skips pruning entirely unless max-cache-size-mb is set, and BuildKit's own GC is time-based only (8 days unused), so on a repo that builds this often nothing ever ages out. Measured over the last 30 days:
app.Dockerfile/linux-amd64— 351 GB, created a day earlierrealtime.Dockerfile/linux-amd64— 249 GB, for an image under 300 MB- layer caches in total — 920 GB across 10 disks
Sticky disk storage is billed per GB-month, and it was still growing.
What changed
- Cap the layer cache per image via a
cache_mbmatrix field, sitting alongside thebs_runnerfield that already encodes per-image sizing. App keeps 100 GB (several generations over its working set: ~34 layers plus monorepo apt/bun cache mounts); everything else takes the 25 GB default, still ~4x the tightest working set in the matrix (pii, whose spaCy models are ~2.2 GB). The fallback lives in the composite action rather than an inputdefault:— an unset matrix key arrives as'', which counts as "provided", so adefault:would never apply and a row that forgot the field would silently go back to unbounded growth. - CodeQL sizes per language.
javascript-typescriptpeaks at 19.5 GB (p95 over 3090 runs) and would OOM below the 8 vCPU tier, so it stays.actionspeaks at 1.3 GB and averages 22% CPU over a 39s median run — it drops to 4 vCPU. piiandrealtimeimage builds drop to 2 vCPU. Both ran on 8 vCPU earlier in the window, so the 8→4 step is measured rather than modelled: realtime went 52s → 51s, pii 24s → 27s.- Five desktop release jobs drop to 2 vCPU. They peak under 0.4 GB and finish in 4–13s.
- Those same five jobs get the
CI_PROVIDERfallback they were missing, against the invariant stated at the top ofci.yml. In GitHub break-glass mode they would have sat inqueuedforever.
Why this can't slow CI
Every downsized job sits off its parallel group's critical path, which is set by an app build in each case:
| Group | Critical path (p50) | Job downsized | Its p50 | Slack |
|---|---|---|---|---|
| CodeQL | js-ts 255s | actions | 39s | 216s |
| Build AMD64 | app 229s | realtime / pii | 39s / 28s | ~190s |
| Build Dev ECR | app 302s | realtime / pii | 51s / 27s | ~250s |
Left alone deliberately
- The 16 vCPU app builds. Memory-bound, not oversized — 16 vCPU measured 2.13x faster and 6% cheaper than 8 vCPU (488s×8 vs 229s×16 vCPU-s). The gain came from memory headroom, so it does not repeat at 32 vCPU.
- Lint and Test. Genuinely CPU-bound — 62% of the run sits above 80% CPU.
- Build App on push.
build-amd64has noneeds:, so this job is what stops a DB migration applying for a build that cannot ship. It is not redundant. next build's type-check pass. Looks duplicated againstturbo run type-check, butapps/sim/tsconfig.jsonexcludes.next, so only thenext buildpass validates Next's generated route types.
Type of Change
- Improvement
Testing
actionlint reports strictly fewer findings than before (the five hardcoded labels are now inside expressions) and zero new ones. Verified all 14 matrix rows resolve to a bounded cache value and that no job can reach the unpruned path. Job names are unchanged, and no ruleset requires status checks. bun run lint, check-block-registry, and all 37 check:audits pass.
Checklist
- Code follows project style guidelines
- Self-reviewed my changes
- Tests added/updated and passing
- No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)