# 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 `rc` | `v2.0.0rc1` | Publishes to **TestPyPI** and creates a GitHub pre-release. |
| anything else | `v2.0.0` | Publishes to **PyPI** after a reviewer approves the `pypi` environment, then creates the GitHub release. |
Two rules the workflow enforces:
- The tag must be exactly `v` plus the version in `cosmicjellyfish/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 (`rc1` to `rc2`, `2.0.0` to `2.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
and on : project `cosmicjellyfish`, owner
`santiagocasas`, repository `cosmicjellyfish`, workflow `main.yml`, environment
`testpypi` or `pypi` respectively.
- GitHub environments `testpypi` (no rules) and `pypi` (deployment restricted to tags
`v*`, required reviewers).
## Releasing step by step
1. **Merge everything you want to ship into `main`** through a pull request, then update
your checkout:
```bash
git switch main
git pull --ff-only
```
2. **Bump the version** in `cosmicjellyfish/version.py` (`_MAJOR`, `_MINOR`, `_PATCH`,
`_SUFFIX`) and the `version:` line of `CITATION.cff`. Release candidates use a suffix
such as `rc1`; final releases use an empty `_SUFFIX`. A `Metadata` CI check fails if the
two files disagree. Commit the bump through a pull request if `main` is protected.
3. **Run the release helper** from a clean, up-to-date `main`:
```bash
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, `main` matches `origin/main`, the
tag does not exist yet, and `version.py` and `CITATION.cff` agree. It then:
- turns `## Unreleased` in `CHANGELOG.md` into a dated `## [vX.Y.Z]` section and commits
only that file;
- creates the annotated tag `vX.Y.Z` (the equivalent of `git tag -a vX.Y.Z -m vX.Y.Z`);
- prints the exact push commands and what to check afterwards.
It never pushes. Use `--yes` to skip the pauses and questions, `--help` for the options.
4. **Push the commit and the tag**, as printed by the script:
```bash
git push origin main
git push origin vX.Y.Z
```
If `main` does 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 `.
5. **Watch the workflow** at .
For a final release, approve the `pypi` environment when GitHub asks.
6. **Check the published package** in a fresh environment, away from your checkout:
```bash
# 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 optional `symbolic_pofk` package is
not on PyPI; install it separately if you want to test `code="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:
```bash
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:
```bash
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.