Skip to content

[NA] [CI] feat: one-click release workflow (tag + publish + bump) - #27

Merged
JetoPistola merged 4 commits into
mainfrom
danield/NA-release-tag-publish-bump
Jul 10, 2026
Merged

[NA] [CI] feat: one-click release workflow (tag + publish + bump)#27
JetoPistola merged 4 commits into
mainfrom
danield/NA-release-tag-publish-bump

Conversation

@JetoPistola

@JetoPistola JetoPistola commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

User description

Details

image

Turns cutting a release into one dispatch. Today a release is manual: edit pyproject.toml, create the vX.Y.Z tag, publish the GitHub Release. This adds a dispatch-triggered release.yml that does all of it.

A human chooses the bump. The dispatch has a bump select — patch / minor / major (default patch) — 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.yml computes the next version by applying the chosen bump to the latest vX.Y.Z git tag — the record of what was actually released — not pyproject.toml, which any PR can edit. A drift guard asserts pyproject.toml equals 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):

  1. Compute next version = latest vX.Y.Z tag + chosen bump (patch/minor/major).
  2. Guard: pyproject.toml must match the last tag (no drift); the new tag must not already exist.
  3. Write the version into pyproject.toml, commit to main.
  4. Tag the bump commit vX.Y.Z and push.
  5. Call publish.yml (builds the tagged source, uploads to PyPI).
  6. Publish the GitHub Release at the tag, with notes regenerated for that tag (see below).
  • A dry_run input computes the version and stops (no tag/commit/publish).

publish.yml (modified): gains a workflow_call entry point (invoked by release.yml with target=pypi + the tag ref), while keeping its existing workflow_dispatch TestPyPI dry-run and release: published manual fallback. The tag-vs-pyproject guard now also covers workflow_call runs that build from a tag ref, and checkout honors the passed ref.

scripts/pyproject-version.sh (new): single-line reader for the [project].version. Both release.yml (drift guard vs the last tag) and publish.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-notes for 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 to main; release.yml retargets that draft to the published tag. Its version-resolver no longer decides the number (a human's bump choice 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 under fix:, or a feat: 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

  • User facing
  • Documentation updated (if needed)
  • Tests added/updated (if needed)
  • Breaking changes documented (if any)

Issues

  • NA (release tooling)

Testing

  • actionlint + YAML parse clean on both workflows locally (no actionlint gate in this repo; local macOS actionlint may not reproduce every CI finding).
  • Confirmed the reusable-workflow wiring: release.yml's publish job grants id-token: write (OIDC/Trusted-Publishing) and passes target: pypi + ref: refs/tags/vX.Y.Z.

Local dry-run with act (Docker, no side effects) + real-state checks:

  • Guard wiring — dispatched with dry_run=true (and bump=minor); only resolve-version is scheduled. bump-tag / publish / publish-github-release are skipped by if: ${{ !inputs.dry_run }}, so a dry-run touches nothing.
  • Version compute — against the real repo (last tag v0.1.1, pyproject 0.1.1): patch → v0.1.2, minor → v0.2.0, major → v1.0.0.
  • Drift guard — pyproject 0.1.1 == last tag 0.1.1 → proceeds; a simulated drift (pyproject ahead of tag) → aborts with a reconcile message.
  • bump-tag → pyproject rewrite — ran the embedded bump on a copy of the real 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 at 0.1.1 (in sync → drift guard passes). A patch dispatch would ship 0.1.2; minor0.2.0; major1.0.0. No one-time setup needed.

Only exercisable on GitHub (not by act): release-drafter reaching the API for notes, the uses: ./publish.yml reusable-workflow call, and PyPI OIDC upload. Not yet run against PyPI — recommended first real exercise once merged is a dry_run dispatch, then the live dispatch.

Documentation

The release.yml and publish.yml header 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.yml workflow computes the next version from the last git tag, enforces the pyproject/tag drift guard via scripts/pyproject-version.sh, updates pyproject.toml, tags, triggers publish, and retargets the GitHub Release notes. Ensure publish.yml remains reusable by accepting workflow_call inputs for the tag/ref, enforcing the same guard, handling the event-to-target mapping, and keeping the existing manual/TestPyPI entry points.

TopicDetails
Publish workflow Adapt the publish workflow to accept workflow_call inputs for target/ref, enforce the tag-vs-pyproject guard with the shared reader, and keep the manual dry-run/release dispatch paths.
Modified files (1)
  • .github/workflows/publish.yml
Latest Contributors(1)
UserCommitDate
danield@comet.comfix(release): regenera...July 10, 2026
Release dispatch Coordinate the release dispatch workflow to compute the next tag, guard against pyproject drift via scripts/pyproject-version.sh, write the bump, push the commit/tag, reuse the publish flow, and publish the GitHub Release notes.
Modified files (2)
  • .github/workflows/release.yml
  • scripts/pyproject-version.sh
Latest Contributors(1)
UserCommitDate
danield@comet.comfix(release): don't wi...July 10, 2026
Review this PR on Baz | Customize your next review

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>
@JetoPistola
JetoPistola marked this pull request as ready for review July 10, 2026 05:39
baz-reviewer[bot]
baz-reviewer Bot previously approved these changes Jul 10, 2026
…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
baz-reviewer Bot dismissed their stale review July 10, 2026 06:08

Baz dismissed its prior approval because a re-review found new findings.

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
…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>
Comment thread .github/workflows/release.yml Outdated
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>
@JetoPistola
JetoPistola merged commit 4f42529 into main Jul 10, 2026
7 checks passed
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