Releasing
July 20, 2026 ยท View on GitHub
Releases are tag-driven: CI builds, validates, and publishes the distribution
when a v* tag is pushed, and a tag only builds if every version source agrees
with it.
Version sources
Each repository declares its version statically in several places --
pyproject.toml (project.version), <package>/__init__.py (__version__),
and any plugin manifests. The authoritative list for a given repository is
whatever its tag check verifies: see the "Check tag matches package versions"
step in .github/workflows/build.yaml. docs/conf.py (release), where
present, also carries the version but is not tag-checked; keep it in step when
bumping.
Cutting a release
- Pick the version (
X.Y.Z) and bump every checked source to it in one commit. - Tag that commit
vX.Y.Zand push the tag. - CI verifies the tag matches all sources -- any mismatch fails the build with
an error naming the offending file -- then builds the wheel and sdist with
uv buildand validates metadata withtwine check. - The
publishjob uploads the distribution to PyPI via trusted publishing.
First release only
Trusted publishing needs one-time wiring before the first tag:
- On PyPI, add a publisher for the package (a pending publisher if the
project does not exist yet): the repository, workflow
build.yaml, environmentpypi. - In the GitHub repository, create the
pypienvironment (Settings -> Environments) that the publish job targets.
Sanity checks before tagging
# suite + hooks green
uv run --no-sync pytest
uv run --no-sync pre-commit run --all-files
# distribution builds and self-describes correctly
uv build
uvx twine check dist/*