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:
releaseexists only when someone asked for a batch. tp release queryhas 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.codepicks 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.