Skip to content

feat: releases ship with PEP 740 attestations - #24

Merged
AlexeyShalaev merged 1 commit into
masterfrom
fix/13-attestations
Oct 16, 2026
Merged

AlexeyShalaev merged 1 commit into
masterfrom
fix/13-attestations

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Member

The problem

uv publish uploads PEP 740 attestations it is given, but it does not create them. So no release in the org has provenance on PyPI (the integrity API returns 404 for idempotency-kit 0.4.1, clientwright 0.5.0, deadline-budget 0.1.3, pg-partsmith 1.8.0), even though the publish job already holds the OIDC token Sigstore signs with.

The decision

Keep uv publish and sign before it:

- run: uv build
- run: uvx --from 'pypi-attestations>=0.0.30' pypi-attestations sign dist/*.whl dist/*.tar.gz
- run: uv publish --check-url https://pypi.org/simple/<slug>/

pypi-attestations sign uses the ambient GitHub OIDC credential (sigstore.oidc.detect_credential() comes before any interactive flow) and writes <file>.publish.attestation next to each distribution. This needs no new permission: the job already has id-token: write. PyPI checks every attestation against the Trusted Publisher and rejects the upload if one does not match, so a broken signature fails the release. It does not ship without provenance.

I found one trap while testing, and the workflow comment now records it. uv publish attaches only attestation files that are in its own file set. The template calls it with no file arguments, and the default dist/* includes the .attestation files. Someone who changes that to uv publish dist/*.whl dist/*.tar.gz would upload the distributions without their attestations, with no error.

Rejected: pypa/gh-action-pypi-publish, which creates attestations by default. It would swap the uploader and --check-url for a container action and skip-existing to get this one feature, and the signing step gets the feature without that.

Evidence

A stand-in upload endpoint that records the form fields of each upload, for the distributions of a render of this branch. The attestation content was taken from PyPI's own provenance for pypi-attestations 0.0.30, because uv does not check the signature and PyPI does:

uv publish (default dist/*), no attestation files     whl: attestations=false   sdist: attestations=false
uv publish (default dist/*), .publish.attestation     whl: attestations=true    sdist: attestations=true   (uv 0.11.25 and 0.13.1)
uv 0.13.1 publish dist/*.whl dist/*.tar.gz            whl: attestations=false   sdist: attestations=false
uv 0.13.1 publish 'dist/*'                            whl: attestations=true    sdist: attestations=true

Signing needs a GitHub OIDC token, so I can't run it locally. I ran it on this PR with a temporary workflow that renders the template, builds, runs the generated sign step as written, and verifies the result against the workflow identity. The workflow was removed afterwards; the result is in the comment below.

actionlint with shellcheck is clean on the generated workflows. The generated gate is green.

After the first release with this change, check with the curl from #13: /integrity/<project>/<version>/<file>/provenance should return 200.

Closes #13

uv publish uploads attestations it is given but does not create them, so no
release in the org had provenance on PyPI, although the publish job already
holds the OIDC token Sigstore signs with.

The publish job now runs pypi-attestations sign over the built wheel and
sdist before uploading. It writes <file>.publish.attestation next to each,
and uv publish, with its default dist/* file set, sends them along. PyPI
verifies each attestation against the Trusted Publisher and rejects the
upload if one does not match, so a broken signature fails the release
instead of shipping without provenance.

Closes #13
@AlexeyShalaev

Copy link
Copy Markdown
Member Author

Signing probe result. The workflow was temporary and has been force-pushed away; the branch is back to d853683.

Run https://github.com/bedrock-python/python-library-template/actions/runs/38919502263. It rendered the template, ran uv build, extracted the sign step from the rendered publish.yml, and ran it as written:

uvx --from 'pypi-attestations>=0.0.30' pypi-attestations sign dist/*.whl dist/*.tar.gz
-rw-r--r-- 1 runner runner 10092 … demo_lib-0.0.0-py3-none-any.whl.publish.attestation
-rw-r--r-- 1 runner runner  9986 … demo_lib-0.0.0.tar.gz.publish.attestation

pypi-attestations verify attestation --identity https://github.com/bedrock-python/python-library-template/.github/workflows/sign-probe.yml@refs/pull/24/merge:

OK: dist/demo_lib-0.0.0-py3-none-any.whl.publish.attestation
OK: dist/demo_lib-0.0.0.tar.gz.publish.attestation
	Issuer: CN=sigstore-intermediate,O=sigstore.dev
Transparency Log (1 entries):
	Log Index: 3281037119

Ambient OIDC detection works on a hosted runner with id-token: write, and nothing prompts. The one part left untested is PyPI accepting the upload, and the first release covers that.

@AlexeyShalaev
AlexeyShalaev merged commit 28d0825 into master Oct 16, 2026
4 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the fix/13-attestations branch October 16, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant