Trevor PaughSystems engineer
Trevor PaughProject Chronicles

Chronicles / Bopsmith

Paying for CI once

Decision

The project's hosted build allowance ran out on 15 October, which turned continuous integration from a habit into a cost. Three slices followed. The server test suite boots its in-memory database once per worker instead of once per test, and runs about fifteen times faster. Pushes that touch only the manager's notes start no run at all, which had been four out of five runs. And a merge to main whose tree is identical to a pull request that already passed inherits that result instead of running again, with every doubt resolved in favour of running.

The pull request for slice 39 sat ready on 17 October with its review done and its fixes verified, and no test run would start: the hosted build service had stopped accepting jobs for the account. The owner’s instruction was to wait rather than override, and once they had restored the allowance the next day the run went green and the merge went through. The three slices around that wait are all about making each run cheaper or making it not happen.

One database per worker

The server tests had been booting a fresh in-memory database and running every migration for almost every test, around 350 times a run. The cost was not the migrations but the database’s own start-up. Slice 39 keeps one pool of instances per test worker, truncates every table between tests, and lets a worker reuse its module graph across files. Six boots per run at three workers. On the manager’s machine the suite went from 83 seconds to 16; on the build service from roughly nine minutes to under two. The trade accepted and written down: tests in one worker now share module state, so a test that changes the environment or fakes timers must put them back.

Runs that should never start

Looking at the history, 126 of the 159 runs on main since 14 October had been pushes of the manager’s own files: briefs, logs, handoffs. Slice 40 tells the workflow to start no run for changes that touch only those paths and the documents nothing reads. The legal documents and the deploy runbook stay in, because the server, the image, and the tests read them. A test pins the list so a new document under docs is tested by default until someone decides otherwise. Main is not branch-protected, so no placeholder workflow is needed; a comment in the workflow says what to change if that ever turns on.

Inheriting a green result

Every pull request merge still ran the full suite twice: once on the request, once on main after the merge. Slice 41 adds a gate job to pushes on main. It skips the checks only when the push is a merge commit whose tree equals its second parent’s, when that parent already contains main’s previous tip, and when every run of that parent completed successfully, with at least one run and all of them read. Any error, any missing run, any doubt, and the checks run. The reasoning the manager recorded: the request’s run tested the head merged onto an ancestor of main’s previous tip, which is exactly the merge’s tree, so running again measures nothing new. The precondition, which the plan cannot enforce, is that main is never force-pushed backwards.

The first real merge after it landed ran the gate in six seconds and inherited the request’s result. Per merge, four billed minutes became about one.