[NA] [CI] feat: one-click release workflow (tag + publish + bump) - #27
Merged
Conversation
Adds release.yml: a manual-dispatch orchestrator that turns a release into one button instead of the prior manual bump-pyproject + create-tag + publish-release dance. Version is not chosen at dispatch — it comes from release-drafter's resolved_version (feat: -> minor, else patch; overridable by a major/minor/patch label on any PR in the release), so cutting a minor just means merging a feat: PR. release.yml then: refuses if the tag exists, writes the version into pyproject.toml and commits to main, tags vX.Y.Z, calls publish.yml to build+upload to PyPI, and flips the drafted GitHub Release to published. publish.yml gains a workflow_call entry point (invoked by release.yml with target=pypi + the tag ref) while keeping its workflow_dispatch TestPyPI dry-run and release:published manual fallback. Its tag-vs-pyproject guard now also covers workflow_call runs that build from a tag ref. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m last tag Replace release-drafter-driven version resolution with an explicit bump choice input (patch|minor|major, default patch). A human decides breaking vs additive vs fix — a commit prefix can't be trusted to make that call. The version number is computed by applying the chosen bump to the LAST GIT TAG (the record of what was actually released), not pyproject.toml (a file any PR can edit). A drift guard asserts pyproject.toml == the last tag before releasing, so a hand-bumped pyproject or a half-completed prior release fails loudly instead of computing off a stale value. release-drafter is now used ONLY to generate the changelog notes for the GitHub Release; it no longer decides the version. The publish step retargets its draft to our computed tag/name before flipping it live, and falls back to a generated-notes release if no draft exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baz-reviewer
Bot
dismissed
their stale review
July 10, 2026 06:08
Baz dismissed its prior approval because a re-review found new findings.
…guard Addresses two Baz review findings on PR #27. 1. Stale changelog link (logical bug): the publish step retargeted the drafted GitHub Release's tag_name/name but reused release-drafter's body, whose "Full Changelog" compare link is rendered against the drafter's resolved version. With a human-chosen minor/major bump that differs from the drafter, the published release shipped a compare link for the wrong tag. Fix: regenerate the body via GitHub's generate-notes API for OUR $TAG and PATCH it in, so the body always matches the published tag. 2. Duplicate version-guard logic: release.yml and publish.yml both read the pyproject [project].version inline. Extract the read into scripts/pyproject-version.sh; both call it (each keeps its own distinct comparison — release.yml vs last tag, publish.yml vs the build tag), so the TOML parsing can't drift between the two workflows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Baz high-sev finding on PR #27: the notes-regeneration step wrapped `generate-notes` in `|| echo ""`, so any API failure left BODY empty and the PATCH published the release with an empty body — silently erasing the drafter's changelog. Now only override the body when regeneration returns non-empty notes; otherwise omit `-f body` from the PATCH, leaving the drafter's existing body intact (and log a warning). A stale compare link is a minor cosmetic issue; publishing a release with no notes is not. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
User description
Details
Turns cutting a release into one dispatch. Today a release is manual: edit
pyproject.toml, create thevX.Y.Ztag, publish the GitHub Release. This adds a dispatch-triggeredrelease.ymlthat does all of it.A human chooses the bump. The dispatch has a
bumpselect —patch/minor/major(defaultpatch) — because whether a change is breaking (major) vs additive (minor) vs a fix (patch) is a judgment call a commit prefix can't be trusted to make. So when product wants a minor or major, you pick it at release time.The version comes from the last git tag, not pyproject.toml.
release.ymlcomputes the next version by applying the chosen bump to the latestvX.Y.Zgit tag — the record of what was actually released — notpyproject.toml, which any PR can edit. A drift guard assertspyproject.tomlequals the last tag before releasing; if they've diverged (someone hand-bumped pyproject, or a prior release half-completed) it fails loudly so a human reconciles, rather than silently computing off a stale value.release.yml(new,workflow_dispatch):vX.Y.Ztag + chosenbump(patch/minor/major).pyproject.tomlmust match the last tag (no drift); the new tag must not already exist.pyproject.toml, commit tomain.vX.Y.Zand push.publish.yml(builds the tagged source, uploads to PyPI).dry_runinput computes the version and stops (no tag/commit/publish).publish.yml(modified): gains aworkflow_callentry point (invoked byrelease.ymlwithtarget=pypi+ the tagref), while keeping its existingworkflow_dispatchTestPyPI dry-run andrelease: publishedmanual fallback. The tag-vs-pyproject guard now also coversworkflow_callruns that build from a tag ref, and checkout honors the passed ref.scripts/pyproject-version.sh(new): single-line reader for the[project].version. Bothrelease.yml(drift guard vs the last tag) andpublish.yml(guard vs the build tag) call it, so the TOML parsing can't drift between the two workflows; each keeps its own distinct comparison.Release notes match the published tag: the drafted GitHub Release is retargeted to our tag, and its body is regenerated (GitHub
generate-notesfor our$TAG) rather than reusing release-drafter's body — whose "Full Changelog" compare link is rendered against the drafter's resolved version and would otherwise point at the wrong tag when the human-chosen bump differs.release-drafter.yml: unchanged — still keeps a draft warm on every merge tomain;release.ymlretargets that draft to the published tag. Its version-resolver no longer decides the number (a human'sbumpchoice does), and its rendered body is replaced with tag-correct notes on publish.Why human-chosen bump (not auto from commits)
A
feat:/fix:prefix can't reliably signal a breaking change — someone can land a backward-incompatible change underfix:, or afeat:that's purely additive. Deciding major/minor/patch is a human call, so it's an explicit select rather than derived from commit metadata.Change checklist
Issues
Testing
actionlint+ YAML parse clean on both workflows locally (no actionlint gate in this repo; local macOS actionlint may not reproduce every CI finding).release.yml'spublishjob grantsid-token: write(OIDC/Trusted-Publishing) and passestarget: pypi+ref: refs/tags/vX.Y.Z.Local dry-run with
act(Docker, no side effects) + real-state checks:dry_run=true(andbump=minor); onlyresolve-versionis scheduled.bump-tag/publish/publish-github-releaseare skipped byif: ${{ !inputs.dry_run }}, so a dry-run touches nothing.v0.1.1, pyproject0.1.1):patch → v0.1.2,minor → v0.2.0,major → v1.0.0.0.1.1== last tag0.1.1→ proceeds; a simulated drift (pyproject ahead of tag) → aborts with a reconcile message.pyproject.toml: only the top-level[project]version =line changed, result still parses as valid TOML.Current-state check (what a real run does today): last released tag is
v0.1.1, pyproject trails at0.1.1(in sync → drift guard passes). Apatchdispatch would ship0.1.2;minor→0.2.0;major→1.0.0. No one-time setup needed.Only exercisable on GitHub (not by
act): release-drafter reaching the API for notes, theuses: ./publish.ymlreusable-workflow call, and PyPI OIDC upload. Not yet run against PyPI — recommended first real exercise once merged is adry_rundispatch, then the live dispatch.Documentation
The
release.ymlandpublish.ymlheader comments document the full flow, the bump semantics, and each entry point inline (no separate RELEASING doc — kept next to the code so it can't drift).Generated description
Describe how the new
release.ymlworkflow computes the next version from the last git tag, enforces the pyproject/tag drift guard viascripts/pyproject-version.sh, updatespyproject.toml, tags, triggers publish, and retargets the GitHub Release notes. Ensurepublish.ymlremains reusable by acceptingworkflow_callinputs for the tag/ref, enforcing the same guard, handling the event-to-target mapping, and keeping the existing manual/TestPyPI entry points.workflow_callinputs fortarget/ref, enforce the tag-vs-pyproject guard with the shared reader, and keep the manual dry-run/release dispatch paths.Modified files (1)
Latest Contributors(1)
scripts/pyproject-version.sh, write the bump, push the commit/tag, reuse the publish flow, and publish the GitHub Release notes.Modified files (2)
Latest Contributors(1)