feat(ci): propose the next release as a reviewable pull request - #301
Merged
Conversation
The release workflow refuses to guess a version: it requires pyproject.toml and CHANGELOG.md to already carry the exact version being published. Somebody had to write those by hand. This writes them instead. scripts/plan_release.py reads the conventional commits since the last tag and computes the next version under the policy already declared in pyproject.toml (major_on_zero = false, allow_zero_version = true), then stamps the version and renders the changelog section in the exact form the release workflow greps for. It replaces the version arithmetic python-semantic-release used to do, which cannot run here: GitPython 3.1.60 removed Actor.name_email_regex, every released version reads it during config load, and the upstream fix is unmerged. .github/workflows/propose-release.yml runs that on every push to main, syncs uv.lock through the existing verify_release_lock helper, pushes release/next with the built-in token, and opens or updates one pull request using the lifecycle App token. The App token is what makes CI run on the proposal; a pull request opened with the built-in token starts no further workflow. The bot cannot land what it proposes. The lifecycle App is never granted contents access, the workflow never pushes to main, and the contract test asserts it contains no merge, no auto-merge, no tag, no release, no publish step, and no reference to the release App. A staged or empty history is a quiet no-op rather than a failure, so main does not go red between releases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automates everything up to the merge. You read one pull request and click Merge; the bot writes it.
What it does
On every push to
main,propose-release.ymlcomputes the next version from the conventional commits since the last tag, writes the three release-metadata files, and opens one pull request titledchore(release): prepare X.Y.Z. If a later commit lands before you merge it, the same pull request updates in place.Right now, against real history, it computes:
Why not python-semantic-release
It cannot run. GitPython 3.1.60 removed
Actor.name_email_regex, every released version reads it while loading its config, and the upstream fix is unmerged. That is what has been failing the release job since 24 August, diagnosed in #298.scripts/plan_release.pydoes the arithmetic instead. It reads the policy already declared inpyproject.tomlrather than hardcoding it, somajor_on_zero = falsekeeps a breaking change on the 0.x line at a minor bump, and a test asserts the file still says that.What it deliberately cannot do
The whole point of #296 was that one identity proposes and a different one publishes. This keeps that:
contentsaccess, so it cannot push code.main.test_release_proposal_can_only_proposeasserts there is nogh pr merge, no--auto, nogit tag, nogh release, no publish action, and no reference to the release App anywhere in the file. Two mutation checks confirm the guards bite: adding an auto-merge fails that test, and droppingneeds: reject-lifecycle-appfails the identity contract.Quiet by default
Between releases the workflow does nothing and says so. A push with no releasable commits prints "No releasable change since the last tag". A push while a bump is already staged and waiting to publish prints that instead. Neither is a failure, so
maindoes not turn red in the gaps.Verification
major_on_zerosettings, changelog rendering against the exact regex the release workflow greps with, and the write path.ruff check .passes. The workflow parses as YAML with the expected jobs and triggers.The one manual step left
Read the proposal, merge it, then dispatch Release and publish with that version and the merge commit. The bot writes the numbers; a person still decides they are right before anything reaches PyPI.
🤖 Generated with Claude Code