Publishing Python packages to PyPI has become more secure and automated thanks to the Trusted Publishers mechanism. I recently adopted this for my package sollu, and it was the second time where I got to use the Trusted Publishers mechanism to publish a Python package, the first being my image caption CLI tool imgcap. The trusted publishers mechanism allows us to publish these Python packages, eliminating the need to generate an API token, which will be used for authentication when publishing to PyPI.
While the mechanism is built upon OpenID Connect (OIDC), you don’t need an OIDC expert. A surface-level understanding of how publishing with OpenID Connect works is more than sufficient; the tools abstract away the complexity.
The entire process of publishing your package to PyPI via this mechanism takes three steps.
- Tell PyPI which Github repo is trusted
- Configuring your Github publish workflow
- Publish release to Github triggering the workflow.
Adding Trusted publisher on PyPI
When publishing a new package , you get to use the pending publishers
mechanism. I found this particularly useful because it allows you to reserve a package name early on. (Naming things while building seems way bigger a problem then solving the actual issue sometimes).
You can create a new pending publisher here : https://pypi.org/manage/account/publishing/
This is an example of how i filled the form for sollu. You’ll need to provide the PyPI package name, the GitHub repository owner, and the workflow name (the name of the YAML file in stored in .github/workflows
).
Optionally you can specify the environment as well (in the above example i have set it to pypi
), it is advised to set an environment since it is used to describe the deployment target. Additionally if needed we can configure the environment with protection rules and secrets as well.These act as specific conditions that need to be met before the job can proceed.
Adding trusted publisher to an existing package
Okay, so the section above walks through setting it up for a brand new package using that handy pending publisher
thing to reserve the name and set up the trust simultaneously. But what if you’ve already got a package happily living on PyPI and want to switch it over to use Trusted Publishers and ditch the old API token method for future releases?
It’s not hugely different, thankfully. Since your package already exists, you won’t use that initial pending publisher
form. Instead, you’ll add the trusted publisher directly via the management page for your existing project on PyPI – you just tell it the same details: your GitHub repo owner, repository name, and the name of your publishing workflow file.
The core idea remains exactly the same though: you’re telling PyPI which specific GitHub repo and workflow it should trust to publish releases for this package. Once you’ve added it there, the rest of the process i.e setting up that matching GitHub environment and configuring your workflow YAML to use the pypa/gh-action-pypi-publish
action without tokens is exactly as described in the steps that follow.
For adding a publisher to a package that’s already been around for a while, the official instructions linked here are the best guide: instructions for adding the mechanism to an existing package.
Creating Github actions environment
Since we specified the environment name on PyPI, we now need to create a matching environment in your GitHub repository. This is done by moving to the settings area of the repository .
Add the name of your environment which you would mentioned in the form.(As mentioned in the example form , will be adding pypi
over here)
Configure you workflow
Now that PyPI trusts your GitHub repo, it’s time to configure a GitHub Actions workflow that does three things
- Runs tests on multiple Python versions
- Builds the package
- Publishes it securely to PyPI using Trusted Publishers
This is the workflow which I’m using
name: Publish Python Package
on:
release:
types: [created]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -e '.[test]'
- name: Run tests
run: pytest
build:
runs-on: ubuntu-latest
needs: [test]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build - name: Build distribution
run: python -m build
- name: Upload distribution
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
publish-pypi:
runs-on: ubuntu-latest
needs: [build]
environment: pypi
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Job 1: Run Tests Across Multiple Python Versions
You want to make sure your package works on different Python environments before publishing it. GitHub’s matrix strategy helps run the same job with multiple Python versions in parallel.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -e '.[test]'
- name: Run tests
run: pytest
For each version it will
- Check your code
- set up python
- install your dependencies
- run your test with pytest
Job 2: Build the package
Once all tests pass, the next job (build
) kicks in. It only runs after the tests succeed thanks to this line
needs: [test]
It does the following
- Setups up python version accordingly
- Installs the
build
package - Build both the source and wheel distribution
- Uploads the build artifacts to share with next job
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build - name: Build distribution
run: python -m build
- name: Upload distribution
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/
Job 3: Publish to PyPI
Finally, the publish-pypi
job takes the built package and securely publishes it to PyPI using the Trusted Publishers OIDC mechanism. Notice the environment: pypi
line connecting this job to the environment we configured, and crucially the permissions: id-token: write block
. This permission allows the GitHub Actions runner to fetch an OIDC token, which PyPI validates against your Trusted Publisher configuration, proving that the request is coming from this specific workflow in this specific repository.
It includes
id-token
: grants the permission needed to authenticate via OIDC- uses:
pypa/gh-action-pypi-publish@release/v1
which is a secure, official PyPA GitHub Action
jobs:
publish-pypi:
runs-on: ubuntu-latest
needs: [build]
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Note : The permissions
block there is essential and a must. Crucially, you don’t need to set up any PyPI API tokens or secrets within your GitHub workflow. The Trusted Publisher configuration on PyPI handles the authentication via OIDC.
Publishing a release
With the build and publish steps automated, the final pre-release task is to ensure your package’s version is correctly updated in the pyproject.toml
file. After specifying the version properly, proceed to create a new release on Github. Click on draft new release, and specify the tag as per the version of the package.
Creating the release, triggers the workflow and publishes to PyPI.
Here is the final package release : https://pypi.org/project/sollu/
And just like that, your package is live on PyPI, published securely and automatically without ever having to handle sensitive API tokens. Setting this up for sollu
(and seeing it work for imgcap
before that) really felt like a solid upgrade in how to get things out there.
It takes a little bit of initial setup, sure, but the peace of mind and the streamlined process you get in return is totally worth it. If you’re managing Python packages, seriously consider making the switch to Trusted Publishers it simplifies things quite a bit.