Skip to content

Release to PyPI from a pushed vX.Y.Z tag, through CI - #212

Merged
jasperblues merged 3 commits into
mainfrom
release-from-tag
Oct 27, 2026
Merged

jasperblues merged 3 commits into
mainfrom
release-from-tag

Conversation

@jasperblues

Copy link
Copy Markdown
Member

Fixes #158. The same design is in liberation-data/drivine4j#284 and liberation-data/drivine#332, so all three Drivine libraries release the same way.

Process (RELEASING.md):

  1. A release PR bumps version in pyproject.toml.
  2. Merging it is the release point. The maintainer tags the merge commit on main with vX.Y.Z and pushes the tag.
  3. The tag push runs .github/workflows/release.yml.

release.yml

  • Triggers.
    • push of a v* tag publishes.
    • workflow_dispatch, and a pull_request that touches release.yml, RELEASING.md, pyproject.toml or scripts/smoke_wheel.py, are dry runs that never publish. This PR's own run is the first dry run.
  • check. On a tag push it fails with a clear message unless:
    • the tag is vX.Y.Z (with an optional -suffix);
    • it equals pyproject.toml's version;
    • the tagged commit is on main.
  • gate. It runs ci.yml itself, which gains workflow_call: and is otherwise unchanged. It needs check, so a bad tag fails fast.
  • build.
    • uv build (sdist, and a wheel built from it).
    • Fails a tag push if PyPI already has the version. A dry run only warns, and an unreachable PyPI warns and carries on.
    • twine check --strict.
    • scripts/smoke_wheel.py on Python 3.11 and 3.14. It installs the wheel into a clean venv, asserts the installed copy is the one imported, and runs python -m drivine.stubs on a throwaway project.
    • Uploads dist/ as an artifact.
  • publish runs only on a tag, in the release environment. It publishes those exact files with pypa/gh-action-pypi-publish and trusted publishing, so no token is stored.
  • github-release runs gh release create --generate-notes --verify-tag, and marks the release as a prerelease if the version has a -.
  • Third-party actions are pinned to full SHAs. Permissions are contents: read, widened only per job: id-token: write on publish, contents: write on github-release.

Settings for the maintainer to add (no secrets):

  1. A GitHub environment named release. Optionally give it required reviewers and a deployment tag rule v*.
  2. A PyPI trusted publisher on the drivine project: owner liberation-data, repository drivine-python, workflow release.yml, environment release.

Review. Three passes; the third was clean.

  • Pass 1 added the PyPI already-has-it check, the prerelease marking, and gate waiting on check.
  • Pass 2 made that check survive a network error.

Checked locally without a publish: the build, twine check, the smoke test on 3.11 and 3.14, the tag check against a matching tag, a mismatched tag, a missing v and a commit off main, and bash -n on every step. The real publish and the trusted-publisher setup can only be tested on GitHub with a real tag.

The tag is checked against pyproject.toml's version and the default branch, the full CI runs, and the sdist and wheel are built, checked with twine and a smoke test of the wheel, and published with trusted publishing. A manual run, or a pull request that changes the release machinery, is a dry run that leaves the built files to inspect. RELEASING.md describes the process.

Closes #158.
@jasperblues
jasperblues merged commit bce2186 into main Oct 27, 2026
16 checks passed
@jasperblues jasperblues mentioned this pull request Oct 28, 2026
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.

Release from a tag: CI builds and publishes, with one tag style across the three libraries

1 participant