Releasing
How the version is stored, how to bump it, and how a release reaches PyPI.
Where the version lives
The version is declared in exactly one place: [project].version in
pyproject.toml (PEP 621). Everything else is derived from it.
| Place | How it gets the version |
|---|---|
pyproject.toml, [project].version |
declared by hand - the only source of truth |
wtv.__version__ |
read from the installed distribution metadata (importlib.metadata) |
wtv-cli --version |
the same metadata, through wtv.__version__ |
uv.lock, the name = "wtv" package |
written by uv when the lock is refreshed |
git tag v<version> |
created when the release is cut |
Never edit the version in uv.lock by hand - refresh the lock with uv lock.
tests/test_version.py fails if the declared version, the installed metadata and
the lock file disagree, so a mismatch cannot go unnoticed.
Version policy
The project adheres to semantic versioning, as recorded at the top of the changelog.
| Change | Bump | Changelog section |
|---|---|---|
| Bug fix with no change in behaviour | patch | Fixed |
| New backwards-compatible functionality, a new CLI option or API parameter | minor | Added |
| A change to what the same input produces, e.g. a rescaled default parameter | minor | Changed |
| Removed or renamed option or parameter, dropped Python version, new output layout | major | Changed / Removed |
Because a changed default can change a user's results, it is never a patch bump. Whatever the bump, the change and its reason belong in the changelog.
Bumping the version
Bumping is done with bump-my-version,
configured in [tool.bumpversion] in pyproject.toml. It reads the current
version from the PEP 621 metadata, so there is no second copy of the version to
keep in sync, and it rewrites pyproject.toml and uv.lock together. It does not
commit or tag - the release branch goes through a pull request like any other
change.
-
Start from a clean, up-to-date
main. The tool refuses a tree with modified tracked files, which is why the setup for a release is committed before the bump itself. Untracked files are not refused, so do not expect it to catch stray data files for you.bash git checkout main && git pull git checkout -b rel/<version> -
Preview what would change -
<part>ismajor,minororpatch:bash uv run bump-my-version bump <part> --dry-run --verboseTwo targets should be listed:
pyproject.toml:project.version, printed as a PEP 621 pointer rather than a bare filename, anduv.lock. Anything else in that list is a mistake in[tool.bumpversion]. -
Apply the bump:
bash uv run bump-my-version bump <part> -
Refresh the environment, so the installed metadata and the lock agree:
bash uv sync --all-extras --devWithout this step the editable install still reports the previous version and
tests/test_version.pyfails. That failure is the intended signal, not a problem with the test.Pass the extras.
devanddocsare optional-dependency extras rather than uv dependency groups, so a bareuv syncremoves them - it uninstalls pytest, ruff, black, isort and bump-my-version, and the verification below then fails for the wrong reason. -
Verify:
bash uv lock --locked # the lock agrees with pyproject.toml uv run pytest # the whole suite, including the version checks uv run ruff check . uv build # dist/wtv-<version>-*.whl and .tar.gz carry the new version uv run wtv-cli --version -
Move the
[Unreleased]changelog block under a## [<version>] - YYYY-MM-DDheading and open a fresh, empty[Unreleased]. Stamp the date of preparation and correct it if the release slips. -
Commit the two rewritten files plus the changelog, push the branch and open a pull request against
main. CI runs the tests and the build on three operating systems.
Cutting the release
After the release pull request is merged:
git checkout main && git pull
git tag -a v<version> -m "Release <version>"
git push origin v<version>
-a makes an annotated tag. The tags up to v0.2.1 are lightweight, so this
deliberately changes the convention: an annotated tag records who tagged it and
when, and git describe ignores lightweight tags unless given --tags.
Then publish a GitHub release for that tag. .github/workflows/publish.yml runs
on the published event, builds with uv build and uploads with
uv publish --token $PYPI_API_TOKEN. The workflow can also be started by hand
(Run workflow) if the release already exists and publishing failed.
Checklist
- [ ] The bump level matches the policy above, not merely the size of the diff
- [ ] The dry run lists only
pyproject.toml:project.versionanduv.lock - [ ] The environment was refreshed with
uv sync --all-extras --dev, so the verification ran with the tools installed - [ ] Every user-visible change since the previous tag is in the changelog section for this release, including its reason and any limitation
- [ ]
uv lock --locked,uv run pytest,uv run ruff check .anduv buildpass - [ ] The release pull request is merged into
mainbefore the tag is pushed - [ ] The GitHub release notes match the changelog section
- [ ] After publishing, the new version installs from PyPI:
pip install wtv==<version> && wtv-cli --version
Limitations
- Publishing requires a published GitHub release; pushing a tag alone publishes nothing.
- The bioconda recipe advertised by the README badge lives outside this repository and is not covered by this workflow.
- Changelog entries are written by hand; nothing generates them from merged pull requests.
- The
uv.lockentry under[tool.bumpversion]matches the shape of the lock file uv writes today. If that shape changes, the bump stops with an error (ignore_missing_version = false) rather than half-bumping the release. devis a published extra, not a uv dependency group, sobump-my-versionand the other release tools appear inRequires-Dist ... extra == "dev"andpip install wtv[dev]pulls them. Moving the wholedevextra to a[dependency-groups]section would keep them out of the published metadata and make a bareuv syncinstall them; that changes everyuv sync --all-extrascall in the README, the docs and CI, so it is a change for its own branch.