Releasing#
For maintainers. Releases are published by .github/workflows/release.yml,
triggered by pushing a v* tag. That is the only path to PyPI; there is no
separate local publishing route. You drive it from your machine with git and
gh, as below.
The version lives in one place#
src/eegprep/__init__.py:
__version__ = "0.5.0"
pyproject.toml declares dynamic = ["version"] and reads that attribute, so
there is no version string to edit in the eegprep pyproject.toml.
eegprep-lean carries the same version in two places, and both must equal
__version__: packages/eegprep-lean/src/eegprep_lean/__init__.py and
version in packages/eegprep-lean/pyproject.toml. The joint release
below exits before it builds if those three strings differ.
Both packages#
scripts/release_packages.py builds eegprep and eegprep-lean with that one
version. With no flags it builds both and does not upload:
python scripts/release_packages.py
--publish checks that both packages have built artifacts and that
UV_PUBLISH_TOKEN is set, then uploads eegprep-lean and eegprep in that
order. It does not upload eegprep unless lean can be uploaded too. Lean has
no trusted publisher, so a GitHub Actions token is not enough for this
command. --check compares the versions and does not build. A v* tag
in GITHUB_REF, or RELEASE_TAG, must name the same version.
.github/workflows/synced-release.yml runs the script without --publish.
That job builds and verifies. It does not upload. release.yml still
publishes the eegprep project when a v* tag is pushed. Uploading
eegprep-lean needs a trusted publisher for that project, which is not
configured; lean-wheel.yml builds the lean wheel and does not publish it.
Release from your machine#
1. Dry run first. This runs the whole pipeline except publishing, so you find problems before a tag exists:
gh workflow run release.yml --ref master -f dry_run=true
gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
2. Bump the version in src/eegprep/__init__.py, then commit and land it on
master:
git commit -am "release: 0.5.0"
git checkout master && git merge --no-ff develop && git push origin master
Tip
If master is strictly behind develop, git push origin develop:master
fast-forwards it without checking out master. Useful when you have local
changes — for example a modified src/eegprep/eeglab submodule — that would
block the checkout.
3. Tag and push. Pushing the tag starts the release:
git tag -a v0.5.0 -m "Release version 0.5.0"
git push origin v0.5.0
4. Watch it. Nothing reaches PyPI unless every check passes:
gh run watch "$(gh run list --workflow=release.yml --limit 1 --json databaseId --jq '.[0].databaseId')" --exit-status
If it fails, inspect and fix:
gh run view --log-failed
5. Add a section to Changelog.
What the workflow does#
verify —
ruff check,ruff format --check,ty check, and the full test suite withEEGPREP_SKIP_MATLAB=1.build —
uv build, then checks that the tag matches__version__, that the vendored EEGLAB checkout is absent from both artifacts, that packaged data (ICLabel weights, help resources) is present, thattwine check --strictpasses, and that the built wheel installs and imports.publish — uploads to PyPI using Trusted Publishing, so no API token is stored in the repository.
github-release — creates the GitHub release with the artifacts attached.
Note
Trusted Publishing needs one-time setup on PyPI under
Manage project → Publishing → Add a new publisher: owner sccn, repository
eegprep, workflow release.yml, environment pypi.
If a tag was pushed at a bad commit#
Fix the problem, then move the tag. This is safe only while the tag published nothing; once a version is on PyPI it can never be replaced.
git push origin :refs/tags/v0.5.0 # delete the remote tag
git tag -f v0.5.0 && git push origin v0.5.0
Docker image#
CI does not build Docker images. After the tag is published, build and push the
image, which also updates the pin in tools/hpc/main.pbs:
docker login
uv run python scripts/build_docker.py
Commit the updated pin. Use --no-push to build without publishing.
Warning
PyPI uploads are immutable. A version number can never be reused, even after the release is deleted. Always dry-run first.
Note
Build with uv build, never python -m build. The repository contains a
build/ output directory that shadows the build package on sys.path, so
python -m build fails with “‘build’ is a package and cannot be directly
executed” no matter what is installed.