Releasing#
How a new version of cosmicjellyfish gets to TestPyPI and PyPI. The short version: you bump
the version, run scripts/maintenance/release/release.sh, and push the tag it creates. The
tag is what starts the publishing workflow.
How it works#
Publishing is done by .github/workflows/main.yml using PyPI Trusted Publishing, so there
are no passwords or tokens stored in GitHub. Which index a release goes to depends only on
the tag name:
Tag |
Example |
What happens |
|---|---|---|
contains |
|
Publishes to TestPyPI and creates a GitHub pre-release. |
anything else |
|
Publishes to PyPI after a reviewer approves the |
Two rules the workflow enforces:
The tag must be exactly
vplus the version incosmicjellyfish/version.py. A mismatch fails the job on purpose.A version can be uploaded to an index only once, ever. If a release turns out to be broken, bump the version (
rc1torc2,2.0.0to2.0.1) instead of re-tagging.
The workflow can also be run by hand (Actions tab, “Main”, “Run workflow”) to publish the current version to TestPyPI without a tag.
One-time setup#
This is already done for the main repository, listed here so it can be recreated:
A pending (or active) trusted publisher on https://test.pypi.org/manage/account/publishing/ and on https://pypi.org/manage/account/publishing/: project
cosmicjellyfish, ownersantiagocasas, repositorycosmicjellyfish, workflowmain.yml, environmenttestpypiorpypirespectively.GitHub environments
testpypi(no rules) andpypi(deployment restricted to tagsv*, required reviewers).
Releasing step by step#
Merge everything you want to ship into
mainthrough a pull request, then update your checkout:git switch main git pull --ff-only
Bump the version in
cosmicjellyfish/version.py(_MAJOR,_MINOR,_PATCH,_SUFFIX) and theversion:line ofCITATION.cff. Release candidates use a suffix such asrc1; final releases use an empty_SUFFIX. AMetadataCI check fails if the two files disagree. Commit the bump through a pull request ifmainis protected.Run the release helper from a clean, up-to-date
main:scripts/maintenance/release/release.sh --dry-run # checks only, changes nothing scripts/maintenance/release/release.sh # guided, one step at a time
The script explains each step before doing it and waits for you. It refuses to start unless you are on
main, the working tree is clean,mainmatchesorigin/main, the tag does not exist yet, andversion.pyandCITATION.cffagree. It then:turns
## UnreleasedinCHANGELOG.mdinto a dated## [vX.Y.Z]section and commits only that file;creates the annotated tag
vX.Y.Z(the equivalent ofgit tag -a vX.Y.Z -m vX.Y.Z);prints the exact push commands and what to check afterwards.
It never pushes. Use
--yesto skip the pauses and questions,--helpfor the options.Push the commit and the tag, as printed by the script:
git push origin main git push origin vX.Y.Z
If
maindoes not accept direct pushes, open a pull request for the changelog commit first and tag the merged commit instead:git tag -a vX.Y.Z -m vX.Y.Z <merge-commit>.Watch the workflow at santiagocasas/cosmicjellyfish. For a final release, approve the
pypienvironment when GitHub asks.Check the published package in a fresh environment, away from your checkout:
# TestPyPI release candidate uv venv /tmp/cj-check --python 3.12 uv pip install --python /tmp/cj-check/bin/python \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/ \ --index-strategy unsafe-best-match \ cosmicjellyfish==2.0.0rc1 # PyPI final release uv pip install --python /tmp/cj-check/bin/python cosmicjellyfish==2.0.0 cd /tmp /tmp/cj-check/bin/python -c "import cosmicjellyfish; print(cosmicjellyfish.__file__)"
The printed path must be inside
/tmp/cj-check. The optionalsymbolic_pofkpackage is not on PyPI; install it separately if you want to testcode="symbolic".
A typical sequence is 2.0.0rc1 to TestPyPI, check the install, then repeat steps 2 to 6
with an empty _SUFFIX for 2.0.0 on PyPI.
When something goes wrong#
Nothing pushed yet. Undo the local tag, and the changelog commit if one was made:
git tag -d vX.Y.Z
git reset --hard HEAD~1 # only if the script made a changelog commit
The workflow failed before anything was uploaded (for example a failing check). Fix the
problem on main, then move the tag. Delete only the broken tag, locally and on GitHub,
and delete the matching GitHub release if one was created:
git tag -d vX.Y.Z
git push origin :refs/tags/vX.Y.Z
Then run release.sh again.
The upload already reached TestPyPI or PyPI. That version is used up. Fix the problem,
bump the version (rc2, or the next patch version), and release again. On PyPI you can
also yank a bad release from the project page, which hides it from new installs without
deleting it.