Skip to content

CI comes home

CI comes home — a technical pencil sketch of a small haus with a conveyor belt running out the front door toward a distant cloud factory, the belt snapped mid-air; inside the haus a workbench now runs the same belt in a tight loop past five inspection stations, each with a small stamped seal. An Abyssinian cat sits on the workbench watching the first item pass inspection, one amber halo on the station that just caught a cracked part.

The month’s 2,000 free GitHub Actions minutes ran out, and the question was whether a local Jenkins would be the free replacement. It would have been free. It also would have been the wrong tool — and the audit that answered the question found something worse than a quota problem: CI had been dead for days, and main was broken underneath it.

The burn was never really 2,000 minutes. sanctum-rs runs its test jobs on macos-14 because the code needs macOS — security-framework wants a Keychain, and the mlx crates want Apple silicon. GitHub bills macOS-hosted runners at a 10x multiplier: a twenty-minute cargo build charges 200 minutes of quota. Ten pushes a month is the whole allowance. The ubuntu jobs were a rounding error; the macOS jobs were the bill.

And once the quota died, every run — macOS and ubuntu alike — failed in three seconds flat. Not a compile error, not a test failure: the job simply never started. In the run list this is nearly indistinguishable from a real failure, and nothing pages you about it.

The Jenkins path meant rewriting every workflow as a Jenkinsfile, wiring webhooks through a firewall that rightly admits nobody, losing the GitHub-native PR checks, and adopting a plugin treadmill as a hobby.

The better answer was already inside GitHub Actions: self-hosted runners are free. Minutes on your own hardware bill nothing, the workflows stay exactly as they are except the runs-on: line, and the runner polls outbound — no inbound holes, no tunnel, nothing for the firewall to forgive. One binary under launchd, the same way everything else in the haus runs:

actions.runner.Ogilthorp3-sanctum-rs.manoir (launchd, KeepAlive)

The hardware argument settles it. GitHub’s macos-14 runner is a throttled M1 with 7 GB of memory. The haus’s own silicon runs the same jobs several times faster, with a warm cargo cache that hosted runners rebuild from nothing every single run.

The runner came online and the first sweep told the truth main had been hiding. Three of five jobs failed — none of them because of the runner.

sanctum-proxy did not compile. The proxyd-hardening batch had pushed three call sites of ModelEntry::ttfb() but never the method itself — the definition sat uncommitted in a side worktree. Half a feature merged, and dead CI said nothing. The fix took the missing field and method verbatim from that worktree, so the in-flight work lands clean later. Clippy’s -D warnings hard gate — one of the standing gates in the haus’s engineering discipline — then flagged what else had slipped through the blackout: an unused variable in the same batch, and a question_mark lint in the swap-usage parser.

Then the deny job — the dependency-advisory hard gate, which had been quota-blocked on ubuntu since it costs nothing to run when CI is alive — executed for the first time in days and immediately earned its keep: RUSTSEC-2026-0204, an invalid pointer dereference in crossbeam-epoch 0.9.18, transitive via rayon and image. One cargo update -p crossbeam-epoch to 0.9.20, no code changes. cargo-deny only reads Cargo.lock, so the job now rides the macOS runner too — nothing in the repo bills a hosted minute anymore.

Third push: all five jobs green.

The second fence was quieter. The runner directory lives under ~/.sanctum — which is itself a git repo with a GitHub remote. The runner’s .credentials and .credentials_rsaparams (RSA key material for its GitHub session) were sitting untracked, one careless git add -A from a public leak — exactly the blanket-add that gitsync refuses on principle. gh-runner/ went into .gitignore before the first green run finished. A control that authenticates as you deserves the same fencing as the secrets it replaces.

CI cost: zero, permanently. CI speed: minutes instead of tens of minutes, warm caches. CI truth: the gates run on every push again, and the first thing the resurrected gates did was catch a real CVE. The quota was never the story — the story was that the haus spent days believing it had CI, and it had a red light nobody was wired to read.

The gates are read again now, on their own silicon, twice warmed and free. That is the kind of vigil he approves of.