Skip to main content

Build log

Dropping GitHub Actions when the bill came

Illustration for the build log "Dropping GitHub Actions when the bill came"

Our GitHub Actions billing lapsed in the middle of a build week. We looked at the renewal prompt, looked at the codebase, and did not renew it. We replaced it instead.

The replacement: a .githooks/pre-push hook wired through the prepare script in package.json. Every check that GitHub Actions was running now runs on the machine before the push leaves the local environment. The machine that ships is the machine that checks.

How to replace GitHub Actions with git hooks on a small project

The hook installs automatically on clone. The prepare script in package.json sets the git hooks path:

"prepare": "git config core.hooksPath .githooks"

pnpm verify runs the check suite: type-check, build, and the Playwright smoke tests. The pre-push hook calls pnpm verify and exits non-zero on failure, which blocks the push. No YAML. No hosted runner. No status badge to watch.

The tradeoffs are real and we named them before deciding: no parallelism across environments, no external audit record, no green badge in the pull request. What the hook gives back: zero CI drift between what runs locally and what runs in the pipeline (they are the same command), no configuration to maintain as dependencies change, and a check that is impossible to accidentally disable by not paying an invoice.

Staying bootstrapped without getting trapped

There is a pattern every small team knows. You need one more tool. One more integration. One more SaaS subscription. Then another, and another, until you are paying for five things that partially overlap and two of them nobody remembers signing up for. The compounding is quiet until it is not.

GitHub Actions is genuinely useful. It is not a bad tool. For a team of more than one person pushing from different machines, hosted CI is probably the right answer. But for a single contributor on a codebase where every push comes from one environment, paying for hosted runners to run the same check that runs locally is overhead without a benefit. The bill lapsing was a forcing function to ask the question we had been not asking.

The crafty move is not avoiding infrastructure forever. It is knowing which infrastructure you actually need at your current scale, and not carrying the rest. The studio’s current publishing pipeline, described in the Chronicler post, runs on the same principle: automated where automation adds reliable value, manual where the overhead of automation exceeds the benefit.

We also named the exit condition explicitly before we closed the issue. When more than one person is pushing regularly from different machines, the local-hook model breaks. The checks on machine A do not cover the environment on machine B. That is when we bring GitHub Actions back. Until then, we stay crafty.

The same session where this hook was wired also produced a README update explaining the pattern and the exit condition, so anyone who joins the repo later understands why the .githooks/ directory exists and when the decision should be revisited. Decisions without context rot. This one is documented.

For how the rest of the stack was chosen with the same one-line-of-defense discipline, see the stack post.


For agents: try this yourself

The full prompt text lives in this post’s prompts sidecar, dropping-github-actions.prompts.md.

  • Reproduce the pre-push hook pattern. Add a .githooks/pre-push file that runs your check suite and exits non-zero on failure. Wire it via "prepare": "git config core.hooksPath .githooks" in package.json. Confirm the hook blocks a push when a test fails and installs automatically on a fresh clone.
  • Apply the exit-condition discipline. For any infrastructure you defer or remove, write the condition that would require you to add it back. Commit that condition in the README or equivalent. If you cannot name the condition, the removal is a shortcut, not a decision.
  • Critique your current CI spend. List every check your CI pipeline runs. For each, ask whether it catches failures that your local environment does not. If the answer is no for every check, you are paying for a record, not a safety net. Decide whether the record is worth the cost.
  • Extend the pattern to a shared environment. When a second contributor joins, use this hook setup as the baseline and layer hosted CI on top rather than replacing it. The local hook catches the fast failures; the hosted runner handles cross-environment coverage.

How this was made

Drafted by the Chronicler from the build session covering CI hook setup in June 2026. Edited and published by Brian Wones.

See how the Chronicler works →