Repository navigation
Releases ship without PEP 740 attestations: uv publish uploads them but does not create them #13
Description
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-attestationspypi-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.attestationnext to each distribution (that is the name inpypi_attestations._cli._sign), anduv publishfinds and uploads those files. - Upload with
pypa/gh-action-pypi-publish@release/v1instead, which creates attestations by default under Trusted Publishing. Itsskip-existing: truetakes the place of--check-url.
After the first release, check the result with the same curl call as above.
Activity
AlexeyShalaev commentedon Oct 16, 2026
Plan: keep
uv publishand sign first. In thepublishjob, afteruv build:pypi-attestations signtakes the ambient GitHub OIDC credential (sigstore.oidc.detect_credential(), before any interactive fallback) and writes<file>.publish.attestationnext to each distribution.uv publishfinds those files and sends them with the upload. The job already hasid-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-urlwith a container action andskip-existingto get one feature. The signing step adds that feature and leaves the rest alone.In the PR I can check that
uv publishattaches.publish.attestationfiles 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 thecurlfrom this issue.AlexeyShalaev commentedon Oct 16, 2026
The fix is in #24. Testing it turned up one thing worth knowing:
uv publishattaches only attestation files that are in its own file set. The template calls it without arguments, and the defaultdist/*includes them.uv publish dist/*.whl dist/*.tar.gzwould upload without them, with no error. I put a comment in the workflow. I also ran the generated sign step on a hosted runner, andpypi-attestations verifyaccepted both attestations (details on the PR).