Releasing a new version¶
A release is a git tag. Pushing vX.Y.Z triggers .github/workflows/release.yml,
which builds the sdist and wheel from packages/typedecide, runs twine check,
publishes, and creates the GitHub Release. Nobody uploads from a laptop. There is no
PyPI API token anywhere: the workflow authenticates with
PyPI Trusted Publishing (OIDC).
| Tag | Published to | GitHub environment | GitHub Release |
|---|---|---|---|
vX.Y.ZrcN, for example v0.2.0rc1 |
TestPyPI | testpypi |
pre-release |
vX.Y.Z, for example v0.2.0 |
PyPI | pypi |
latest |
The rule is literal: a tag containing rc goes to TestPyPI, and any other matching
tag goes to PyPI.
Two facts shape the whole procedure
A PyPI upload cannot be undone, and a version number can never be reused, not even after deleting the release. So every check runs before the upload, and the fix for a bad release is always a new version.
The version lives in one place: __version__ in
packages/typedecide/src/typedecide/__init__.py. pyproject.toml reads it from there,
and the workflow refuses to run if the tag disagrees with it.
The maintainers' full procedure, including the one-time PyPI and GitHub setup, is
RELEASING.md
in the repository. This runbook is the operational summary.
Symptoms¶
You are here because:
- A change is merged and needs to reach users, or
- A release run failed, or
- A published release is bad and must be withdrawn.
Diagnosis¶
Before tagging: is main releasable?¶
Run from packages/typedecide on an up-to-date main with a clean working tree.
python -m pip install -e ".[dev]" build twine
python -m pytest
python -m ruff check src tests
python -m mypy --strict src/typedecide
rm -rf dist && python -m build && python -m twine check --strict dist/*
- The
checksworkflow is green on the commit you will tag. -
CHANGELOG.mdlists every user-visible change under[Unreleased]. -
README.mdreads correctly on its own. PyPI renders it as the project page and does not resolve relative links. -
import typedecidestill imports neither torch nor transformers. - The documentation builds:
mkdocs build --strict. - For anything other than a patch release, a release candidate has been through TestPyPI first.
After a failed run: which job failed?¶
| Failed job | What it means | Has anything been published? |
|---|---|---|
build: Tag must match __version__ |
The tag is not exactly v + __version__, or the version is not X.Y.Z / X.Y.ZrcN |
No |
build: Changelog must have a section |
No non-empty ## [X.Y.Z] section in CHANGELOG.md |
No |
build: twine check |
Package metadata is invalid or the README does not render | No |
build: wheel contents or smoke test |
A subpackage, py.typed or the licence is missing from the wheel, or the installed wheel does not import cleanly |
No |
publish-pypi / publish-testpypi: invalid-publisher |
The Trusted Publisher registration does not match the repository, workflow file name or environment name | No |
publish-*: file already exists |
That version was already uploaded | It was, earlier |
github-release |
The package is published. Only the GitHub Release is missing | Yes |
Fix¶
Cut a release¶
-
Pick the version. SemVer: patch for fixes, minor for additions, major for breaking changes. While the version is
0.y.z, a breaking change bumps the minor. Versions are PEP 440:0.2.0or0.2.0rc1. Novinside the version, and no hyphen beforerc. -
Bump the version. One line in
src/typedecide/__init__.py: -
Update the changelog. Rename
## [Unreleased]to## [0.2.0] - YYYY-MM-DD, add a fresh empty## [Unreleased]above it, and update the link references at the bottom of the file. The heading format matters: the workflow extracts the section whose heading starts## [0.2.0]and uses it as the GitHub Release body. A release candidate does not need its own section;0.2.0rc1falls back to[0.2.0], then to[Unreleased]. -
Commit and merge through the normal pull-request path.
-
Tag the merged commit on
mainand push the tag. -
Approve the deployment if the
pypienvironment has required reviewers: Actions, then thereleaserun, then Review deployments.
For a release candidate, do the same with __version__ = "0.2.0rc1" and the tag
v0.2.0rc1. Release candidates are published to TestPyPI only.
Recover from a failed run¶
-
Failed in
build. Nothing was published. Delete the tag, fix the problem, tag again: -
Failed with
invalid-publisher. Correct the Trusted Publisher registration on PyPI (owner, repository name, workflow file namerelease.yml, environment name), then re-run the failed job. Do not re-tag. -
Failed after a publish job succeeded. The version is spent. Re-run the failed job. Do not re-tag and do not rebuild.
Withdraw a bad release¶
Yank it. Do not delete it. A yanked release stays installable for anyone who pinned it exactly, so existing lockfiles keep working, and resolvers stop selecting it for unpinned installs. Deleting breaks pinned users and still does not free the version number.
- On PyPI: Manage project, Releases, the bad version, Options, Yank, with a reason. pip shows the reason to anyone installing that version.
- Ship the fix as the next patch version through the normal process, and record it under Fixed in the changelog, naming the yanked version.
- Edit the GitHub Release for the bad tag: prefix the title with
[YANKED], say why, and untick Set as the latest release. Leave the git tag in place.
Delete a release only if it contains something that must not be public, such as a committed secret, and rotate the secret first.
Verify¶
A release candidate on TestPyPI. TestPyPI does not mirror PyPI, so the dependencies must come from the real index:
python -m venv /tmp/td-verify && . /tmp/td-verify/bin/activate
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ "typedecide==0.2.0rc1"
cd /tmp # not the repo, or Python imports src/ and not the installed wheel
python -c "import typedecide; print(typedecide.__version__)"
typedecide --help
python -c "import sys, typedecide; assert 'torch' not in sys.modules"
A final release on PyPI.
python -m venv /tmp/td-verify && . /tmp/td-verify/bin/activate
pip install "typedecide==0.2.0"
cd /tmp && python -c "import typedecide; print(typedecide.__version__)" && typedecide --help
- The PyPI project page shows the new version with a rendered README, and both a
.tar.gzand a.whlare listed. - The GitHub Releases page has the release, with the changelog section as its body and both files attached.
- The documentation site has rebuilt from
main(docs.ymlruns on every push tomainthat touches the package orweb/).
One-time setup¶
Until this is done, the publish jobs fail with invalid-publisher. It is done once per
index, by the account that will own the project.
| Where | What |
|---|---|
| PyPI, Publishing, Add a new pending publisher (GitHub tab) | Project typedecide, owner ardada2468, repository typedecide, workflow release.yml, environment pypi |
| TestPyPI, the same page | The same values with environment testpypi |
| GitHub, Settings, Environments | Create pypi with required reviewers and a tag rule v*. Create testpypi with the same tag rule |
| GitHub, Settings, Rules (recommended) | A tag ruleset on v* restricting who can create, update and delete release tags |
Every field of the publisher registration is matched exactly against the OIDC token
GitHub issues. The workflow name is the file name only, not its path. Do not add
secrets to either environment; there is nothing to store. If the GitHub repository is
not named typedecide, register its real name.