Publish to PyPI with a trusted publisher instead of an API token - #39
Merged
Conversation
Merged
The release job authenticated with `secrets.PYPI_API_TOKEN`, a long-lived credential with upload rights to the project. It now mints a short-lived OIDC token that PyPI exchanges for those rights, so there is nothing stored to leak or rotate. Building and publishing are split into two jobs, following the GitHub starter workflow, so that the job holding `id-token: write` checks nothing out and runs none of the repository's own code: it downloads an artifact and uploads it. pypa/gh-action-pypi-publish goes from v1.4.2 to v1.14.2. The pinned version predates trusted publishing by four years and would ignore the OIDC token. actions/setup-python goes from v3 to v5. Adds a dependabot config for github-actions. Pinning to a SHA is the safer way to depend on a third-party action, but only if something bumps it, and nothing did: that is how the publish action came to predate the authentication the workflow now needs. Inert until the matching publisher exists on PyPI. Until then a release would build and then fail to upload.
huangziwei
force-pushed
the
release-trusted-publishing
branch
from
August 18, 2026 10:06
fdbb6d2 to
bca5504
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 3 of the trusted-publishing switch — the half that lives in the repo. The
PyPI side is yours.
Do not merge before configuring PyPI
The workflow is inert until a release is published, and at that point it needs a
matching publisher to already exist. If a release goes out first, it builds and
then fails to upload. At
https://pypi.org/manage/project/rfest/settings/publishing/, add a GitHub
publisher with:
berenslabRFEstpython-publish.ymlpypi"Workflow name" is the filename, not the
name:inside the file. Nothing needsdoing on the GitHub side: referencing an environment in a workflow creates it.
Repository settings only come into it if you want protection rules on that
environment, such as requiring a reviewer before a publish can proceed.
The change
user: __token__/password: ${{ secrets.PYPI_API_TOKEN }}are gone. The jobmints a short-lived OIDC token instead, which PyPI exchanges for upload rights
to this project alone. Nothing is stored, so there is nothing to leak or
rotate, and a compromised repo secret stops being a way to publish
rfest.Building and publishing are now two jobs, matching the GitHub starter
workflow.
release-buildchecks out and builds, and uploadsdist/as anartifact;
pypi-publishdownloads that artifact and uploads it. The point ofthe split is that the job holding
id-token: write— the one that can publish —checks nothing out and runs none of this repository's code. It needs no
contentspermission at all.The action moves from v1.4.2 to v1.14.2. The SHA pinned in the tree is
v1.4.2, which predates trusted publishing by about four years, so adding
id-token: writeto it would have achieved nothing. The new pindc37677…isthe commit behind the
v1.14.2annotated tag; the tag object's own SHA doesnot resolve in
uses:.actions/setup-pythonalso goes v3 → v5.A dependabot config comes with it. Pinning to a SHA is the safer way to
depend on a third-party action, but it only works if something bumps it, and
nothing here did — that is exactly how the publish action ended up four years
behind the authentication method it needed. Monthly, github-actions ecosystem,
which also covers
checkout,setup-python,setup-uvand the artifactactions across both workflows.
The starter workflow uses
@release/v1, a moving branch, and would sidestep thestaleness problem differently. Pinning is kept because that is what the file
already did and because this job can publish; dependabot is the part that was
missing.
A side effect worth having: from v1.11 the action publishes PEP 740 digital
attestations by default under trusted publishing, so releases become verifiably
built by this workflow.
After the first successful release
Revoke the old credential in both places — the
PYPI_API_TOKENsecret in reposettings, and the token itself at https://pypi.org/manage/account/token/.
Deleting the secret alone leaves a working token in existence.
Testing
There is no dry run for trusted publishing, and PyPI will not accept a
re-upload of an existing version, so the first real proof is the 2.2.0 release
in #40. If you would rather rehearse, add a second publisher on TestPyPI and
point a throwaway workflow at
repository-url: https://test.pypi.org/legacy/.The
Testsworkflow runs on this PR and should be green; it says nothing aboutthese files, which only trigger on
release.