How to Publish to PyPI with Trusted Publishing
Trusted publishing lets GitHub Actions upload packages to PyPI without storing any secrets. Instead of creating an API token and pasting it into your CI settings, you tell PyPI which GitHub repository and workflow are allowed to publish. GitHub Actions then proves its identity to PyPI using an OIDC token that lives only for the duration of the workflow run. Why use trusted publishing for PyPI? explains what this protects against.
This guide covers GitHub Actions, the most common CI provider for Python projects. PyPI also supports trusted publishing from GitLab CI/CD, Google Cloud, and ActiveState.
Prerequisites
- A PyPI account
- A GitHub repository containing a Python package with a
pyproject.toml - uv installed (installation guide)
Configure PyPI to trust your repository
Log in to PyPI
Go to pypi.org and sign in.
Open the trusted publisher settings
For an existing project: On the Your projects page, click Manage on the project, then click Publishing in the project’s sidebar.
For a new project that has never been uploaded: Go to your account publishing settings and scroll to Create a new pending publisher. Enter the package name exactly as it will appear on PyPI. The first successful upload creates the project and converts the pending publisher into a normal one.
Fill in the GitHub details
Enter the following:
| Field | Value |
|---|---|
| Owner | Your GitHub username or organization |
| Repository name | The repository that will publish the package |
| Workflow name | The filename of the workflow, e.g. publish.yml |
| Environment name | Optional. If your workflow uses a GitHub Actions environment, enter its name here. |
Save
Click Add. PyPI will now accept uploads from that specific workflow.
Create the publish workflow
Create .github/workflows/publish.yml in your repository. The workflow uses two jobs: a build job that produces the distributions, and a publish job that uploads them. Only the publish job holds the credential:
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: astral-sh/setup-uv@v10.0.1
- run: uv build
- uses: actions/upload-artifact@v7.0.1
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write
environment:
name: pypi
url: https://pypi.org/p/<YOUR_PACKAGE_NAME>
steps:
- uses: actions/download-artifact@v8.0.1
with:
name: dist
path: dist/
- uses: astral-sh/setup-uv@v10.0.1
- uses: astral-sh/attest-action@v0.0.6
- run: uv publish --trusted-publishing alwaysA few things to note:
- Only the
publishjob grantsid-token: write.uv buildruns third-party build backends and their dependencies, any of which can execute code during the build. Keeping the OIDC permission out of thebuildjob means a malicious build dependency cannot request an OIDC token and mint a PyPI credential. Thebuildjob hands its output topublishthroughupload-artifact, andpublishrunsuv publishwith no build step. PyPA’s own pip release workflow splits the jobs the same way. permissions: id-token: writelets thepublishjob request an OIDC token from GitHub’s token service. Without it,uv publishhas no credential to present to PyPI.on: releasetriggers the workflow when you create a GitHub release. You can also trigger on tag pushes (on: push: tags: ["v*"]).astral-sh/attest-actionwrites a.publish.attestationfile next to each distribution indist/, signed with the same OIDC identity. These are digital attestations (PEP 740), the signed statements PyPI displays as Provenance.uv publishdoes not generate them itself, but it uploads any it finds indist/. How to publish Python packages with digital attestations covers the alternatives.uv publishdetects the GitHub Actions environment, requests an OIDC token, exchanges it with PyPI for a short-lived upload credential, and publishes the files indist/.--trusted-publishing alwaysmakes the step fail immediately if no OIDC token is available, instead of falling back to other credentials.- The
environmentblock is optional but recommended. GitHub environments let you add required reviewers, restrict which branches can deploy, and see deployment history. If you specify an environment here, use the same environment name in your PyPI trusted publisher configuration.
To rehearse the workflow against a throwaway index first, follow How to publish to TestPyPI with uv.
Publish a release
Tag your release
$ git tag v0.1.0
$ git push origin v0.1.0
Create a GitHub release
Go to your repository on GitHub, click Releases โ Draft a new release, select the tag you just pushed, and click Publish release.
Watch the workflow
Open the Actions tab. The publish workflow starts, and the uv publish step logs one line per file:
Publishing 2 files to https://upload.pypi.org/legacy/
Hashing example-0.1.0-py3-none-any.whl (1.4KiB)
Uploading example-0.1.0-py3-none-any.whl (1.4KiB)
Hashing example-0.1.0.tar.gz (635.0B)
Uploading example-0.1.0.tar.gz (635.0B)
Verify the upload
Open your project’s PyPI page and select the new release. Its file list shows a Provenance entry for each file, naming the repository and workflow that published it. If the entry is missing, the attest-action step did not run before uv publish, or the job lacks id-token: write.
Revoke old API tokens
If you were previously using an API token to publish, go to your PyPI account settings and delete the token. There is no reason to keep a long-lived secret around once trusted publishing is working.
Troubleshooting
Failed to obtain token for trusted publishing / No OIDC token discovered: uv could not get an OIDC token from GitHub. Check that permissions: id-token: write is set on the publish job and that the step runs on GitHub Actions, not a self-hosted runner without OIDC.
invalid-publisher from PyPI: the OIDC token is valid but matches no configured publisher. Verify that the owner, repository name, and workflow filename in your PyPI publisher configuration match the actual repository and the file in .github/workflows/, and that the environment name matches or is blank on both sides.
Pending publisher not matching: The package name in the pending publisher configuration must exactly match the name field in your pyproject.toml (PyPI normalizes names, so hyphens and underscores are equivalent).
Renamed or transferred repository: Renaming the GitHub repository, transferring it to a new owner, or renaming the workflow file breaks trusted publishing. The OIDC token then carries an owner, repository, or workflow value that no longer matches the trusted publisher on PyPI, and PyPI returns invalid-publisher. Update the publisher in your PyPI project’s Publishing settings so the owner, repository, and workflow filename match the current names, removing the stale entry and adding a new one if the values changed.
Reusable workflows: PyPI does not currently support reusable GitHub Actions workflows as the trusted workflow. The workflow file that calls uv publish must be defined directly in your repository.