Releasing¶
Altar releases every distribution in the repository together, at one version, from one tag. Publishing a GitHub
release runs .github/workflows/release.yml, which checks, tests, builds, and uploads all of them to PyPI. When
GitHub Actions cannot run, scripts/release_local.py runs the same steps from a maintainer's machine.
Distributions¶
| PyPI project | Source directory |
|---|---|
altar-identity |
identity/ |
altar |
altar/ |
altar-alphagenome |
bindings/alphagenome/ |
altar-alphamissense |
bindings/alphamissense/ |
altar-borzoi |
bindings/borzoi/ |
altar-cherimoya |
bindings/cherimoya/ |
altar-chrombpnet |
bindings/chrombpnet/ |
altar-clinvar |
bindings/clinvar/ |
altar-e2g-atlas |
bindings/e2g-atlas/ |
altar-enformer |
bindings/enformer/ |
altar-gnomad |
bindings/gnomad/ |
altar-gpnstar |
bindings/gpnstar/ |
altar-legnet |
bindings/legnet/ |
altar-opentargets-e2g |
bindings/opentargets-e2g/ |
altar-sei |
bindings/sei/ |
altar-spliceai |
bindings/spliceai/ |
They upload in dependency order. altar pins the exact altar-identity version, so altar-identity goes
first, then altar, then the bindings, which require altar. Model runtime images are not published to PyPI.
A release can publish only some bindings, as 0.1.0 does: it publishes altar-identity, altar,
altar-chrombpnet, altar-cherimoya, altar-alphamissense, and altar-opentargets-e2g. Every distribution
still gets the version and is built and checked; see Publish a subset.
One-time PyPI setup¶
Do this once, before the first release.
- Create a PyPI account and turn on two-factor authentication. PyPI requires it to upload.
- Optionally, create a PyPI organization for the lab. PyPI approves organizations by hand, which can take days. A project can move into an organization later, so a release does not need to wait for one.
- In the GitHub repository, open Settings → Environments and create an environment named
pypi. Every upload job inrelease.ymlruns in it. To guard uploads, add yourself as a required reviewer and limit deployments to tags matchingv*. -
On PyPI, open Your account → Publishing, and add a pending GitHub publisher for each project you publish: all 16 in the table above, or only the subset a release publishes. Use the same values for every project:
Field Value PyPI project name the project, e.g. altar-chrombpnetOwner kundajelabRepository name altarWorkflow name release.ymlEnvironment name pypiA pending publisher lets the workflow create the project on its first upload. It does not reserve the name, so release soon after adding them. If you created an organization, add the pending publishers from the organization's Publishing page instead.
Release with GitHub Actions¶
-
On a branch, set the new version everywhere it appears:
versionin each of the 16pyproject.tomlfiles;__version__inaltar/altar/__init__.pyandidentity/altar_identity/__init__.py;- the
altar-identity==<version>dependency inaltar/pyproject.toml; - the
altar>=X.Y.Z,<X.(Y+1)dependency in every binding, at every release. Its lower bound is the release version itself, because a binding in this repository can import names that only the samealtarrelease provides. For example, 0.2.0 needsaltar>=0.2.0,<0.3. versioninCITATION.cffandaltar/CITATION.cff.
The bindings currently require
altar>=0.1,<0.2, which equals the 0.1.0 floor. The next release must raise every binding's floor: the bindings already use names that 0.1.0 does not have, such asAnnotationContract. Both release paths refuse a binding whosealtarrange differs from>=X.Y.Z,<X.(Y+1), even when the range admits the release version.ALTAR_API_VERSIONinaltar/altar/plugins/manifest.pyis the plugin ABI version. Change it only when the plugin ABI changes, as described in the plugin ABI. 2. Relock the workspace and every runtime, because their lockfiles record these versions: -
In
CHANGELOG.md, give the version's section the release date, as## [X.Y.Z] - YYYY-MM-DD, and add its link reference at the bottom. -
Run the local gate and a release dry run, then merge the branch:
-
Tag the merge commit on
mainand push the tag: -
Publish a GitHub release for the tag, with the version's CHANGELOG section as its notes. Keep
--prereleasefor an alpha or beta, and drop it for a stable release.release.ymlpublishes every distribution, so a subset release cannot use this path.
Publishing the release starts release.yml. It fails before building if the tag and any version or internal
pin disagree. It
runs the identity, core, and binding tests, strict mypy, the boundary and public-API checks, and uv lock
--check for the workspace and every runtime. It then builds a wheel and an sdist of each distribution, runs
twine check --strict, and installs the wheels in clean environments. Last, it uploads altar-identity, then
altar, then the 14 bindings in parallel.
If an upload fails, for example because a project has no trusted publisher, fix the cause and re-run the failed jobs from the Actions page. Files already on PyPI are skipped.
Release from a local checkout¶
Use this path when GitHub Actions cannot run. It needs Linux or macOS, Python 3.11 or newer to run the script,
uv on PATH (the script runs uv 0.12.17 through uvx), and network access. If
your python3 is older, run the script as uvx uv@0.12.17 run --no-project --python 3.12 scripts/release_local.py.
-
Prepare the release as in steps 1 to 3 above and merge it. Tag the merge commit with an annotated tag as in step 5, and check out the tag in a clean clone or worktree:
-
Run the dry run:
The script stops at the first failure and uploads nothing. It:
- refuses modified tracked files, and untracked files inside any distribution's directory;
- refuses a tag that disagrees with any version or internal pin, including a binding whose
altarrequirement is not>=X.Y.Z,<X.(Y+1), and a CHANGELOG without the version's section; - syncs a temporary environment from
uv.lockand runsscripts/check_local.pyin it; - replaces
dist/with one wheel and one sdist per distribution, indist/<project>/, and records the commit, the gate result, and each file's SHA-256 indist/release-stamp.json; - refuses an sdist that holds any file git does not track, such as one a global gitignore hides;
- runs
twine check --stricton all 32 files; - installs
altar-identityandaltaralone and checks the public API, then installs every wheel in a clean environment, checks each version, imports each package, and loads each binding's entry points; - runs
uv publish --dry-runfor each distribution in upload order, which reports any file already on PyPI.
The gate takes several minutes.
--skip-gateskips it for a quick repeat, but a build made that way cannot be published. To publish only some bindings, add--onlyhere and in step 4, as described in Publish a subset. -
Create a PyPI API token at Account settings → API tokens. Before the first release the projects do not exist, so the token's scope must be the entire account. Read it into the environment without echoing it or saving it in shell history:
-
Publish the files the dry run built and checked:
--from-distskips the gate and the build. It acceptsdist/only if its stamp names the commit atHEAD, records a passed gate, and matches every file's SHA-256. It then repeats the other checks and uploads withuv publishin dependency order. Without--from-dist,--publishruns the gate and builds again.--publishrefuses to run without an annotated tag atHEADor without a dated CHANGELOG section. Only the uploadinguv publishprocesses receiveUV_PUBLISH_TOKEN; the script never prints the token or passes it as an argument. Files already on PyPI are skipped, so if an upload fails part-way, fix the cause and run the same command again. -
Delete the API token on PyPI.
- The projects now exist without a trusted publisher. For each project you published, open Manage → Publishing and add a GitHub publisher with the values from the one-time setup.
- Push the tag if you have not, and publish the GitHub release for it as in step 6 above. If Actions runs
release.yml, its upload jobs skip the files already on PyPI. Without the trusted publishers from step 6, those jobs fail, although nothing is lost. After a subset release, do not publish a GitHub release.
To rehearse on TestPyPI, use a TestPyPI account and token and add --index testpypi.
TestPyPI does not mirror third-party packages, so install from it with
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ altar.
Publish a subset¶
--only names the bindings to publish. altar-identity and altar are always published, because every
binding requires them. For 0.1.0:
python3 scripts/release_local.py v0.1.0 \
--only altar-chrombpnet altar-cherimoya altar-alphamissense altar-opentargets-e2g
python3 scripts/release_local.py v0.1.0 --publish --from-dist \
--only altar-chrombpnet altar-cherimoya altar-alphamissense altar-opentargets-e2g
The script still runs the gate and builds and checks every distribution, so the release is validated as a whole
and dist/release-stamp.json covers every file. It also installs only the published wheels in a clean
environment and checks their versions, imports, and entry points, which shows that the published set works on
its own. Only the uv publish step, dry run or real, is limited to the selection. It refuses an unknown name,
and a selection that leaves out a workspace distribution that a selected one requires.
The stamp records the selection. --from-dist refuses a --only selection, or its absence, that differs from
the build's, so pass the same --only to the dry run and to the publish. The summary lists every file that was
or would be uploaded, in upload order.
Do not publish a GitHub release for a subset release
release.yml uploads every binding in the workspace. If you publish a GitHub release for a subset release's
tag, its upload jobs fail for every project without a trusted publisher, or, where a pending publisher
exists, publish bindings you meant to hold back. Push the tag, but leave the GitHub release unpublished, and
add trusted publishers, pending or not, only for the projects you published.
After a release¶
-
Install from PyPI in a clean environment and check that the binding registers:
python3.12 -m venv /tmp/altar-release-check /tmp/altar-release-check/bin/pip install --no-cache-dir "altar==X.Y.Z" "altar-chrombpnet==X.Y.Z" /tmp/altar-release-check/bin/python -c \ 'import altar; from altar.models import get_model_plugin; print(altar.__version__, get_model_plugin("CHROMBPNET"))'A new release can take a few minutes to appear in the index. 2. Check that each project you published has a trusted publisher on PyPI, and that no API token is left. 3. Check that each project page on PyPI shows its README and links. 4. Start an
## [Unreleased]section at the top ofCHANGELOG.mdfor the next release.