Encoding the release, part 4 of 8.
Automating a release means automating things you cannot take back. A merged PR, a published GitHub Release, an approved deployment that ships to npm — none of these un-happen. The interesting design question was never how to automate them; it was which of them a machine should be allowed to do without looking a human in the eye.
Our answer has three parts.
Every irreversible step pauses. Before the merge, the command states what it is
about to do and waits: merge pr #198? (y/n). Before the GitHub Release:
cut release v0.4.1? (y/n). Before approving the deployment gate:
approve the release gate for run #16? (y/n). The pause is not ceremony — it is the
last moment at which stopping costs nothing. Answer n and the rail stops with a named
reason, leaving a state the next preflight can assess.
--yes exists, but it is not a skeleton key. Run ekohacks release 0.4.1 --yes
and the merge and Release pauses answer themselves — appropriate for the steps that are
pure rail once CI is green. The gate is different. The gate is the moment the package
actually ships to the registry, the step our manual process always reserved for a
maintainer's explicit approval. So the rule is absolute: the gate always asks.--yes does not reach it. Nothing does.
The rule is pinned by a test, not a code review. In the suite there is a test whose whole job is to answer "no" at the gate and then assert that the approvals tracker is empty:
const result = await runShip({ gh, confirm: () => Promise.resolve(false) });
expect(result).toEqual({ stopped: 'gate not approved' });
expect(approvals.data).toEqual([]);
And a second one that runs the entire chain with --yes and asserts the only question
ever asked was the gate's. These are the most valuable tests in the repository, and
neither tests that something works. They test that something refuses to happen. If a
future refactor ever lets an approval sneak past a "no", or lets --yes leak into the
gate, the suite fails before the code reaches main.
Mechanically it is all very boring, which is the point: the confirm is an injectable
function, the policies take it as a parameter, and the chain decides which pauses
--yes may auto-answer. No framework, no prompt library — a function returning a
promise of a boolean, and a design decision about who gets to call it.
The human decision was the first requirement we wrote down for this stage, before any code: the one human decision stays human. Everything else in the series is machinery for deleting toil. This part is the machinery for keeping the judgement.


