Problem Statement
The release pipeline has a version sync gap. Release Drafter computes a semver version from PR labels and tags the GitHub release (e.g., v0.2.0), but the publish workflow builds from pyproject.toml which has its own hardcoded version. Nothing connects these two values — no step bumps pyproject.toml to match the tag, and no step gates the publish if they diverge.
Technical Context
The current flow:
- Release Drafter (
.github/release-drafter.yml) auto-drafts a GitHub release with a version resolved from PR labels via version-resolver (major/minor/patch). Creates a tag like v0.2.0.
- Publish to PyPI (
.github/workflows/publish_to_pypi.yml) triggers on release: published. It runs python -m build, which reads version = "0.1.0" from pyproject.toml.
- The two versions are completely independent. The tag could say
v0.2.0 while PyPI receives 0.1.0.
Failure Scenario
- PRs labeled
feature are merged → release drafter bumps draft to v0.2.0
- Maintainer publishes the GitHub release
python -m build reads version = "0.1.0" from pyproject.toml
twine upload either publishes 0.1.0 (wrong version) or fails if 0.1.0 already exists on PyPI
- Git tag says
v0.2.0, PyPI says 0.1.0 — or the publish fails entirely
Affected Code Areas
pyproject.toml — hardcoded version = "0.1.0"
.github/workflows/publish_to_pypi.yml — build and publish step
.github/release-drafter.yml — version resolution config
Options
A) Dynamic versioning from git tags (recommended)
Use hatch-vcs to derive the version from the git tag at build time. Remove the hardcoded version from pyproject.toml. The release drafter tag becomes the single source of truth.
Changes:
- Add
hatch-vcs to [build-system] requires in pyproject.toml
- Replace
version = "0.1.0" with dynamic = ["version"]
- Add
[tool.hatch.version] config pointing to VCS
- No workflow changes needed —
python -m build picks up the tag automatically
B) Version gate in publish workflow
Add a step that extracts the version from the release tag, compares it to pyproject.toml, and fails the workflow if they don't match. Forces a manual version bump PR before publishing.
Changes:
- Add a validation step to
publish_to_pypi.yml before the build step
- Script extracts tag from
${{ github.event.release.tag_name }}, strips v prefix, compares to pyproject.toml
C) Auto-bump in publish workflow
Add a step that writes the tag version into pyproject.toml before building. The tag overrides at publish time.
Changes:
- Add a
sed or Python script step to publish_to_pypi.yml that patches pyproject.toml before build
Acceptance Criteria
Non-Goals
- Changing the release-drafter label/category configuration
- Migrating away from release-drafter
- Changing the PyPI publish trigger (remains
release: published)
Constraints
- Must work with
hatchling build backend (already in use)
- Must not break local development (
pip install -e .)
- Must be compatible with the existing CI matrix (
testing.yml)
Risk Considerations
Medium. This changes the version source of truth for the package. Option A (hatch-vcs) is well-established and used by many Python packages, but requires testing that pip install -e . still works in development and that the CI build step picks up the tag correctly.
Testing Expectations
- Verify local
pip install -e . works after the change
- Verify
python -m build produces the correct version from a git tag
- Verify the publish workflow succeeds on a test release (or verify via dry-run)
Problem Statement
The release pipeline has a version sync gap. Release Drafter computes a semver version from PR labels and tags the GitHub release (e.g.,
v0.2.0), but the publish workflow builds frompyproject.tomlwhich has its own hardcoded version. Nothing connects these two values — no step bumpspyproject.tomlto match the tag, and no step gates the publish if they diverge.Technical Context
The current flow:
.github/release-drafter.yml) auto-drafts a GitHub release with a version resolved from PR labels viaversion-resolver(major/minor/patch). Creates a tag likev0.2.0..github/workflows/publish_to_pypi.yml) triggers onrelease: published. It runspython -m build, which readsversion = "0.1.0"frompyproject.toml.v0.2.0while PyPI receives0.1.0.Failure Scenario
featureare merged → release drafter bumps draft tov0.2.0python -m buildreadsversion = "0.1.0"frompyproject.tomltwine uploadeither publishes0.1.0(wrong version) or fails if0.1.0already exists on PyPIv0.2.0, PyPI says0.1.0— or the publish fails entirelyAffected Code Areas
pyproject.toml— hardcodedversion = "0.1.0".github/workflows/publish_to_pypi.yml— build and publish step.github/release-drafter.yml— version resolution configOptions
A) Dynamic versioning from git tags (recommended)
Use
hatch-vcsto derive the version from the git tag at build time. Remove the hardcoded version frompyproject.toml. The release drafter tag becomes the single source of truth.Changes:
hatch-vcsto[build-system] requiresinpyproject.tomlversion = "0.1.0"withdynamic = ["version"][tool.hatch.version]config pointing to VCSpython -m buildpicks up the tag automaticallyB) Version gate in publish workflow
Add a step that extracts the version from the release tag, compares it to
pyproject.toml, and fails the workflow if they don't match. Forces a manual version bump PR before publishing.Changes:
publish_to_pypi.ymlbefore the build step${{ github.event.release.tag_name }}, stripsvprefix, compares topyproject.tomlC) Auto-bump in publish workflow
Add a step that writes the tag version into
pyproject.tomlbefore building. The tag overrides at publish time.Changes:
sedor Python script step topublish_to_pypi.ymlthat patchespyproject.tomlbefore buildAcceptance Criteria
development.rstis updated to reflect the new version source (see also tracker item Add early validation for fk_field configuration #3)Non-Goals
release: published)Constraints
hatchlingbuild backend (already in use)pip install -e .)testing.yml)Risk Considerations
Medium. This changes the version source of truth for the package. Option A (hatch-vcs) is well-established and used by many Python packages, but requires testing that
pip install -e .still works in development and that the CI build step picks up the tag correctly.Testing Expectations
pip install -e .works after the changepython -m buildproduces the correct version from a git tag