Releasing
A release is a tag. Pushing v<version> runs .github/workflows/release.yml, which publishes the
same two wheels two ways:
tag v0.2.0 ──► build ─────────────► pypi-runner ──► pypi ──► github-release
release_check.sh trusted trusted wheels + install.sh
install.sh, end publishing, publishing as release assets
to end norboten-runner norboten
The same tag also runs lab-publish.yml, which pushes every lab to GHCR as a signed OCI artifact.
What is published
| Artifact | Where | Used by |
|---|---|---|
norboten-<v>-py3-none-any.whl |
PyPI, the GitHub release | install.sh, norboten update |
norboten_runner-<v>-py3-none-any.whl |
PyPI, the GitHub release | a dependency of the above; copied into lab VMs |
install.sh |
the GitHub release; the site serves the copy from site/static |
curl -fsSL https://norboten.org/install.sh | sh |
The norboten wheel carries the image registry, the labs, the question banks, the journals and the
site's recordings ([tool.hatch.build.targets.wheel.force-include] in cli/pyproject.toml), so an
installed norboten has its whole catalogue with no checkout. paths.content_root() reads them; a lab
pulled into ~/.norboten/labs replaces the packaged copy only when its version is newer.
Cutting a release
-
Set the version in all three places —
cli/pyproject.toml,runner/pyproject.toml,cli/src/norboten/__init__.py— and runuv lock. -
make release-check. It fails if the three disagree, builds both wheels intodist/, installs them into a clean Python 3.12 environment outside the checkout, and checks thatnorboten --versionmatches and that the installed package finds its labs, banks and journals. It also shellchecksinstall.sh. -
Commit, merge to main, then:
git tag v0.2.0 git push origin v0.2.0
The build job repeats step 2 with the tag's version, then runs install.sh against the fresh
wheels (NORBOTEN_FIND_LINKS=dist) on a runner with no terminal — so it neither asks about QEMU nor
opens the TUI — and runs the installed norboten --version. Nothing is published unless it passes.
The jobs
| Job | Does | Needs |
|---|---|---|
build |
release_check.sh <version>, the installer end to end, uploads dist/*.whl |
— |
pypi-runner, pypi |
pypa/gh-action-pypi-publish with trusted publishing, one project each: no token is stored |
the pypi-runner and pypi environments, and a trusted publisher on PyPI for each project |
github-release |
gh release create v<version> with both wheels and install.sh, notes generated from the commits |
contents: write |
The release is created only after PyPI accepted the wheels, so the GitHub release never offers a version PyPI lacks.
Setup, once
PyPI. Add a pending trusted publisher for each project — this repository, workflow
release.yml — with a different environment per project: norboten-runner with environment
pypi-runner, and norboten with environment pypi. PyPI refuses a second pending publisher whose
owner, repository, workflow and environment all match the first, which is why the release has one
upload job per project. In GitHub, create both environments; add required reviewers to them if a
person should approve every upload.
Trying the installer
make release-check
NORBOTEN_FIND_LINKS="$PWD/dist" NORBOTEN_NO_LAUNCH=1 sh site/static/install.sh
On a clean Linux, in a container:
docker run --rm -it -v "$PWD:/src:ro" ubuntu:26.04 sh -c \
'apt-get update -qq && apt-get install -y -qq curl >/dev/null &&
NORBOTEN_FIND_LINKS=/src/dist sh /src/site/static/install.sh'
There is no /dev/kvm in a container, so the script says so; everything else is the real path.