Skip to content

Releases ship without PEP 740 attestations: uv publish uploads them but does not create them #13

Closed

Description

@AlexeyShalaev

Versions: python-library-template 8ebf45d, Copier 9.18.2, uv 0.13.1

template/.github/workflows/publish.yml.jinja:71-74 builds with uv build and uploads with uv publish. uv uploads PEP 740 attestations it finds next to the distributions, but it does not create them. The uv packaging guide says: "uv publish does not currently generate attestations; attestations must be created separately before publishing." The only related flag in uv publish --help is --no-attestations, which turns that upload off. As a result, no library release has provenance on PyPI, even though the publish job already has the OIDC token Sigstore needs to sign one.

Reproduction

for spec in "idempotency-kit 0.4.1 idempotency_kit-0.4.1-py3-none-any.whl" \
            "idempotency-kit 0.4.1 idempotency_kit-0.4.1.tar.gz" \
            "pypi-attestations 0.0.30 pypi_attestations-0.0.30-py3-none-any.whl" \
            "pypi-attestations 0.0.30 pypi_attestations-0.0.30.tar.gz"; do
  set -- $spec
  curl -s --retry 3 -o /dev/null -w "$1 $3 %{http_code}\n" \
    -H 'Accept: application/vnd.pypi.integrity.v1+json' "https://pypi.org/integrity/$1/$2/$3/provenance"
done
curl -s -H 'Accept: application/vnd.pypi.integrity.v1+json' \
  https://pypi.org/integrity/idempotency-kit/0.4.1/idempotency_kit-0.4.1.tar.gz/provenance
uvx --from uv==0.13.1 uv publish --help | grep -A1 -- --no-attestations

pypi-attestations is a control here: it is a project that publishes with attestations.

Actual

idempotency-kit idempotency_kit-0.4.1-py3-none-any.whl 404
idempotency-kit idempotency_kit-0.4.1.tar.gz 404
pypi-attestations pypi_attestations-0.0.30-py3-none-any.whl 200
pypi-attestations pypi_attestations-0.0.30.tar.gz 200
{"message":"No provenance available for idempotency_kit-0.4.1.tar.gz"}
      --no-attestations
          Do not upload attestations for the published files [env: UV_PUBLISH_NO_ATTESTATIONS=]

The latest releases of clientwright (0.5.0), deadline-budget (0.1.3) and pg-partsmith (1.8.0) also return 404 for both files.

Why

An attestation lets anyone check that a file on PyPI was built by this repository's publish.yml at a given commit, for example with pypi-attestations verify pypi --repository https://github.com/bedrock-python/<lib> ..., and PyPI shows it on the file's page. With Trusted Publishing already in place, it costs one step.

Proposal

Either of these works:

  • Keep uv and sign before uploading. uvx pypi-attestations sign dist/* writes <file>.publish.attestation next to each distribution (that is the name in pypi_attestations._cli._sign), and uv publish finds and uploads those files.
  • Upload with pypa/gh-action-pypi-publish@release/v1 instead, which creates attestations by default under Trusted Publishing. Its skip-existing: true takes the place of --check-url.

After the first release, check the result with the same curl call as above.

Activity

  1. AlexeyShalaev commented on Oct 16, 2026

    @AlexeyShalaev
    MemberAuthor

    Plan: keep uv publish and sign first. In the publish job, after uv build:

    - name: Sign the distributions (PEP 740)
      run: uvx --from 'pypi-attestations>=0.0.30' pypi-attestations sign dist/*.whl dist/*.tar.gz

    pypi-attestations sign takes the ambient GitHub OIDC credential (sigstore.oidc.detect_credential(), before any interactive fallback) and writes <file>.publish.attestation next to each distribution. uv publish finds those files and sends them with the upload. The job already has id-token: write, so this needs no new permission. If PyPI rejects an attestation, it rejects the upload, so a broken signature fails the release instead of shipping without provenance.

    I decided against pypa/gh-action-pypi-publish. It is the reference path, but here it would replace the uploader and --check-url with a container action and skip-existing to get one feature. The signing step adds that feature and leaves the rest alone.

    In the PR I can check that uv publish attaches .publish.attestation files to the upload, using a local index that records the request. Signing itself needs a GitHub OIDC token, so the first release after the merge is the end-to-end check, with the curl from this issue.

  2. AlexeyShalaev commented on Oct 16, 2026

    @AlexeyShalaev
    MemberAuthor

    The fix is in #24. Testing it turned up one thing worth knowing: uv publish attaches only attestation files that are in its own file set. The template calls it without arguments, and the default dist/* includes them. uv publish dist/*.whl dist/*.tar.gz would upload without them, with no error. I put a comment in the workflow. I also ran the generated sign step on a hosted runner, and pypi-attestations verify accepted both attestations (details on the PR).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions