feat: implement release-branch workflow#1076
Conversation
Replaces the cut-from-main release flow with long-lived release/vX.Y branches carrying rcs and finals. main carries X.Y.0.devN for the next minor. Patches cherry-pick onto the existing release branch. Adds four workflow_dispatch workflows: cut-release-branch, publish-release (was cd.yml), cherry-pick-to-release, publish-dev-from-main. Adds bump_version.py with five PEP 440 transition modes plus unit tests. Prerelease publishing to PyPI is gated on PUBLISH_PRERELEASES (default false). Auth migrates from the mellea-auto-release GitHub App to GITHUB_TOKEN with inline permissions blocks. See RELEASE.md for the full operator-facing flow. Assisted-by: Claude Code Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
|
Stepping back to the original problem we had on the last release, where we had to basically pause new PR merges for a few days while the release was prepared, I think the main thing we want from a release branch is to let contributors keep merging to main while a release is being stabilized. With that in mind, I want to push back a bit on the scope of this PR. I think most of the machinery here is solving an adjacent problem (formal PEP 440 rc/dev/patch lifecycle) rather than the merges-during-stabilization problem we ran into last time. I think we could actually solve this with a simpler flow:
I'd suggest splitting this in two: one PR for the release branch and a separate issue/PR for the PEP 440 flow. I think that would allow for more discussion but also let us close on the merge-during-release problem quicker. |
| ) | ||
|
|
||
| if mode == "rc": | ||
| if current.pre is None or current.pre[0] != "rc": |
There was a problem hiding this comment.
Suggestion: when patch > 0, both mode=rc and mode=patch-rc produce the same output (X.Y.Zrc(N+1)). mode=rc quietly succeeds on patch rcs by accident. RELEASE.md's operator table only documents patch-rc for the patch cycle, so someone running rc by muscle memory gets the right answer with no signal they've used the wrong mode.
Consider explicitly rejecting mode=rc when patch != 0:
if patch != 0:
raise ValueError(
f"mode=rc is for minor rcs (X.Y.0); got {current}. "
"Use mode=patch-rc to iterate a patch rc."
)There was a problem hiding this comment.
The overlap is intentional, I'll let Claude explain:
The two modes have different jobs:
mode=rcis the rc iterator — requires an existing rc and bumps the rc number. Works the same way regardless of whether it's a minor rc (0.6.0rc1→0.6.0rc2) or a patch rc (0.6.1rc1→0.6.1rc2).mode=patch-rcis the transition mode for going from a final to the first rc of a patch cycle (0.6.0→0.6.1rc0). It can also iterate patch rcs after that, but its primary purpose is the transition.
The error messages reflect that split: mode=rc rejects non-rcs ("If this is a final, use mode=patch-rc to start a patch cycle"), and mode=patch-rc rejects minor rcs ("Use mode=rc to iterate minor rcs"). An operator running the wrong mode for the wrong context still gets a hard error — the only soft case is "running rc against a patch rc that already exists," which is also rc's job.
Rejecting rc when patch != 0 would break the design: there'd be no single mode that just iterates an rc, and operators would have to mentally branch on whether they're in a minor or patch cycle every time they bump. I'd rather keep rc as the universal iterator.
| # Pull the generated notes back locally to update the changelog. | ||
| REL_NOTES=$(mktemp) | ||
| gh release view "${TARGET_TAG_NAME}" --json body -q ".body" >> "${REL_NOTES}" |
There was a problem hiding this comment.
We should have a discussion of what we want the release notes to encompass. If we cut release tags for rc1, devN, etc... I think these autogenerated release notes will be problematic.
Github auto-builds these notes with using PRs merged since the last release. I do not know if it accounts for standard semver things. If not, we will either have to do this ourselves or change our release tagging process documented here.
There was a problem hiding this comment.
This will be affected by the choices outlined in my refactor ideas below, but if we keep pre-release tags then I would just need to update the changelog workflow to compare to the last final release rather than the most recent one, a trivial change
| options: | ||
| - rc | ||
| - final | ||
| - patch-rc | ||
| - patch-final | ||
| - none |
There was a problem hiding this comment.
Why would we want to tag a rc0, etc... release (really anything but the final release for a v0.X.Y)?
There was a problem hiding this comment.
These are more of a future proofing choice for supporting pre-releases on pypi when/if we decide to add those by enabling PUBLISH_PRERELEASES=true
|
I addressed @planetf1 technical review with a new commit and a response, as for @psschwei and @jakelorocco your review is the type of feedback that I wanted to drive the discussion at sync on Monday, in fact I had meant to open this as draft to make it clear this was just a proposal. In writing this I focused on making it full-featured so we could remove undesired features afterwards rather than needing to rework new functionally in, so I'm open to removing things to streamline it. As such I'll have Claude detail out some refactoring ideas for discussion based on your comments and my opinions, we can then dig into these ideas both here and on Monday's call: Refactor ideas for MondayFour points to discuss. Items 1 and 2 are alternatives — pick at most one. Item 3 is orthogonal to both. Item 4 is the "split into multiple PRs" question. My current lean on each is in italics. 1. Drop speculative prerelease tagging. Currently we create git tags for every rc and dev (
2. Drop prereleases entirely. A more aggressive trim that supersedes #1: stabilization happens in-place on the release branch with no rc cycle, no 3. Drop cherry-pick, switch to PRs targeting the release branch. This is Paul's proposal and is orthogonal to #1 and #2. The current PR's flow is "every change lands on main first; maintainers run a cherry-pick workflow to port selected commits onto the release branch." Paul's alternative: contributors open stabilization PRs directly against What gets removed if we switch: Tradeoffs:
I'd lean toward keeping cherry-pick. "Main is the source of truth" matches what most contributors already do, and the cherry-pick machinery is written and validated. The simplification Paul gets is real but not large. 4. Split into two PRs. @psschwei suggested splitting release-branch + cherry-pick into PR1 and the PEP 440 lifecycle into PR2. I'd push back on this. If we decide to keep prerelease versioning, it should land integrated, not split — and if we decide to remove it (per #2 above), there's nothing to split. Splitting creates an awkward intermediate state where the project has half a release model. For illustration, the cleanest seam would be:
The seam isn't clean — both PRs edit The smaller-review-surface benefit Paul wants from a split, we can also get by trimming features inside this PR after Monday's discussion — without the integration churn. |
Misc PR
Type of PR
Description
Replaces the cut-from-main release flow with a release-branch model. Every minor release gets a long-lived
release/vX.Ybranch carrying rcs and the final;maincarriesX.Y.0.devNfor the next minor. Patches cherry-pick onto the existing release branch and go through their own rc cycle. SeeRELEASE.mdfor the full operator-facing documentation.Adds four
workflow_dispatchworkflows (cut-release-branch,publish-release(renamed fromcd.yml),cherry-pick-to-release,publish-dev-from-main), abump_version.pyhelper with five PEP 440 transition modes, and aPUBLISH_PRERELEASESrepo variable that gates PyPI uploads for rc/dev versions. Auth migrates from themellea-auto-releaseGitHub App toGITHUB_TOKENwith inlinepermissions:blocks.Admin actions required after merge are listed at the bottom of
RELEASE.md.Testing
End-to-end dry-run validated on
ajbozarth/melleafork: cut-release, cherry-pick, publish-dev-from-main, rc, final, and the explicit downstream-workflow dispatches (pypi.yml,docs-publish.yml,ci.yml) that work around GitHub's anti-loop rule forGITHUB_TOKEN-authored events.Attribution