Versioning & releases¶
One source of truth¶
The repo-root VERSION file is the only place the version lives:
| Consumer | How it gets the version |
|---|---|
| PyPI wheel / sdist | pyproject.toml reads VERSION via setuptools' dynamic version = { file = "VERSION" } |
| Python runtime | mef3io_coregistration.__version__ (installed metadata; 0+unknown in an uninstalled source tree) |
| Saved results | tool_version in every result and clock map (= __version__) |
| Git tags | vX.Y.Z, created by the release trigger |
CI asserts mef3io_coregistration.__version__ == VERSION, and git tags
mirror the file — they are created by the release trigger, so they cannot
drift.
Releasing¶
Before the release: revise the documentation (required)¶
Every release starts with a complete documentation revision, done as its own reviewed pull request and merged before the tag is created. Check each of these against the code that is about to ship, not against the previous release:
- [ ]
README.md(it is also the PyPI project page, so links must be absolute) - [ ] the documentation site (
docs/, built withmkdocs build --strict) - [ ]
examples/README.mdand the example scripts it lists - [ ]
pyproject.toml:description,keywords,classifiers - [ ]
CITATION.cff - [ ]
CLAUDE.md(status, module map, invariants) - [ ] the release notes: draft what changed and what an upgrading user must
do in
docs/release_notes/X.Y.Z.md, and add it to the release-notes index and themkdocs.ymlnav (newest first). Between releases, pull requests collect their notes indocs/release_notes/unreleased.md: the first pull request after a release that needs a note creates it (starting with a*Draft.*line) and adds it to the index and themkdocs.ymlnav as "Unreleased". The revision renames it toX.Y.Z.md, rewrites it for the reader (highlights, fixes, upgrading), replaces its draft line with*Released YYYY-MM-DD.*and removes the "Unreleased" entries. The release workflow refuses to publish while the notes page still starts a line with*Draft, so this step cannot be skipped. Links in these pages are absolute, so they work on GitHub too: the release workflow usesdocs/release_notes/X.Y.Z.mdas the GitHub release text when it exists (GitHub's generated notes otherwise).
Nothing in these may state a fact that goes stale on its own (test counts, "currently", dates of past work); link to the release notes instead.
Cutting the release¶
One button: run the bump-version workflow (GitHub → Actions →
bump-version → Run workflow) on main, choosing patch, minor, major,
or an explicit X.Y.Z (pre-releases such as 1.3.0rc1 too). An explicit
version equal to VERSION releases the current commit as it is. It commits
the bumped VERSION as Release vX.Y.Z, tags, and dispatches the
release workflow, which:
- checks that the tag matches
VERSIONand thatdocs/release_notes/X.Y.Z.mdis no longer marked as a draft; - builds the sdist and the wheel (
python -m build) and checks them withtwine check; - installs the built wheel and runs the test suite against it;
- publishes both to PyPI (
twine,PYPI_Token_Generalsecret); - creates the GitHub release with the release notes page as its text and
the two files attached (marked as a pre-release for
aN/bN/rcN).
Dispatching the release workflow manually on a branch is a dry run: it builds and tests, and publishes nothing.
If main is ever protected (pull requests required), give GitHub
Actions a bypass in that rule: bump-version pushes the VERSION commit
and the tag to main itself.
CI¶
ci.yml runs on pushes to main and dev and on every pull request: ruff,
the full pytest matrix (Ubuntu 3.11/3.12/3.13, macOS 3.13, Windows 3.13), a
Python 3.14 smoke test, and the version-consistency check. The docs site
(docs.yml) builds with mkdocs build --strict and checks the built site for
private names, device names and dataset channel labels on every pull request
and push to main; only main is deployed to GitHub Pages.