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.yml fragments: true turns the flow on. Without it every path is as today.
  • Story branch (story/<n>-*): tp release requires a fragment changes/<n>.*.md, cuts release from the base if the remote has none, pushes the story and opens its PR against release (title: the fragment line). No version heading, no CHANGELOG edit, no tag.
  • Base or release branch: tp release runs the batch. It reads .tp-release.yml, orders the stories by after: (ties by issue number), and for each story not yet merged: merges its remote branch into release locally (--no-ff), runs check, pushes release. 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 ### Added etc., one line each with (#n), in merge order; the fragment files deleted in the same commit; stories stamped and roadmap items moved as today. Then release is pushed and its PR to the base opened, and the existing wait / tag run takes over.
  • Version from the fragments: any kind other than fixed or security gives a minor bump, else a patch. Overrides release_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 is Release vX.Y.Z. With fragments: true, .tp-release-comment is 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-inflight as today. A closed or missing issue is a warning.
  • After the tag the release branch is deleted locally and on the remote, and .tp-release.yml removed; 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.md gains a "Release branch" section; README; .gitignore template 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's release/vX.Y.Z branches.
  • A major bump from fragments.

Decisions

  • ADR 0005: a release branch collects stories; progress is git ancestry, intent is .tp-release.yml, the CHANGELOG is fragments.
  • The batch merges locally and runs check before pushing release, so a story that breaks the build never reaches the remote release.
  • release is short-lived: deleted after each tag, so its history never diverges from the base.
  • Story 005's release/vX.Y.Z branches cannot coexist with a release branch (git ref namespace); with fragments: true the batch replaces them, and a leftover release/* 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