Skip to content
Back to Ideas 5 min read

The First Real Run Said No

The First Real Run Said No

Encoding the release, part 6 of 8.

The first time ekohacks release ran against a real repository, for real, it produced this:

ekohacks release 0.4.1
  FAIL changelog entry: no ## 0.4.1 heading in the changelog
  ok version is new
  ok on main
  ok clean working tree
  ok lockfile registry
  ok package smoke
stopped: preflight failed: changelog entry

We count this as the tool's first success, and we mean that without irony. Everything was in place — clean main, unpublished version, healthy lockfile, passing smoke — except the one input our process deliberately keeps human: the changelog entry, the piece of writing that becomes the PR body and the Release notes. The rail refused to start, named the missing piece, and touched nothing. A release tool earns trust by refusing well before it earns trust by shipping.

Then, while writing that entry, a question came up that sounded almost naive:

"So… do we deploy the local main, or origin/main?"

It is the sharpest question anyone had asked about the tool. The answer: what ships is always GitHub's main — the merge happens there, the Release is tagged there, the publish workflow runs there. But the rail builds on the local main: the release branch, the version bump and the PR all start from whatever commit it points at. The entire flow rests on an unstated promise that the two are the same commit.

And nothing checked that promise. Start from a stale local main — a teammate's merged work never pulled — and one of two things happens. The loud one: the release PR conflicts with origin's main, package.json being the classic battlefield, and the rail dies half-done. The quiet one is the frightening one: nothing textually conflicts, the PR merges green, and the published package silently omits work that was already on main. Users install a version that does not contain what everyone believes main contains, and no step of the process ever says a word.

Closing the gap followed the rule from part 1, in order. First the document: RELEASING.md gained a new first line in its "Before cutting" checklist — main matches origin/main, git pull before anything else — merged as its own PR. Then the tool mirrored it, red-first, in five commits: the nulled git learned to answer a configured sync state, an integration test pinned the real behaviour against a bare origin, and preflight grew its seventh check:

  ok on main
  ok in sync with origin
  ok clean working tree

The pattern to keep is not the specific check. It is that the checklist grew the way checklists should: one real question, one real near-miss, one line in the document, one test-first check in the tool. Preflight is not a list of clever ideas. It is a list of afternoons.

Next: What One Real Release Taught Us in an Afternoon.

More from Ideas

·8 min read

Inversion of Control Doesn't Need a DI Container

We started designing a dependency scanner, and its needs led us to Inversion of Control. It turned out we were already doing it, without a DI container.

A
Anastasios Orfanidis
·7 min read

Unknown Is an Answer

A stub that holds no documents could not say whether a change still matched a query. Letting it answer unknown named a case the real database has too.

·7 min read

The Inbox That Delivers Nowhere

Signing in by emailed code puts a mailbox on the critical path. How we use Mailpit on Studomia, how our tests read the code, and what one question taught us.