Skip to content

feat(ci): propose the next release as a reviewable pull request - #301

Merged
abrichr merged 1 commit into
mainfrom
feat/propose-release-bot
Aug 26, 2026
Merged

feat(ci): propose the next release as a reviewable pull request#301
abrichr merged 1 commit into
mainfrom
feat/propose-release-bot

Conversation

@abrichr

@abrichr abrichr commented Aug 26, 2026

Copy link
Copy Markdown
Member

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.yml computes the next version from the conventional commits since the last tag, writes the three release-metadata files, and opens one pull request titled chore(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:

previous=0.92.0
next=0.93.0
released=true
changes=6

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.py does the arithmetic instead. It reads the policy already declared in pyproject.toml rather than hardcoding it, so major_on_zero = false keeps 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:

  • The lifecycle App is never granted contents access, so it cannot push code.
  • The branch push uses the built-in token. The App token opens the pull request, which is the only reason CI runs on the proposal at all; a pull request opened with the built-in token starts no further workflow.
  • The workflow never pushes to main.

test_release_proposal_can_only_propose asserts there is no gh pr merge, no --auto, no git tag, no gh 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 dropping needs: reject-lifecycle-app fails 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 main does not turn red in the gaps.

Verification

  • 316 tests pass across the plan, identity, release, evidence, and importer suites.
  • 28 new tests cover the version arithmetic on both 0.x and 1.x lines, breaking changes under both major_on_zero settings, 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

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>
@abrichr
abrichr merged commit 445414a into main Aug 26, 2026
1 check passed
@abrichr
abrichr deleted the feat/propose-release-bot branch August 26, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant