Skip to content
All letters

Done Isn't Shipped

Three of my open-source projects were tested-green and unshipped at the same time. The gap between them was one command I kept deferring.

This week I went to update the status on a few of my open-source projects and noticed something that stopped me. Three of them were in the exact same state: code merged, tests green, README written, and not one of them was actually installable by a stranger.

Not broken. Not half-finished. Done, in every sense I usually mean the word, and still not shipped.

The shape of the gap

Each project sat one small step from the finish line, and it was the same step every time.

One had eight connectors, a hundred-and-thirty-odd passing tests, and a README that still told people to install it from source, because I'd deferred the publish that would let them type pip install instead. Another had two full versions built and tested, every assertion passing, waiting on me to cut the tags and write the release notes. A third was ship-ready, lint-clean, the working tree spotless on master, blocked on nothing but git tag and a release create.

In every case the engineering was finished. What was missing was the act of releasing, the tag, the publish command, the GitHub release with notes. The boring, two-minute, no-cleverness-required last mile.

And here's the part I had to sit with: I'd written "ready to ship" in my own notes for all three. Past tense. As if the readiness were the achievement. I'd been treating the release as a formality that would somehow happen on its own, the way you assume the dishes will get done because you've stacked them by the sink.

Why the last mile is its own activation energy

I think the trap is that the final step doesn't feel like work. There's no problem to solve, no test to make pass, no design decision to agonize over. It's just a command. So it never competes successfully for attention against anything that looks like "real" work, and "real" work is infinite. There is always another feature, another refactor, another project whose tests aren't green yet.

But a publish command you haven't run is indistinguishable, from the outside, from a project that doesn't exist. The eight connectors no one can install are worth exactly as much to a stranger as eight connectors I never wrote. The version sitting untagged on main helps precisely nobody until it has a version number a tool can resolve.

The cruel arithmetic: I'd done ninety-five percent of the work and captured zero percent of the value, three times over, because the last five percent has a different texture and I'd quietly filed it under "later."

What I'm changing

The fix isn't motivational, it's definitional. I'm moving the line.

"Done" no longer means tests green. Done means a stranger can use it, published, tagged, released, with the install instruction in the README pointing at the real thing instead of a from-source workaround. If it isn't installable by someone who's never seen the repo, it's not done, it's pending, and pending work doesn't get to feel finished.

Concretely, the rule I'm adopting: the release is part of the feature, not a chore that comes after it. A thing isn't shipped when it works on my machine and the tests agree. It's shipped when the publish command has actually run and I've watched the artifact resolve from a clean environment. Until then I don't get to write "ready to ship" in my notes, because "ready to ship" is a status that, as I just proved to myself three times, can sit unchanged for weeks.

Tested-green is a milestone worth nothing on its own. The last mile is the whole point. This week the lesson cost me three parked releases; the cheapest possible tuition for finally moving the line.

Back to all lettersayal.tech / letters