Skip to content
FocusedCat
Build notes

Why we moved off cloud CI

Ivan · FocusedCat Studio

June 27, 2026


Our CI ran on a private GitHub repo, and one afternoon it just stopped. Not a flaky test, not a broken build — a billing block. A failed payment on the account tripped a hold that rejected every workflow run before it even started. Pull requests sat there with no status at all, like the checks had never been asked to run.

That's an uncomfortable thing to depend on for the one gate that decides whether code is allowed to merge. The outage itself was resolved in a day, but it left us with a question we couldn't unask: what else was quietly wired to "assume the cloud service is up"?

One command, run by a human

The fix we landed (ADR 0019) folds every check that used to live across separate CI jobs into a single local command, bin/ci — RuboCop, Brakeman, our security audits, the i18n grep, the throttle-registry check, and the full RSpec suite, all in one run. When it's green, it ends by recording a gh signoff status, and that status is what branch protection actually requires before a merge is allowed.

Deploys changed too. There used to be an automatic trigger that pushed to the dev box the moment main went green. Now a deploy is a deliberate act: someone runs kamal deploy from their own machine, over Tailscale, with secrets resolved through 1Password at deploy time rather than sitting in a cloud secrets store.

CI is a human responsibility, not a cloud service.
The idea behind gh-signoff, the tool this pipeline leans on — not something we invented, just a philosophy worth borrowing.

Losing the cloud runner for a day was annoying. But it forced a pipeline that's smaller, more legible, and doesn't depend on a third party's billing state to tell us whether our own code is safe to ship. For a one-person shop, that trade is an easy one to take.