0007 — The release branch is opened on request; a lone story merges straight to the base

Status: accepted (amends 0005 and 0006) Date: 2026-10-31 Story: stories/030-a-release-branch-is-opened-on-request-and-a-lone.md

Context

At code level 1 every story went through release: the story run cut it from the base when the remote had none and opened the story's pull request into it; the batch run on the base merged the stories, made the release commits and opened a second pull request. One story was two pull requests and two merges, which 0006 accepted as the price of one process. The release branch earns its place only when there is more than one story to collect, and the implicit cut was where #174's forge-lag trouble came from. Every state the new shape can reach must have a tp release step out of it: nothing may wait on a human closing a pull request by hand.

Options

Where the lone story's release commits live, since the base refuses direct pushes:

Option Complexity Testability Domain clarity Operational risk
A. On the story branch, made by tp release there before its pull request the 0002 path returns for the one-story case, plus a conversion real git in a temp dir; a fake forge for the retarget a story's pull request is the release two stories racing for the base conflict on the CHANGELOG, which the conversion resolves
B. On a release cut by the base run after the story merges none new as today the release commit always reviewed on its own two merges for one story: no gain
C. Pushed to the base by tp through an admin bypass of the protection a bypass per forge needs a forge protection becomes "except tp"; release loses its reason a different ladder

How a lone release's commits are undone when a batch forms:

Option Complexity Testability Domain clarity Operational risk
R. A revert commit pushed on the story branch one git operation real git the history says what happened none: forward-only, every checkout fast-forwards
F. Reset the remote branch with --force-with-lease lease handling; the story's own checkout must detect the rewrite real git clean history a checkout that pushed meanwhile loses the lease
K. Keep them; the batch run folds the untagged entry back into fragments the inverse of gather parsing tests a version that never ships sits in the story's pull request the fold must understand every CHANGELOG shape

How the batch is requested: tp release batch on the base; implicitly by a second story's tp release while a lone release is in flight; and by a .tp-release.yml naming another story, which the walk (story 023) already relies on. A --batch flag on the first story's run would hide a shared decision in one story's run and still need the implicit path.

Decision

A, R, and all three requests. While the remote has no release, a story's tp release makes the release commits on its branch and opens its pull request into the base. A second story's run, or tp release batch, converts a lone release in flight: release is cut from the base, the lone story's release commits are reverted on its branch in one Unprepare vX.Y.Z commit, and its pull request is retargeted. Each step is skipped when its state already holds, so a rerun resumes. A rerun on the lone story with a new fragment redoes its release commits the same way: revert, then prepare again. The version of a lone release is read from the CHANGELOG at the branch's tip, so a branch carrying an untagged version is a prepared release, not a leftover, while its pull request into the base is open and there is no release.

Consequences

Easier:

  • One story is one pull request and one merge; n stories are n + 1.
  • No implicit cut: release exists only when someone asked for a batch.
  • tp release query has no row that says wait.

Harder:

  • A story branch's history records a prepare and, if a batch formed, an unprepare; the base branch's history carries both through the merge.
  • The forge clients grow a call to retarget a pull request.
  • A batch that ends up holding one story ships as a batch, two merges: the long path, not a gap.

What still holds:

  • 0005's release branch, fragments and ancestry as progress, once a batch is open.
  • 0006's single fact: maturity.code picks the process; a lone release is the batch process with no second story.
  • 0003's bounded wait: a lone story's run waits for its merge and walks on to the base to tag.