021 — Release branch collects stories in dependency order, with CHANGELOG fragments (#92)
Status: shipped (v0.27.0)
Why
Releasing is one story per release: tp release on a story branch opens
the PR and that PR carries the version. Two ready stories mean two
release runs in a fixed order, a conflict on the shared CHANGELOG
[Unreleased], a .tp-release-comment that can name only one branch,
and the version worked out by hand. The same sentence is typed into the
issue, commit, CHANGELOG, comment file and PR title (gobank-deploy #46,
#47). And a plain issue worked on a story branch is never closed by the
release that ships it (gobank-deploy #35, #91), because the release
closes only stories and roadmap items. #91 is folded in here: a
fragment names its issue, and the release closes it.
Vocabulary
| Term | Meaning |
|---|---|
| fragment | changes/<n>.<kind>.md, committed by a story: its CHANGELOG line, for issue #n |
| kind | added, changed, deprecated, removed, fixed, security — the Keep a Changelog section; roadmap a retired roadmap item (#142), rendered - #n text under ### Roadmap, bumping nothing |
after: |
optional front matter in a fragment, after: [46]: this story merges after #46 |
| release branch | release, cut from the base branch; collects the batch's stories; one PR from it to the base |
| batch | the stories listed in .tp-release.yml stories: [46, 47] (gitignored, the releaser's) |
| merged | a story whose remote branch tip is an ancestor of release — read from git, never recorded |
What changes
task-plus.ymlfragments: trueturns the flow on. Without it every path is as today.- Story branch (
story/<n>-*):tp releaserequires a fragmentchanges/<n>.*.md, cutsreleasefrom the base if the remote has none, pushes the story and opens its PR againstrelease(title: the fragment line). No version heading, no CHANGELOG edit, no tag. - Base or
releasebranch:tp releaseruns the batch. It reads.tp-release.yml, orders the stories byafter:(ties by issue number), and for each story not yet merged: merges its remote branch intoreleaselocally (--no-ff), runscheck, pushesrelease. A conflict or failed check stops the run with the story named; fix the story branch and run again — merged stories are skipped by ancestry. - Once every story is merged, the release commit on
release: the fragments gathered under## [x.y.z] - date, grouped by kind into### Addedetc., one line each with(#n), in merge order; the fragment files deleted in the same commit; stories stamped and roadmap items moved as today. Thenreleaseis pushed and its PR to the base opened, and the existing wait / tag run takes over. - Version from the fragments: any kind other than
fixedorsecuritygives a minor bump, else a patch. Overridesrelease_bump; editable in the plan walk as now. - Comment from the fragments: the release comment is their lines
joined with
;. The release PR's title isRelease vX.Y.Z. Withfragments: true,.tp-release-commentis not read. - Issues closed after the tag (#91): every fragment's
#n, with the stories' and roadmap items' issues, de-duplicated, written to.tp-release-inflightas today. A closed or missing issue is a warning. - After the tag the
releasebranch is deleted locally and on the remote, and.tp-release.ymlremoved; the next batch cuts afresh. - A hotfix is a story like any other and ships through
release. internal/fragment(parse, order, gather);docs/release-workflow.mdgains a "Release branch" section; README;.gitignoretemplate line for.tp-release.yml.
Acceptance
Story branch, fragments: true:
Fragment changes/<n>.* |
Remote release |
Story PR into release |
tp release |
|---|---|---|---|
| none | any | any | error: add changes/<n>.<kind>.md; nothing pushed |
two (47.added.md, 47.fixed.md) |
any | any | allowed: one story may add several lines |
| present, unknown kind | any | any | error naming the kinds |
| present | absent | — | release cut from base and pushed; story pushed; PR opened |
| present | present | none | story pushed; PR opened |
| present | present | open | story pushed; PR reported |
Batch run (base or release branch):
.tp-release.yml |
Stories | tp release |
|---|---|---|
| absent | — | error listing the open PRs into release as the suggested stories: |
lists #46, #47; 47 has after: [46] |
none merged | plan shows order 46, 47; merges 46, check, push; 47, check, push; release commit; PR to base |
| as above | 46 merged | skips 46 ("already in release"); merges 47 … |
| as above | 47 conflicts with release |
stops: "merge release into story/47-… and rerun"; release pushed with 46 only |
| as above | check fails after merging 47 |
stops naming 47; local merge undone; release not pushed past 46 |
after: cycle among listed stories |
— | error naming the cycle; nothing merged |
after: [45], #45 neither listed nor merged into base |
— | error: #47 depends on #45; nothing merged |
lists #48, no remote story/48-* |
— | error naming #48; nothing merged |
| all merged, release PR open | — | reported; nothing else |
| all merged, release PR merged, untagged | — | tag run; issues closed; release deleted; .tp-release.yml removed |
Gathering, with merged fragments 46.fixed.md, 47.added.md, 47.fixed.md:
| Latest tag | Bump | CHANGELOG section |
|---|---|---|
| v0.26.0 | v0.27.0 (an added) |
### Added — 47's line; ### Fixed — 46's line, 47's line; each (#n) |
v0.26.0, only fixed fragments |
v0.26.1 | ### Fixed only |
Issues closed after the tag:
| Source | Closed |
|---|---|
fragment changes/n.* |
#n |
story **Status:** ready with an issue |
its issue |
| roadmap item counted done | its issue |
| same number from several sources | once |
| already closed, or not found | warning; the rest still closed |
Out of scope
- Merging story PRs through the forge API; the forge sees the merge by ancestry (Forgejo "autodetect manual merge") or the PR is left for the human to close.
- Deriving the batch from open PRs without
.tp-release.yml. issue:lines in.tp-release-comment(#91's original proposal), superseded by fragments.- Projects without
fragments: true: unchanged, including story 005'srelease/vX.Y.Zbranches. - A major bump from fragments.
Decisions
- ADR 0005: a
releasebranch collects stories; progress is git ancestry, intent is.tp-release.yml, the CHANGELOG is fragments. - The batch merges locally and runs
checkbefore pushingrelease, so a story that breaks the build never reaches the remoterelease. releaseis short-lived: deleted after each tag, so its history never diverges from the base.- Story 005's
release/vX.Y.Zbranches cannot coexist with areleasebranch (git ref namespace); withfragments: truethe batch replaces them, and a leftoverrelease/*branch is an error naming it.
Done when
- tests express the acceptance tables and pass
- spec updated (
docs/release-workflow.md, README) - ADR 0005 written
- changelog entry
- issues #91, #92 closed with a link to the release