Maintainer release workflow
This page is for maintainers preparing TestPyPI or PyPI releases. It is not part of the user-facing release notes.
Maintain the changelog
docs/changelog.md is the canonical version-by-version record of user-visible
changes. Keep its first section titled Unreleased — <development version>,
where the development version matches pyproject.toml and
phy/__init__.py.
Every pull request with a user-visible change should add a concise entry under
Added, Changed, Deprecated, Removed, Fixed, or Security in the
unreleased section. Describe the effect on users rather than listing commits
or test changes. Link an issue or pull request when it gives useful context.
Pure tests, refactors, formatting, and other internal changes do not need
separate entries.
When preparing a release:
- Review the unreleased entries for completeness, accuracy, and duplicates.
- Replace the unreleased heading with the final version and release date.
- Add a fresh unreleased section for the next development version.
- Update the comparison links at the bottom of
docs/changelog.md. - Verify that the package version, changelog heading, release notes, tag, and built artifacts all agree.
Local release smoke test
The repository contains a local packaging smoke test that mirrors the end-user install flow in an
isolated virtual environment and uses the small template dataset from ../phy-data/template/.
Run this before upload to validate the built wheel:
make smoke-local
make open-local
make smoke-local builds the wheel, creates .release-smoke/local, installs phy from
the local wheel into that fresh environment, checks imports and CLI entry points, and runs:
phy template-describe ../phy-data/template/params.py
make open-local launches the GUI from that isolated environment so that you can confirm
the dataset opens correctly.
After upload to TestPyPI or PyPI, validate the published package in another fresh environment:
make smoke-pypi
make open-pypi
For TestPyPI, use:
make smoke-test
make open-test
smoke-pypi and open-pypi default to the version from pyproject.toml.
smoke-test and open-test default to the recorded latest TestPyPI dev build.
You can override either with SMOKE_VERSION=<version>.
This validates the intended user path: a plain install in a fresh environment, followed by opening
a dataset with phy template-gui.
Disposable TestPyPI dev releases
TestPyPI does not let you overwrite an existing file for the same version, so the repository includes a disposable dev-release workflow around the current checked-in release candidate version.
Before publishing, provide a TestPyPI API token. Username/password uploads are rejected by
TestPyPI. The helper accepts TESTPYPI_TOKEN, TEST_PYPI_TOKEN, or UV_PUBLISH_TOKEN, and also
falls back to ~/.pypirc under [testpypi] when it contains:
username = __token__
password = pypi-...
Run:
make publish-test
This command:
- reads the current version from
pyproject.toml(for example2.1.0rc1) - queries TestPyPI for existing
2.1.0rc1.devNreleases - picks the next free version such as
2.1.0rc1.dev3 - creates a temporary staged copy of the repository
- updates the version in that temporary copy only
- builds and publishes that disposable version to TestPyPI
- records the published version in
.release-smoke/latest-testpypi-version.txt
Your working tree keeps the original final candidate version unchanged.
After publishing the disposable TestPyPI build, validate exactly that uploaded version with:
make smoke-test
make open-test
You can print the recorded version directly with:
make version-test
Final PyPI publish
Once the disposable TestPyPI builds have been validated on your different operating systems and you
are ready to publish the exact checked-in version from pyproject.toml, run:
The final publish helper accepts PYPI_TOKEN or UV_PUBLISH_TOKEN, and also falls back to
~/.pypirc under [pypi] with:
username = __token__
password = pypi-...
Then run:
make publish-pypi
This target refuses to publish if the checked-in version still contains .dev.
Typical RC release checklist
For a normal release-candidate cycle, the usual command sequence is:
# 1. Local code and packaging checks
make test-fast
make build
make smoke-local
# 2. Publish a disposable TestPyPI build for this RC line
make publish-test
# 3. Verify that uploaded TestPyPI build on this machine
make smoke-test
make open-test
# 4. Repeat step 3 on your other OS machines
make version-test
# then on the other machine, after recording that version locally:
make smoke-test
make open-test
# 5. Once everything is green, publish the checked-in RC version to real PyPI
make publish-pypi
# 6. Verify the real PyPI release
make smoke-pypi
make open-pypi
If a TestPyPI upload fails validation, fix the issue locally and run
make publish-test again. It will automatically choose the next free .devN
version without changing the checked-in RC version.
If another machine does not yet have .release-smoke/latest-testpypi-version.txt, you can still
fall back to:
make smoke-test SMOKE_VERSION=<version>
make open-test SMOKE_VERSION=<version>
Post-release checklist
After the final PyPI upload succeeds:
- Confirm the final changelog section, date, comparison links, and package version agree.
- Confirm the PyPI JSON index lists both the wheel and source distribution, and verify their SHA-256 hashes against the files built from the release tag.
- Install the exact version from PyPI in a fresh environment with
make smoke-pypi SMOKE_VERSION=<version>and complete the GUI check withmake open-pypi SMOKE_VERSION=<version>. - Create a non-prerelease GitHub release from the existing annotated tag. Attach the exact wheel and source distribution already verified against PyPI; do not rebuild them.
- Verify the documentation build for the default branch and check the live release notes, README badges, PyPI page, and GitHub release links.
- Publish the prepared community announcement only after PyPI, GitHub, and the documentation are live.
- Close or update the release milestone. Keep the release branch until all public artifacts have been checked, then remove it only with maintainer approval.
- Make any development-version bump and fresh unreleased changelog section on the default branch in a separate pull request.