Publishing with a Trusted Publisher
The easy way
Once you have a publisher configured, you can use the
PyPA's pypi-publish
action to publish your packages.
This looks almost exactly the same as normal, except that you don't need any explicit usernames, passwords, or API tokens: GitHub's OIDC identity provider will take care of everything for you:
jobs:
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
# Specifying a GitHub environment is optional, but strongly encouraged
environment: release
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
# retrieve your distributions here
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
If you're moving away from a password or API token-based authentication flow, your diff might look like this:
jobs:
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
+ # Specifying a GitHub environment is optional, but strongly encouraged
+ environment: release
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
steps:
# retrieve your distributions here
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- with:
- username: __token__
- password: ${{ secrets.PYPI_TOKEN }}
Note the id-token: write
permission: you must provide this permission
at either the job level (strongly recommended) or workflow level
(discouraged). Without it, the publishing action
won't have sufficient permissions to identify itself to PyPI.
Note
Using the permission at the job level is strongly encouraged, as it reduces unnecessary credential exposure.
Publishing to indices other than PyPI
The PyPA's pypi-publish
action also supports trusted publishing with other (non-PyPI) indices, provided
they have trusted publishing enabled (and you've configured your trusted
publisher on them). For example, here's how you can use trusted publishing on
TestPyPI:
- name: Publish package distributions to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
The manual way
Warning
STOP! You probably don't need this section; it exists only to provide some
internal details about how GitHub Actions and PyPI coordinate using OIDC.
If you're an ordinary user, it is strongly recommended that you use the PyPA's
pypi-publish
action instead.
The process for using an OIDC publisher is:
- Retrieve an OIDC token from the OIDC identity provider;
- Submit that token to PyPI, which will return a short-lived API key;
- Use that API key as you normally would (e.g. with
twine
)
GitHub is currently the only OIDC identity provider supported, so we'll use it for examples below.
All code below assumes that it's being run in a GitHub Actions
workflow runner with id-token: write
permissions. That permission is
critical; without it, GitHub Actions will refuse to give you an OIDC token.
First, let's grab the OIDC token from GitHub Actions:
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
NOTE: audience=pypi
is only correct for PyPI. For TestPyPI, the correct
audience is testpypi
. More generally, you can access any instance's expected
OIDC audience via the {index}/_/oidc/audience
endpoint:
$ curl https://pypi.org/_/oidc/audience
{"audience":"pypi"}
The response to this will be a JSON blob, which contains the OIDC token.
We can pull it out using jq
:
oidc_token=$(jq '.value' <<< "${resp}")
Finally, we can submit that token to PyPI and get a short-lived API token back:
resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}")
api_token=$(jq '.token' <<< "${resp}")
# tell GitHub Actions to mask the token in any console logs,
# to avoid leaking it
echo "::add-mask::${api_token}"
This API token can be fed into twine
or any other uploading client:
TWINE_USERNAME=__token__ TWINE_PASSWORD="${api_token}" twine upload dist/*
This can all be tied together into a single GitHub Actions workflow:
on:
release:
types:
- published
name: release
jobs:
pypi:
name: upload release to PyPI
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: deps
run: python -m pip install -U build
- name: build
run: python -m build
- name: mint API token
id: mint-token
run: |
# retrieve the ambient OIDC token
resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
oidc_token=$(jq '.value' <<< "${resp}")
# exchange the OIDC token for an API token
resp=$(curl -X POST https://pypi.org/_/oidc/github/mint-token -d "{\"token\": \"${oidc_token}\"}")
api_token=$(jq '.token' <<< "${resp}")
# mask the newly minted API token, so that we don't accidentally leak it
echo "::add-mask::${api_token}"
# see the next step in the workflow for an example of using this step output
echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}"
- name: publish
# gh-action-pypi-publish uses TWINE_PASSWORD automatically
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ steps.mint-token.outputs.api-token }}