Skip to content

How to Publish Python Packages with Digital Attestations

Digital attestations let PyPI record who published each file, not just what the file contains. Each attestation is a Sigstore-signed statement, defined by PEP 740 (index support for digital attestations), that binds a distribution’s hash to the CI workflow that uploaded it.

Attestations require trusted publishing: PyPI only accepts attestations signed by a trusted publisher identity from GitHub Actions, GitLab CI/CD, or Google Cloud. This guide shows the two ways to add them to a GitHub Actions workflow.

Prerequisites

Choose a publishing path

Two publishing steps produce attestations. The PyPA publish action signs and uploads in one step with no configuration. uv publish uploads attestations but does not generate them, so it needs astral-sh/attest-action run first. Pick the PyPA action if you have no other reason to use uv in the publish job; pick uv if you already publish with uv publish or want the same tool locally and in CI.

Both workflows split build and publish into separate jobs so that only the publish job holds id-token: write, the permission that mints both the upload credential and the signing certificate.

Publish with the PyPA publish action

Create .github/workflows/publish.yml:

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: pypa/gh-action-pypi-publish@release/v1

The action signs each file in dist/ with the workflow’s OIDC identity through Sigstore, then uploads the files and their attestations together. Attestations are on by default; no input is needed.

Publish with uv publish

Replace the publish job’s final step with attest-action followed by uv publish:

  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 always

attest-action signs every file matching dist/* (change the paths input for other locations) and writes a <distribution>.publish.attestation file beside each one. uv publish discovers those files and uploads them with their distributions:

$ ls dist/
example-1.0.0-py3-none-any.whl
example-1.0.0-py3-none-any.whl.publish.attestation
example-1.0.0.tar.gz
example-1.0.0.tar.gz.publish.attestation

Outside GitHub Actions, generate the same files with the pypi-attestations CLI (uvx pypi-attestations sign dist/*) from a job that holds a Sigstore-capable OIDC token, such as GitLab CI/CD with id_tokens. PyPI’s producing attestations page has the GitLab and Google Cloud recipes.

Tip

If uv publish targets a private index that rejects attestations, pass --no-attestations to skip uploading them.

To rehearse either workflow against a throwaway index first, follow How to publish to TestPyPI with uv.

Verify attestations on PyPI

Open your project’s PyPI page, select the new release, and check its file list. Each file shows a Provenance entry naming the repository and workflow that published it.

To verify a file cryptographically, run pypi-attestations against the published file and your repository. It downloads the file and its provenance through the PyPI Integrity API, checks that the signing identity matches the repository, and validates the signature:

$ uvx pypi-attestations verify pypi \
    --repository https://github.com/<org>/<repo> \
    pypi:<file>

For example, against attrs:

$ uvx pypi-attestations verify pypi \
    --repository https://github.com/python-attrs/attrs \
    pypi:attrs-25.1.0-py3-none-any.whl
OK: attrs-25.1.0-py3-none-any.whl

A wrong repository fails with Verification failed: provenance was signed by repository "python-attrs/attrs", expected "<org>/<repo>", and a file uploaded without attestations fails with Provenance for file "<file>" was not found. Both exit with status 1.

Record publisher identities in lockfiles

pylock.toml defines an attestation-identities table for recording each package’s publisher, but uv and pip do not write it yet; Why pylock.toml includes digital attestations explains what that table will make visible in code review once installers fill it in. An attestation proves who built a file, not what went into it; pair it with hash verification on the consuming side and a byte-identical build on the publishing side so anyone can rebuild the wheel and compare digests.

Last updated on