From 16b3c1783fbba6f67e7d1e38b04003b84fc9bc9d Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Mon, 2 Nov 2026 11:19:57 +0100 Subject: [PATCH] Add the release workflow: PyPI trusted publishing and the GitHub release (cherry picked from commit 01124af31b695a68d90d1abb48f6ae8d48e5a04e) --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++-------- AGENTS.md | 25 ++++++++++++++---------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b85a24..90d1d3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,10 +5,11 @@ # The distributions are the four libraries easyssf-core, easyssf-receiver, easyssf-test and # easyssf-receiver-fastapi (the conformance harness and the examples carry the classifier # "Private :: Do Not Upload" and are not built here), built once, tested, checked, and uploaded -# with trusted publishing: no stored API token, the publish job's GitHub OIDC token is exchanged -# for a PyPI token that lives a quarter of an hour. The trusted publisher of each of the four -# projects on PyPI names this repository, this workflow file and the environment "pypi". Only the -# publish job may request the OIDC token, as PyPI recommends; it runs no code of the repository. +# with trusted publishing: no stored API token, each publish job's GitHub OIDC token is exchanged +# for a PyPI token that lives a quarter of an hour and reaches its one project. The trusted +# publisher of each of the four projects on PyPI names this repository, this workflow file and the +# environment "pypi-". Only the publish jobs may request the OIDC token, as PyPI +# recommends; they run no code of the repository. name: Release # a tag push carries no commit for GitHub to title the run with, so name it by the tag run-name: Release ${{ github.ref_name }} @@ -119,14 +120,22 @@ jobs: path: notes.md publish: - name: Publish to PyPI + name: Publish ${{ matrix.distribution }} to PyPI needs: build runs-on: ubuntu-latest timeout-minutes: 15 - # the GitHub environment the trusted publishers name, and where a required reviewer can be set + strategy: + # one upload that fails (a project without its trusted publisher yet) leaves the others be; + # "Re-run failed jobs" retries it and then creates the GitHub release + fail-fast: false + matrix: + # the DISTRIBUTIONS of the build job; the env context is not available to a matrix + distribution: [easyssf-core, easyssf-receiver, easyssf-test, easyssf-receiver-fastapi] + # one GitHub environment per distribution, which its trusted publisher names: PyPI accepts a + # pending publisher only once per owner, repository, workflow and environment environment: - name: pypi - url: https://pypi.org/project/easyssf-receiver/ + name: pypi-${{ matrix.distribution }} + url: https://pypi.org/project/${{ matrix.distribution }}/ permissions: # the OIDC token that trusted publishing exchanges for the PyPI token id-token: write @@ -135,11 +144,22 @@ jobs: with: name: dist path: dist + - name: Select the files of the distribution + # the version in the pattern keeps easyssf_receiver- from matching easyssf_receiver_fastapi- + run: | + version=${GITHUB_REF_NAME#v} + name=${DISTRIBUTION//-/_} + mkdir upload + mv "dist/$name-$version-py3-none-any.whl" "dist/$name-$version.tar.gz" upload/ + ls -l upload + env: + DISTRIBUTION: ${{ matrix.distribution }} - name: Upload to PyPI # with digital attestations (PEP 740); skip-existing makes a rerun after a partial upload # idempotent, PyPI refuses to replace a file it has uses: pypa/gh-action-pypi-publish@release/v1 with: + packages-dir: upload/ skip-existing: true github-release: diff --git a/AGENTS.md b/AGENTS.md index 1106fbf..88751dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,18 +194,23 @@ Releases go to [PyPI](https://pypi.org): the four libraries `easyssf-core`, `eas a pushed tag `v`. It checks that the tag matches the version of every member of the workspace and takes the release notes from the `CHANGELOG.md` section of that version. It then tests, lints, builds the four, checks that there is a wheel and an sdist of each at that version -(`twine check --strict`), and installs the wheels in a clean environment. The `publish` job -uploads them, and the `github-release` job creates the GitHub release with the files attached, -marked as a prerelease when the version has a label (`0.8.0rc1`). +(`twine check --strict`), and installs the wheels in a clean environment. The `publish` matrix +uploads each library in a job of its own, and the `github-release` job creates the GitHub release +with the files attached once all four are up, marked as a prerelease when the version has a label +(`0.8.0rc1`). The upload uses [trusted publishing](https://docs.pypi.org/trusted-publishers/): no API token is -stored. Only the `publish` job has `id-token: write`, and it runs in the GitHub environment `pypi`. -`pypa/gh-action-pypi-publish` exchanges the job's OIDC token for a short-lived PyPI token and -uploads with digital attestations (PEP 740). Each of the four projects on PyPI has a trusted -publisher that names owner `easyssf`, repository `easyssf-python`, workflow `release.yml` and -environment `pypi`. Before a project's first release, it is a "pending publisher" in the PyPI -account (Your projects, Publishing). The first upload creates the project and turns it into a -regular publisher. A required reviewer on the `pypi` environment holds a release until approved. +stored. Only the `publish` jobs have `id-token: write`, each in the GitHub environment +`pypi-` (`pypi-easyssf-core`, ...). `pypa/gh-action-pypi-publish` exchanges the job's +OIDC token for a short-lived PyPI token for that one project and uploads with digital attestations +(PEP 740). Each of the four projects on PyPI has a trusted publisher that names owner `easyssf`, +repository `easyssf-python`, workflow `release.yml` and its environment. One environment per +library is needed because PyPI accepts a pending publisher (the publisher of a project that does +not exist yet, in the PyPI account under Your projects, Publishing) only once per owner, +repository, workflow and environment, and holds at most three pending publishers at a time. The +first upload creates the project and turns its pending publisher into a regular one. A new +library therefore needs a GitHub environment and a pending publisher before its first release. A +required reviewer on the environments holds a release until approved. A release goes through a commit like every other change: