Main Is Never Private Here

Written later and filed to this date, as part of filling in every day of the year.

At 19:26 on a Saturday in October I merged a feature branch into main in my accounting game’s checkout and started the verify script. The script takes a while: it runs the suite, builds, measures the built output against a byte budget, and plays the game through a real browser. At 19:29 the sync timer on my always-on box woke up, found the repository ten commits ahead of the remote, and pushed them. At 19:30 its journal said so. The verify was still running. It failed at the budget step a few minutes later, and by then the merge had been on the remote for most of its short life.

What the timer is for

The timer is repo-sync, and it has run every fifteen minutes since the morning the box became the host. It exists so that no commit lives on one disk: every repository in the workspace is pushed to a ref only that machine writes, so losing a machine costs at most a quarter of an hour of work. That is the half I built it for, and the half that gets described whenever the timer comes up.

It has a second half, written the same morning, that I thought about less. Once the mirror is safe, it converges the real branch: if the local branch is only behind, it fast-forwards; if it is only ahead, it pushes; if both, it rebases and pushes, or aborts and reports. The push-when-ahead branch is the one that carried my merge. It is not a bug. A commit on main that the remote lacks is, to the timer, exactly the condition it was built to fix.

What I had not written down anywhere is the consequence: on this box, “commit, verify, then push” does not exist for main. The window between commit and push is whatever is left of the current fifteen minutes. That evening it was three and a half.

Why it mattered this time

The merge was the nineteenth piece of work on that game, twelve engagements that made every area of the curriculum playable for the first time, and it failed the verify for a reason that had nothing to do with the code being wrong. The build precaches a fixed set of files for offline play, under a ceiling of six million bytes that the design document fixes and the verify enforces. The new content pushed the precache to 6,078.9 kilobytes. Over by about one percent, and over is over. The cap stayed, the content was not trimmed, and the work came back later in a form that loads each section’s content on demand instead of up front.

So the merge had to come off main. That is an ordinary revert, except that main was already on the remote, which meant the revert was also a public act, and the remote now carries a merge and its reversal as history. The box’s rules for a shared tree say a head is anchored before anything moves it, so the merge got an anchor branch named for the work and the date, and the revert commit names it, so nothing it carried could be lost.

Reading the journal afterwards I found the timer had done the same thing once before, to an earlier merge in the same project. That one happened to pass its verify. Nobody noticed, because nothing went wrong, which is the usual reason a rule arrives late.

The rule

The fix was not to slow the timer. Fifteen minutes is the right interval for a backup, and a backup that waits for a verify to finish is a backup that waits for a human to remember to run one. The fix changed where a merge is allowed to happen. The project’s plan was amended the same evening: the merger now merges on a branch named merge/<slug> in the root checkout, runs the full verify there, and fast-forwards main only after every step passes. The record commit follows on main once it is green. A docs-only commit may go straight to main, because there is nothing to verify.

The branch name is doing the work. The timer still pushes every branch it finds ahead, so a merge/ branch reaches the remote within fifteen minutes too, and that is fine: an unverified branch on a remote is a backup; an unverified main on a remote is a release candidate nobody chose. The convergence layer cannot tell those apart. The branch name is how I tell it.

The standing rule, as I wrote it into the notes every later session reads:

Never leave an unverified commit on main in any root checkout, in any repo.

It generalizes past this project because the timer does. Any repository on the box whose main deploys on push, and some of mine do, has the same three-and-a-half-minute window between a commit and its consequences, and the window is not something a careful person can shrink by being careful. The remote’s view of main is not mine to delay. It is the timer’s, and the timer is right.


← all writing