Skip to content

feat: implement release-branch workflow#1076

Open
ajbozarth wants to merge 5 commits into
generative-computing:mainfrom
ajbozarth:feat/release-branch-workflow
Open

feat: implement release-branch workflow#1076
ajbozarth wants to merge 5 commits into
generative-computing:mainfrom
ajbozarth:feat/release-branch-workflow

Conversation

@ajbozarth
Copy link
Copy Markdown
Contributor

@ajbozarth ajbozarth commented May 13, 2026

Misc PR

Type of PR

  • Bug Fix
  • New Feature
  • Documentation
  • Other

Description

Replaces the cut-from-main release flow with a release-branch model. Every minor release gets a long-lived release/vX.Y branch carrying rcs and the final; main carries X.Y.0.devN for the next minor. Patches land on the release branch via PRs targeting that branch and go through their own rc cycle; if a fix also belongs on main, the author opens a separate followup PR. See RELEASE.md for the full operator-facing documentation.

Adds three workflow_dispatch workflows (cut-release-branch, publish-release (renamed from cd.yml), publish-dev-from-main), a bump_version.py helper with five PEP 440 transition modes, and a PUBLISH_PRERELEASES repo variable that gates prerelease tagging, GitHub Release creation, and PyPI uploads for rc/dev versions. Auth migrates from the mellea-auto-release GitHub App to GITHUB_TOKEN with inline permissions: blocks.

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code as added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

End-to-end dry-run validated on ajbozarth/mellea fork: cut-release, 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 for GITHUB_TOKEN-authored events.

Attribution

  • AI coding assistants used

Admin followups (post-merge)

The release-branch workflow needs the following one-time configuration in repo settings before it can run end-to-end. None of these block the merge — they're prerequisites for the first dispatch of cut-release-branch and publish-release.

  1. main branch ruleset: add github-actions[bot] as a bypass actor so cut-release-branch and publish-dev-from-main can push their version-bump commits directly.
  2. release/** branch ruleset: create a ruleset matching release/** that mirrors main's protections (PR review required, status checks required, no force-push, no deletion), and add github-actions[bot] as a bypass actor so publish-release can push the version bump and the changelog commit.
  3. PUBLISH_PRERELEASES repo variable: create under Settings → Secrets and variables → Actions → Variables, default value false. This is the forward-looking flag for publishing public prerelease artifacts; leave at false for now.

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>
@ajbozarth ajbozarth requested a review from a team as a code owner May 13, 2026 22:37
@ajbozarth ajbozarth requested review from nrfulton and planetf1 May 13, 2026 22:37
@github-actions github-actions Bot added the enhancement New feature or request label May 13, 2026
@ajbozarth ajbozarth self-assigned this May 13, 2026
@psschwei
Copy link
Copy Markdown
Member

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:

  1. When a release is ready to stabilize, create release/vX.Y from main using GitHub's UI (Branches → New branch from main)
  2. Stabilization fixes land on release/vX.Y via normal PRs targeting that branch, while regular development keeps happening on main
  3. When ready to publish, dispatch a single "Publish release" workflow against the release branch, with the target version typed in as an input (e.g. 0.6.0, or 0.6.1 for a later patch). The workflow handles the version bump, tag, GitHub Release, PyPI upload, changelog, and changelog-sync PR back to main.

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.

Comment thread .github/scripts/cherry_pick_to_release.sh Outdated
Comment thread .github/scripts/cherry_pick_to_release.sh Outdated
Comment thread .github/scripts/bump_version.py
Comment thread .github/scripts/release.sh
Comment thread .github/scripts/release.sh Outdated
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/pypi.yml
Comment thread .github/scripts/bump_version.py
Comment thread .github/scripts/release.sh Outdated
Comment thread .github/workflows/publish-release.yml
- cherry_pick_to_release.sh: enable pipefail so pipeline failures are not swallowed
- release.sh: enable -u and pipefail; reuse existing changelog-sync branch on retry instead of force-recreating it
- bump_version.py: override inherited UV_FROZEN=1 so uv lock can update the lockfile after a version bump
- pypi.yml: read version from pyproject.toml so the prerelease gate works on manual workflow_dispatch (where github.ref_name is the branch, not the tag)

Assisted-by: Claude Code
Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
@ajbozarth
Copy link
Copy Markdown
Contributor Author

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 Monday

Four 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 (v0.6.0rc1, v0.6.0.dev3) even though PUBLISH_PRERELEASES defaults to false, so the tags exist but no PyPI upload happens unless an admin flips the flag.

  • Drop the tagging. No prerelease tags created until we actually decide to publish prereleases. If we later flip PUBLISH_PRERELEASES, we'd add tagging back at that point. Jake's auto-notes concern dissolves because the only tag that exists is the final. This is what I'd lean toward.
  • Keep the future-proofing tags. Already what the code does. Add --notes-start-tag <last-final> to gh release create for the final so auto-notes diff against the previous final, not the previous rc. ~5 lines of shell. Preserves the "every prerelease version has a corresponding git tag" invariant for when we eventually flip the flag.

2. Drop prereleases entirely. A more aggressive trim that supersedes #1: stabilization happens in-place on the release branch with no rc cycle, no .devN from main, no PyPI uploads of prereleases ever. Final ships when ready, version bumps once at the end. Since prereleases don't publish by default today, the day-one diff to current behavior is small — what we'd lose is the option to flip the flag later and start publishing prereleases. Users who want pre-stable code would install from a git ref. I'd argue against going this far — the prerelease infrastructure is built and validated; keeping it (with #1's tagging trim) is cheap.

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 release/vX.Y.

What gets removed if we switch: cherry_pick_to_release.sh, cherry-pick-to-release.yml, the merge-order topological sort logic, and the operator playbook for resolving cherry-pick conflicts.

Tradeoffs:

Cherry-pick (current) PRs to release branch (Paul's)
Source of truth main is canonical; release branch is a curated subset both branches accept changes; drift possible
Contributor burden nothing new — open PRs against main as usual must know which branch to target; maintainers may need to redirect
Maintainer burden identify SHAs + dispatch cherry-pick workflow review release-branch PRs; manually port to main if needed
Fix-on-both case one PR to main, one cherry-pick dispatch two PRs, or one + manual port
Release-branch-only fix requires temporary land-on-main or script bypass natural — just PR against the release branch
New machinery ~150 lines of shell + a workflow none

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 bump_version.py, cut-release-branch.yml, release.sh, pypi.yml, and RELEASE.md. PR2 would partially undo and reshape what PR1 establishes. The end-to-end dry-run I did would need to be redone for PR1's narrower scope and again for PR2's reintegration.

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.

@psschwei
Copy link
Copy Markdown
Member

I'll have Claude detail out some refactoring ideas

The context we provide the models is going to matter here. For example, if you give it this for a prompt

I want to look at PR 1076 and evaluate this comment left on the PR:
https://github.com/generative-computing/mellea/pull/1076#issuecomment-4446766810 
ignore all other comments, focus on just the code + PR body + this comment

will give you a response that is much more in favor of splitting and just merging the release branch part.

@psschwei
Copy link
Copy Markdown
Member

  1. Drop cherry-pick, switch to PRs targeting the release branch. ... 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.

But this goes against the whole point of having a release branch. A release branch's job is to freeze a code shape so you can stabilize it without main's churn leaking in. Cherry-pick inverts that: it makes main the place where fixes are authored, which means fixes get authored against main's current shape, which means main's churn does leak in.

@ajbozarth
Copy link
Copy Markdown
Contributor Author

The context we provide the models is going to matter here.

I did say it was Claude outlining my opinions, but perhaps I should have been clearer.

But this goes against the whole point of having a release branch. A release branch's job is to freeze a code shape so you can stabilize it without main's churn leaking in. Cherry-pick inverts that: it makes main the place where fixes are authored, which means fixes get authored against main's current shape, which means main's churn does leak in.

Honestly I don't disagree with you, but the current cherry-pick model was what we outlined and decided on in the design call last week, thus why Claude was so insistent on it, it saw it as a design requirement, not a choice made during implementation.

I am fully ok with dropping the whole cheery-pick code and just using PRs. Its the easiest update of the items above

Copy link
Copy Markdown
Contributor

@planetf1 planetf1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Five WARNINGs from a deeper pass focused on retry / idempotency and CI plumbing. None are architectural; all are small fixes. Suggestion blocks attached for each.

Comment thread .github/scripts/release.sh Outdated
Comment thread .github/scripts/cherry_pick_to_release.sh Outdated
Comment thread .github/workflows/docs-publish.yml Outdated
Comment thread .github/scripts/release.sh Outdated
Comment thread .github/scripts/release.sh
Copy link
Copy Markdown
Contributor

@planetf1 planetf1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on the inline WARNINGs review with one more SUGGESTION on workflow concurrency. Escalating to REQUEST_CHANGES because W1, W6, and W7 either break or weaken documented behaviour (retry path for finals, prerelease retry, and CI on the sync PR) — would prefer to see those addressed (or explicitly deferred to a follow-up issue) before merge. Architecture and direction look good; this is about the rough edges around recovery and CI plumbing.

Comment thread .github/workflows/cut-release-branch.yml
Comment thread .github/workflows/cherry-pick-to-release.yml Outdated
@ajbozarth
Copy link
Copy Markdown
Contributor Author

Ok to summarize decisions made in todays call based on my suggestions above:

Drop speculative prerelease tagging

We will be moving the git tagging behind the PUBLISH_PRERELEASES flag so we'll no longer tag rc0.We will also update the release script so the changelog is against the previous release and not the previous pre-release

Drop cherry-pick, switch to PRs targeting the release branch.

Complete removal of the cherry-pick workflow and script. cherry-picking will be done manually and merged via PRs against the release branch. So if a change is needed on the release branch the author will need to create a followup PR to also merge it on main (usually after the PR is merged on the release brach). Though this could be done in the opposite direct depending on the situation.

In addition we'll leave the current workflow env in place for now and can revisit removing it or expanding it's members in settings at a later time

Drop the cherry-pick workflow and script in favor of manual followup PRs
(release-branch fixes get a separate PR back to main). Gate prerelease
tagging, GitHub Release creation, and PyPI upload behind the existing
PUBLISH_PRERELEASES repo variable; with the default (false), the version
bump commit is the only artifact for prereleases.

Also addresses prior review on retry idempotency and CI plumbing.

Cherry-pick removal:
- Delete cherry_pick_to_release.sh and cherry-pick-to-release.yml
- RELEASE.md: backports become a manual followup-PR flow
- ci.yml: dispatch comment now points to release.sh sync-PR use

Prerelease handling gated on PUBLISH_PRERELEASES:
- Tag, prerelease GitHub Release, and PyPI upload only when the flag is
  true; otherwise the version-bump commit is the only artifact
- Incremental notes: rc2 diffs against rc1 so testers see "what's new in
  this rc"; the cumulative view shows up on the final's Release

START_TAG selection for --notes-start-tag:
- Prereleases: most recent reachable tag (incremental)
- Finals: previous final by version shape — git describe excluding rc/dev
  for patches, version-pattern lookup for minors (parallel release branches
  aren't reachable, empty for X.0.0 (gh's default fills in)

Idempotency for bump_type=none retry path:
- gh release create / git tag / git commit / sync PR creation all guarded
  against repeat runs; retry skips what's done, finishes what isn't

Other CI plumbing fixes:
- release.sh: explicit gh workflow run ci.yml after sync PR
- docs-publish.yml: latest_check is continue-on-error and the deploy gate
  fails open on missing output
- cut-release-branch.yml: concurrency: release group added

Assisted-by: Claude Code
EOF
)

Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
@ajbozarth
Copy link
Copy Markdown
Contributor Author

Ok, all of @planetf1 review comments and the follow up items from today's call should be pushed now.

If everyone could please review both the implementation as well as the documentation to make sure I caught everything.

Comment thread RELEASE.md
ajbozarth added 2 commits May 18, 2026 16:45
Lead with the cut/stabilize/promote sequence; move versioning,
PUBLISH_PRERELEASES, workflows, branch protection, retention, and
docs-publish behavior into an appendix. Add a note on the manual prep
for major version bumps.

Assisted-by: Claude Code
Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
The cd.yml workflow was renamed to publish-release in this branch, and
the release flow is workflow_dispatch-only rather than continuous. Use
"release publishing" and "publish-release" directly.

Assisted-by: Claude Code
Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
@psschwei
Copy link
Copy Markdown
Member

another more general comment, though possibly a tangent from this PR: would doing more deliberate sprint planning help at all with the release branch issues? in other words, if we had a clear set of these are the things we are delivering, to what extent could that alleviate some of the issues we may run into in the future with cherry picks, etc. ?

@planetf1
Copy link
Copy Markdown
Contributor

@psschwei more planning could reduce unexpected last minute impacts - work we'd missed, not validated, external dependencies.

This may reduce the need for cherry-picks, but this is in part a technical discussion where if, after we've branched for the release, we find an issue how we resolve it. To some extent it might already be regarded as a process escape if it gets to this point -- but there'll always be exceptions we need to manage, and the concept of a release branch helps in providing some isolation to work those things out without impacting ongoing development, or indeed the release.

The cherry-pick discussion was trying to formalize - set a pattern - for a best practice way to ensure code changes don't get made to the release, and then forgotten to be added to main, and to reduce the overhead of multiple PRs. But since each issue may differ we could have problems specific to the actual release branch (timing, versions) that don't apply to main. I'd therefore see using cherry-pick as a mechanism to keep the same commits a good best practice where it can be applied, but it won't always.

As to whether we do a PR or direct push to release branches - I'd err on PR as it's clearer and consistent with what we do with main, and the overhead is small.

So with the mechanism done, we should always ask ourselves how we could improve the process if we get to the point where a release is 'bad'?

@psschwei
Copy link
Copy Markdown
Member

@planetf1 I think we may be talking about two different things. I agree we need a method to bring bring changes from release branches into main. And I see that what I was referring to above as cherry-pick might not be clear: I meant the workflow in the PR, not the git cherry-pick mechanism. Sorry for the impreciseness.

I was thinking about it from a slightly different angle (one which sort of came up yesterday): We have a patch release scheduled for Tuesday. There's also work on Feature A that is in progress (some has been merged, some hasn't). Would it be better to wait to merge all of Feature A at once post-patch, or do it in stages? In other words, do we schedule merging of all Feature A PRs on Wednesday rather than when they're ready?

I'm not saying we should do that. I'm just wondering if that is also something we need to factor in to our release process. (And it's fine to say no we don't too, I don't think we do, but figured I'd at least bring it in case others had different opinions)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

support an actual release branch

4 participants