Releases are published to PyPI by the
release.yml workflow using Trusted Publishing (OIDC) — no API token is
ever stored in the repository.
-
PyPI trusted publisher. On PyPI, add a pending trusted publisher for the
dsoxlabproject (Account → Publishing, or the project's Publishing settings):- Owner:
stephrobert - Repository:
dsoxlab - Workflow:
release.yml - Environment:
pypi
- Owner:
-
GitHub environment. Create an environment named
pypiin the repository settings (Settings → Environments). Optionally add required reviewers and restrict it to tags matchingv*so every publish is gated by an approval.
-
Bump the version in
pyproject.toml, then refresh the lockfile withuv lock. There is nothing to bump insrc/dsoxlab/__init__.py:__version__is read from the installed package metadata precisely so it cannot drift frompyproject.toml. -
Move the
Unreleasedentries under a new## [X.Y.Z]heading in both CHANGELOG.md and CHANGELOG.fr.md, and update the comparison links at the bottom of each file. The project is bilingual: an English-only entry is an incomplete entry. -
Commit through a pull request and merge to
main. -
Wait for CI to be green on
main. The tag builds from that commit, and PyPI is final: a version number can never be republished. -
Run the local check before tagging:
python3 scripts/check-release.py
It replays the steps above offline: clean tree,
mainup to date, tag consistent withpyproject.toml, CHANGELOG section present in both languages,uv.lockaligned, version still free on PyPI, CI green. It prints the exact command to run once everything passes. The workflow guard only speaks after the tag is pushed, and it then has to be deleted on both sides. -
Tag the release and push the tag:
git tag -a vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Z
Pushing the tag triggers release.yml, which:
- builds the sdist and wheel with
uv buildand checks them withtwine, - records SLSA build provenance for both artifacts
(
actions/attest-build-provenance), - publishes to PyPI via OIDC, attaching PEP 740 build attestations,
- creates the GitHub Release with the CHANGELOG section for the tag, the
distributions, and
provenance.intoto.jsonl.
Two details of that pipeline are deliberate. The publish job runs no project
code and holds id-token: write only; and github_release runs after PyPI
succeeds, so no Release ever advertises a version that failed to upload.
-
Confirm the release actually landed, once the workflow is done:
python3 scripts/check-release.py --publiee
A green workflow is not proof that the version is installable. When 0.1.42 was published, the upload had received two
200 OKfrom PyPI and the project page answered, yethttps://pypi.org/simple/dsoxlab/— the only index pip and uv read to resolve a version — did not list it yet. During those minutes,uv tool install dsoxlabsilently installed the previous version.This check looks at the tag on
origin, at the Release assets (wheel, sdist, provenance), and at the simple index. It distinguishes "published but not served yet", which is a matter of waiting, from "absent from PyPI", which means the upload never happened despite a green job.One last trap it prints as a reminder: uv caches the index, so verifying by installing needs
uv tool install --force --refresh dsoxlab. Without--refresh, uv happily reinstalls the version it already knows.
provenance.intoto.jsonl is attached as a release asset on purpose: it is a
distinct artifact from the attestation recorded on GitHub's API, and it is the
one OpenSSF Scorecard's Signed-Releases control looks for. That control scores
the five most recent releases, so it only reaches its maximum once five
consecutive releases carry the asset.
Anyone can verify that a published artifact really came from this repository's workflow, and from which commit:
gh release download vX.Y.Z --repo stephrobert/dsoxlab --pattern '*.whl'
gh attestation verify dsoxlab-X.Y.Z-py3-none-any.whl --repo stephrobert/dsoxlabdsoxlab follows Semantic Versioning. Breaking changes to
the declarative contract (meta.yml / lab.yaml) or the CLI bump the major
version once the project reaches 1.0.