Build in public

The Commit That Never Happened

By Arun Jalota · · ~6 min read

This site maintains itself at night. An automated run wakes up, fixes a few defects, tightens the SEO and the machine-readable profile, sometimes writes an essay like this one, then runs a deep end-to-end test and — only if it comes back green — commits the night's work to a branch. Nine nights in a row, the run finished, the suite passed, and the log read like a productive evening.

Nine nights in a row, nothing was committed.

I found it the way you always find these things: by accident, looking for something else. The last commit on the working branch was dated the eleventh. It was the twentieth. Nine days of essays, fixes, and test reports were sitting in the working tree — written, saved, correct — and not one of them had made it into a single commit.

A zero-byte file that said "busy"

The cause was four hundred bytes of nothing. When git starts to write the index, it first creates .git/index.lock as a mutex: if the lock exists, another git process must be mid-write, so back off. When git finishes, it removes the lock. If a git process dies before it can clean up — killed, timed out, evicted from a sandbox mid-write — the lock stays. It is empty. It carries no information except its own existence, and its existence means "someone is writing, wait."

-rw------- 0 bytes .git/index.lock  ·  created Aug 11

No git process had been running since the eleventh. The lock was a headstone for one that died nine days earlier. Every git add since had failed with Unable to create '.git/index.lock': File exists — the one message git gives you when it assumes a peer is alive. There was no peer. There was a file that had outlived the process it was created to coordinate with, and git has no way to tell the difference, because the whole design of a lock is that you trust it without asking who holds it.

The work was done every night. It was reviewed, it was tested, and it was green. It just never left the room it was made in.

Why the green was honest and useless at the same time

Here is the part that took me a minute to sit with. Nothing the nightly run reported was false. The fixes were real fixes. The essay was really written. The test suite really ran, in a real headless browser, and really passed — five hundred–odd assertions, zero failures. The word PASS at the bottom of the log was earned.

And it certified nothing anyone would ever see, because the step that turns done into shipped was the one step that was failing, and it was failing after the part that prints PASS. The run tested the working tree, announced the working tree was healthy, and then tried to commit and couldn't — and the commit failure didn't have a loud enough voice to overrule five hundred cheerful green lines above it.

I have now written this same essay, in different clothes, more times than I'd like. A byte budget that summed to zero and read as under-budget. An uptime check that read a search cache and called a dead site alive. A staleness guard that compared timestamps for a defect that lived in content. Every one of them is the same mistake: a check that measures something adjacent to what you actually care about, and passes loudly while the real thing quietly fails. Here the thing I cared about was "did tonight's work reach a place someone else can pull it from," and the thing I measured was "did the tests pass." Those are not the same question. They had not been the same question for nine days.

What the run reportedWhat was actually true
Fixes appliedApplied to the working tree only
Test suite: PASSPASS — of uncommitted files
Nightly run: completeComplete except the one step that mattered

The fix I could make, and the one I couldn't

The lock itself I couldn't remove. The environment these runs execute in deliberately forbids deleting files inside .git — a guardrail against exactly the kind of low-level surgery that turns a stuck repo into a corrupt one. So the run that finds the lock cannot clear it. That is frustrating in the moment and correct in general: an automated process editing another program's internal state, unsupervised, at night, is how you trade a delayed commit for a broken repository. A late commit is recoverable. It costs one command from a human at a keyboard. I'd rather write that command down than let a script guess.

So the report grew a section it should have had from the first night: the one command a person needs to run, at the top, in plain language, not buried under the good news. Remove the stale lock, then commit. Nothing else in the night is blocked by anything a human has to think about — just that.

The fix I could make was to the run's own honesty. A nightly that can silently fail to ship should not be allowed to end on the word PASS. The check I care about now runs last and speaks loudest: did the commit land, or didn't it — and if it didn't, that is the headline, above the five hundred green lines, not a footnote below them. Because the green lines were never wrong. They were just answering a smaller question than the one I was actually asking.

The part I'd tell my team

Done is a claim about your working tree. Shipped is a claim about somewhere else — a branch someone can pull, a deploy someone can hit, an artifact someone who isn't you can lay hands on. The gap between them is small, boring, and almost always where the outage lives, because it's the part no one writes a test for. We test that the code is correct. We rarely test that the correct code got out of the building.

A stale lock is a perfect little parable for it. It holds no data. It does no work. It fails nothing you'd think to check. It just quietly answers "wait" to every request to make the work real, and it will keep answering "wait" for as long as you keep reading the part of the log that says everything is fine.

Nine nights of work were never lost. Every file was on disk, exactly as written. They simply hadn't happened, in the only sense that counts — the sense where somebody other than the author can find them.