005 — tp release through a pull request when main is protected (#26)
Status: shipped (v0.4.0)
Why
tp release makes one commit on main (the rolled changelog, README
markers, the WASM build where configured), tags it and pushes. A project at
code-flow level 1 of the maturity ladder
protects main against direct pushes, so the push is refused and the
release rolls back. gobank hit this on its first protected release
(2026-10-03). Whitelisting the releasing account for pushes would get the
release through, but it makes the release the one commit on main nobody
reviews, and the project wants tp release to stay the single way to ship
while a review that has been switched on still happens.
What changes
tp releasedecides between two modes in Gather: direct (today's flow) whenmainon the release remote is not protected, pull request when it is (Forgejo API: the branch'sprotectedflag) or whentask-plus.ymldeclaresmaturity.codeof 1 or more.- Pull request mode, first run: Execute steps 1 to 6 (dirty-tree commit,
tag check, version update, CHANGELOG commit, WASM build) happen on a new
branch
release/vX.Y.Zcut frommain; no tag is made. The branch is pushed and a pull request "Release vX.Y.Z" is opened againstmain, its body the version's CHANGELOG section and the release comment. The run prints the PR URL and stops with exit 0: merge it, then runtp releaseagain. - Pull request mode, second run:
tp releaseonmainlooks for the release PR by head branch. Merged: fetch, fast-forwardmain, confirm the CHANGELOG's top heading is the version and no tag exists, tagmainHEAD, then steps 7 to 13 as today (push tag, mirrors, goreleaser, cleanup, install, docs deploy, post_release) and delete the release branch if the forge has not. Still open: print the URL, stop. Closed without merging: error. - Rollback: before the PR is opened, delete the release branch locally and
remotely. After it is opened nothing on
mainhas changed, so there is nothing to roll back; the PR is the human's to merge or close. - Only the tag is ever pushed directly; Forgejo branch protection does not
cover tags. A tag protection rule on
v*, if the project adds one, is outside this story. docs/release-workflow.mdgains a "Pull request mode" section with the two-run sequence; README command table unchanged.
Acceptance
main protected or maturity.code ≥ 1 |
release PR for the version | tp release outcome |
|---|---|---|
| no | — | today's direct flow, unchanged |
| yes | none | release/vX.Y.Z created with the release commit, pushed, PR opened; URL printed; exit 0; no tag; main untouched |
| yes | open | URL printed; exit 0; nothing changed |
| yes | merged, tag absent | main fast-forwarded, tagged at HEAD, tag pushed, steps 7–13 run, release branch deleted |
| yes | merged, tag present | error "vX.Y.Z already released"; nothing changed |
| yes | closed without merge | error naming the PR; suggests deleting the branch to start again |
| yes, release remote is not Forgejo | — | error "pull request mode needs a Forgejo release remote"; nothing changed |
Resuming after a failure in pull request mode:
| Failure point | State left | Next tp release |
|---|---|---|
| before the branch is pushed | nothing remote; local branch deleted | starts afresh |
| branch pushed, PR not opened | remote branch exists | opens the PR for the existing branch |
| after tagging, during steps 8–13 | tag on main |
as today: those steps are retried by hand or the release is complete as far as the tag |
Out of scope
- Merging the PR itself. Self-merge would make the review a formality; the human merges in the forge.
- Waiting for CI on the PR (level 2); the second run simply finds the PR merged or not.
- GitHub or GitLab release remotes in pull request mode.
- Tag protection rules.
- The level 2 "tag-only" release shape, where the version heading lands in the story's own PR and a release is only a tag. Recorded as a later option on the roadmap.
Decisions
- ADR 0001: two runs rather than one run that polls for the merge: a release can wait hours for a reviewer, and a process that must stay alive across that is fragile; the branch and the PR are the whole state, so the second run needs nothing local.
- The second run finds its work on the forge, not from a chosen version: a merged release PR whose version has no tag. So "merged, tag present" is not an error at the start of a run (that is just the next release); it is an error only when that version is chosen again.
- Mode is detected from the forge, not only from
maturity.code, so a protectedmainis respected even by a project that has not declared its level.
Done when
- tests express the acceptance table and pass
- spec updated (
docs/release-workflow.md) - ADR written if a fork was resolved (ADR 0001)
- roadmap ticked and stamped; changelog entry
- issue closed with a link to the release (v0.4.0)