Releasing¶
A PyPI version number can never be reused, and a release cannot be edited after the fact — only yanked. Everything below exists to make the irreversible step the last one, and to make it boring.
One-time setup¶
PyPI publishing uses Trusted Publishing, so there is no API token in the repository and nothing to rotate or leak. It has to be registered on PyPI first.
For the first release, the project does not exist on PyPI yet, so register a pending publisher: PyPI → Your account → Publishing → Add a pending publisher.
| Field | Value |
|---|---|
| PyPI project name | qmlkit |
| Owner | Ziadt160 |
| Repository | qmlkit |
| Workflow | release.yml |
| Environment | pypi |
Repeat the same on TestPyPI with environment testpypi.
Then create both environments in the GitHub repository (Settings → Environments):
testpypi and pypi. Adding a required reviewer to pypi is worth it — it turns
the final upload into something you approve by hand.
Cutting a release¶
-
Update the changelog. Move
## [Unreleased]to## [X.Y.Z] - YYYY-MM-DD. Every entry should say what changed and why, not just what was added. -
Set the version in two places, which must agree or the workflow refuses:
pyproject.toml(project.version) andsrc/qmlkit/__init__.py(__version__). Those are the only two -verify_install.pyreads the packaging metadata and checks the module constant against it, rather than carrying a third copy that has to be remembered. -
Run the whole thing locally, in both interpreters — no single one can import all four backends:
- Verify the built artifact, not the source tree. An editable install imports
out of
src/and keeps working even if a module never made it into the wheel:
python -m venv /tmp/clean && /tmp/clean/bin/pip install dist/qmlkit-*.whl && /tmp/clean/bin/python scripts/verify_install.py
On Windows the venv puts them in Scripts rather than bin:
python -m venv $env:TEMP\clean
& "$env:TEMP\clean\Scripts\python.exe" -m pip install (Get-Item dist\qmlkit-*.whl)
& "$env:TEMP\clean\Scripts\python.exe" scripts\verify_install.py
- Push, then tag.
The tag must match the packaged version exactly; the workflow checks and refuses
otherwise, because a tag that disagrees with pyproject.toml would publish a
version nobody asked for.
This step used to be the one that was easy to get wrong. qmlkit was developed as the
qmlkit/subdirectory of a lecture repository and published as agit subtree splitof it, sorelease.ymlexisted only in the published copy and tagging upstream triggered nothing — while the split itself published whatever branch it was given, which twice nearly shipped a release missing half its fixes. The library moved to its own repository on 2026-09-13 and none of that applies any more.
-
Watch the workflow. It runs the suite again on the tagged commit, builds, re-verifies the wheel in a clean environment, publishes to TestPyPI, and only then publishes to PyPI.
-
Install it the way a stranger would, from a machine that has never seen the source:
If something goes wrong¶
- Bad metadata or a broken README on TestPyPI — fix it, bump to the next patch version, tag again. Do not try to reuse the number.
- Already published to PyPI and it is broken —
yankthe release rather than deleting it. Yanking leaves existing pins working while stopping new installs from resolving to it. - The tag does not match
pyproject.toml— the workflow fails before publishing anything. Delete the tag, fix the version, tag again:
-
Nothing happened when you pushed the tag — check it actually arrived with
git ls-remote --tags origin, and that Actions is healthy. On 2026-09-13 two pushes tomaincreated no workflow runs at all during a GitHub incident, andworkflow_dispatchreturned HTTP 500 while queueing the job anyway. A tag pushed into that is a release that may half-complete, and a half-completed release burns the version number on TestPyPI. Wait for Actions to be draining normally. -
Never force-push over a published tag. It must keep pointing at the commit that produced the artifact on PyPI, or the provenance is a lie.