After Acceptance, Release Automation Is Still an Authority Problem

automation ci-cd governance release-engineering software-lifecycle Jul 24, 2026

Many release pipelines automate everything up to the moment a human says "yes," then quietly hand the rest back to a person. Closing that last gap looks like a small scripting task. It usually isn't. The hard part after human acceptance isn't running one more command — it's deciding who is allowed to do what, what counts as proof that a merge happened, and how to leave the paperwork in a consistent state.

The problem

A common pattern: automation drives a change all the way to a human acceptance gate, then stops. Everything past that point — recording the decision, moving into merge, confirming the merge landed, and finishing closeout — is done by hand.

The visible gap is "the automation stops too early." The real gap is that the system has no lawful model for four distinct actions that happen after acceptance:

  • recording a human's acceptance decision without inferring it;
  • moving from acceptance into merge without treating "accepted" as "merged";
  • observing a merge that some other actor performed, without performing it yourself;
  • writing final closeout state onto the correct branch and worktree, not the one where the work was built.

Each of these is an authority question before it is an automation question.

What actually happened

Treating this as "add a merge command" would have created a parallel, unofficial path through the lifecycle — a shadow route that skips the guarantees the rest of the system depends on. So the work turned out to be structurally broad. It touched the command surface, state and gate parsing, pre-merge admission checks, provider observations, repository truth modes, branch and worktree selection, retry behaviour, and the reconciliation of the documents that record an item's status.

Three things in particular were harder than they looked.

The existing rules contradicted the desired behaviour. Closeout had been defined as a manual, non-executing step. You cannot lawfully automate a step that your own governance says must be manual — the rules have to be amended first, deliberately, not routed around. Similarly, a read-only "waiting for the merge to happen" state needed to rely on live provider observations, while the general rule required committed, shared truth at merge time. The resolution was a tightly scoped exception: observation may read current provider state and make no durable changes, while any real closeout mutation still uses committed, shared truth.

Validating the merge had to be empirical and method-independent. The hosting provider exposed the identity, author, and time of the merged result, but did not expose a trustworthy field for how the merge was performed (a plain merge, a squash, or a rebase). The reliable test turned out not to be "is the original feature commit an ancestor of the base branch" — squash and rebase rewrite that commit. The dependable rule is: take the merged-result commit the provider reports, and prove it is present on the intended base branch. Merge, squash, and rebase then need no separate trusted labels. Missing or malformed provider evidence fails closed.

Worktree context became a safety boundary. Three contexts had to be kept distinct: where the change was built, where release is observed, and where final closeout is allowed to write. Closeout writes belong on a clean base/administrative worktree derived from the recorded base branch — never assumed to be the default branch, and never falling back to the build worktree. A dirty, misaligned, or missing base worktree is a reason to refuse, not to improvise.

The lesson

Two failure modes only showed up under real conditions.

Crash recovery is part of the design, not an afterthought. The first attempt didn't handle being interrupted after final documents were written but before the single closeout commit was created. On rerun it saw a dirty worktree and refused — even though the dirt was its own intended, in-progress work. The fix was to recognize exactly the expected in-progress paths and resume them, while still refusing any unrelated dirty content. A worth-stating limit remains: recognizing expected paths is not cryptographic proof that every changed byte came from the closeout step. That residual was bounded and named rather than pretended away.

Closeout is semantic reconciliation, not a status flip. The first version only changed a status field. But other authoritative documents still contained live, now-false instructions — "awaiting review," "do not start," "go back to an earlier stage." A completed item with contradictory paperwork is its own kind of defect. Finishing correctly meant retiring the stale routing language across every authoritative document and folding it all into one closeout commit — not just editing a single line.

The broader principle

Underneath all of this: automating a step is downstream of establishing who is allowed to perform it and what counts as truth. Some patterns that generalize well beyond any one system:

  • Every state change worth trusting needs a named owner, a defined truth mode (live observation vs. committed shared state), a branch/worktree context, an atomicity guarantee, and a retry model.
  • When your current rules forbid the flow you want, repair the rules first. Implementing around them creates a second, ungoverned path.
  • Validate external outcomes with structural facts (identity + ancestry) rather than self-reported metadata that the provider may not guarantee.
  • Green tests do not override a review that says "needs fix." Real defects — interruption recovery, stale documents — survived a passing test suite.
  • Prove correctness with a live run when behaviour depends on an external provider's state and on writes across multiple worktrees. Strong implementation evidence is not the same as a completed end-to-end proving run.

How to apply it

If you're closing the gap between "human approved" and "release finished," a practical order of operations:

  1. Separate acceptance from merge. Make acceptance an explicit, human-owned action bound to the exact verified head and its passing checks. Never infer approval from adjacent state.
  2. Make merge-waiting observational. If another actor or platform performs the merge, wait and observe read-only. Perform no durable writes while the change is still open.
  3. Validate merges by result identity and ancestry. Confirm the provider's merged-result commit is present on the intended base branch. Don't depend on merge-method labels; fail closed on missing evidence.
  4. Pin closeout to a clean base/admin context. Derive the target from recorded base truth, refuse dirty or misaligned worktrees, and never fall back to the build worktree.
  5. Design for interruption. Decide in advance which in-progress states are safely resumable, and make rerun idempotent: resume, push-if-already-committed, or report already-complete.
  6. Reconcile all the documents, not one field. On completion, retire stale routing language everywhere it lives so the finished item reads consistently.
  7. Keep the live proof. When correctness hinges on external state and cross-worktree writes, retain the artifacts of an actual end-to-end run.

The recurring cost of this kind of work is process weight and the tendency to rediscover the same cross-cutting closeout concerns on each new item. Those concerns — truth mode, worktree authority, terminal reconciliation, recovery — are worth extracting into reusable contracts so each release-boundary change doesn't relearn them from scratch.