Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Producing attestations fails when attempting publishing from unsupported reusable workflows (when enabled) #283

Closed
efriis opened this issue Oct 30, 2024 · 58 comments
Labels
invalid This doesn't seem right question Further information is requested

Comments

@efriis
Copy link

efriis commented Oct 30, 2024

Howdy! We just had to turn off release attestations to get our releases running in langchain-ai/langchain. Haven't had a chance to dig deeper into attestation configuration in order to see what we need to fix, and thought I'd file an issue in case others run into the same thing!

langchain-ai/langchain#27765

Release Workflow

We run releases from the two workflow files edited in ^ that PR

  • _release.yml, which calls _test_release.yml, and then publishes to pypi
  • _test_release.yml for publishing to test.pypi

Error

We were seeing errors in your releases, e.g. in this workflow run: https://github.com/langchain-ai/langchain/actions/runs/11602468120/job/32307568692

Configuration of test release - 2 main things that look weird are /legacy/ and repository_url (we configure repository-url per docs)

Run pypa/gh-action-pypi-publish@release/v1
  with:
    packages-dir: libs/core/dist/
    verbose: true
    print-hash: true
    repository-url: https://test.pypi.org/legacy/
    skip-existing: true
    user: __token__
    repository_url: https://upload.pypi.org/legacy/
    packages_dir: dist
    verify_metadata: true
    skip_existing: false
    print_hash: false
    attestations: true
  env:
    POETRY_VERSION: 1.7.1
    PYTHON_VERSION: [3](https://github.com/langchain-ai/langchain/actions/runs/11602468120/job/32307568692#step:5:3).10

Logs - partially redacted

Checking libs/core/dist/langchain_core-0.3.14-py3-none-any.whl: PASSED
Checking libs/core/dist/langchain_core-0.3.14.tar.gz: PASSED
Notice: Generating and uploading digital attestations
Fulcio client using URL: https://fulcio.sigstore.dev
TUF metadata: /root/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev
TUF targets cache: /root/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev
Found and verified trusted root
Generating ephemeral keys...
Requesting ephemeral certificate...
Retrieving signed certificate...
Found <Name(O=sigstore.dev,CN=sigstore-intermediate)> as issuer, verifying if it is a ca
attempting to verify SCT with key ID xxx
Successfully verified SCT...
DSSE PAE: xxx
proposed: xxx
integrated: xxx
Transparency log entry created with index: 145293525
DSSE PAE: xxx
proposed: xxx
integrated: xxx
Transparency log entry created with index: 145293526
Showing hash values of files to be uploaded:
/github/workspace/libs/core/dist/langchain_core-0.3.14-py3-none-any.whl

SHA256: xxx
MD5: xxx
BLAKE2-256: xxx

/github/workspace/libs/core/dist/langchain_core-0.3.14.tar.gz

SHA256: xxx 
MD5: xxx
BLAKE2-256: xxx

/github/workspace/libs/core/dist/langchain_core-0.3.14-py3-none-any.whl.publish.attestation

SHA256: xxx
MD5: xxx
BLAKE2-256: xxx

/github/workspace/libs/core/dist/langchain_core-0.3.14.tar.gz.publish.attestation

SHA256: xxx
MD5: xxx
BLAKE2-256: xxx

Uploading distributions to https://test.pypi.org/legacy/
INFO     libs/core/dist/langchain_core-0.3.14-py3-none-any.whl (399.1 KB)       
INFO     libs/core/dist/langchain_core-0.3.14.tar.gz (320.2 KB)                 
INFO     password set by command options                                        
INFO     username: __token__                                                    
INFO     password: <hidden>                                                     
Uploading langchain_core-0.3.14-py3-none-any.whl
INFO     Response from https://test.pypi.org/legacy/:                           
         400 Bad Request                                                        
INFO     <html>                                                                 
          <head>                                                                
           <title>400 Could not verify the uploaded artifact using the included 
         attestation: Verification failed: 0 of 2 policies succeeded</title>    
          </head>                                                               
          <body>                                                                
           <h1>400 Could not verify the uploaded artifact using the included    
         attestation: Verification failed: 0 of 2 policies succeeded</h1>       
           The server could not comply with the request since it is either      
         malformed or otherwise incorrect.<br/><br/>                            
         Could not verify the uploaded artifact using the included attestation: 
         Verification failed: 0 of 2 policies succeeded                         
                                                                                
                                                                                
          </body>                                                               
         </html>                                                                
ERROR    HTTPError: 400 Bad Request from https://test.pypi.org/legacy/

Temporary Fix

langchain-ai/langchain#27765

We turned off attestations with attestations: false

@webknjaz
Copy link
Member

FYI reusable workflows are still not properly supported on the PyPI side. Some things happen to work almost by accident. There's a tracking issue somewhere in the Warehouse repo discussing this.

cc @woodruffw

@webknjaz webknjaz added invalid This doesn't seem right question Further information is requested labels Oct 30, 2024
@kesara
Copy link

kesara commented Oct 31, 2024

This worked for me when pubslishing for test PyPI, but I got the same error when the GHA tries to publish to real PyPI.

Test PyPI:

Checking dist/svgcheck-0.9.0.tar.gz: PASSED
Notice: Generating and uploading digital attestations
Fulcio client using URL: https://fulcio.sigstore.dev/
TUF metadata: /root/.local/share/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev
TUF targets cache: /root/.cache/sigstore-python/tuf/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev
Found and verified trusted root
Generating ephemeral keys...
Requesting ephemeral certificate...
Retrieving signed certificate...
Found <Name(O=sigstore.dev,CN=sigstore-intermediate)> as issuer, verifying if it is a ca
attempting to verify SCT with key ID dd3d306ac6c7113263191e1c99673702a24a5eb8de3cadff878a72802f29ee8e
Successfully verified SCT...

PyPI:

Checking dist/svgcheck-0.9.0.tar.gz: PASSED
Checking dist/svgcheck-0.9.0.tar.gz.publish.attestation: ERROR    InvalidDistribution: Unknown distribution format:                      
         'svgcheck-0.9.0.tar.gz.publish.attestation'

kesara added a commit to ietf-tools/svgcheck that referenced this issue Oct 31, 2024
@webknjaz
Copy link
Member

Thanks for the report. It would be useful to have a link to the log. And I even more useful if the job is restarted in debug mode.
Meanwhile, we'll have to wait for @woodruffw to see this and take a look from his side.

@kesara
Copy link

kesara commented Oct 31, 2024

@webknjaz
Copy link
Member

Thanks! I wonder where this is coming from..

Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'
Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'

@webknjaz
Copy link
Member

Extracting the artifacts from that job for history (they will be garbage-collected in a while) and further inspection:
artifacts.zip

@kesara
Copy link

kesara commented Oct 31, 2024

Thanks! I wonder where this is coming from..

Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'
Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'

Looks that's from a left over old test file, It's unrelated to this issue.

@woodruffw
Copy link
Member

woodruffw commented Oct 31, 2024

Thanks for the details @kesara! This one is weird: that error is coming from twine itself, not from PyPI.

From a quick look, I think what happened here is that I forgot to add a skip for attestations in twine check. I can root cause that a bit more in a moment.

Edit: Yep, that's what it looks like. I'll work on a fix now.

woodruffw added a commit to woodruffw-forks/twine that referenced this issue Oct 31, 2024
This fixes a bug that I accidentally introduced with
attestations support: `twine upload` learned the difference
between distributions and attestations, but `twine check`
didn't.

As a result, `twine check dist/*` would fail with
an `InvalidDistribution` error whenever attestations are
present in the dist directory, like so:

```
Checking dist/svgcheck-0.9.0.tar.gz: PASSED
Checking dist/svgcheck-0.9.0.tar.gz.publish.attestation: ERROR    InvalidDistribution: Unknown distribution format:
         'svgcheck-0.9.0.tar.gz.publish.attestation'
```

This fixes the behavior of `twine check` by having it
skip attestations in the input list, like it does with
`.asc` signatures. To do this, I reused the `_split_inputs`
helper that was added with pypa#1095, meaning that `twine upload`
and `twine check` now have the same input splitting/filtering
logic.

See pypa/gh-action-pypi-publish#283
for some additional breakage context.

Signed-off-by: William Woodruff <william@yossarian.net>
@woodruffw
Copy link
Member

pypa/twine#1172 should fix the behavior observed by @kesara!

For @efriis: @webknjaz is right that reusable workflow support is currently unfortunately limited on PyPI. My colleague @facutuesca and I are currently working on improving that, but in the mean time most users should stick to non-reusable workflows for Trusted Publishing.

sigmavirus24 pushed a commit to pypa/twine that referenced this issue Oct 31, 2024
This fixes a bug that I accidentally introduced with
attestations support: `twine upload` learned the difference
between distributions and attestations, but `twine check`
didn't.

As a result, `twine check dist/*` would fail with
an `InvalidDistribution` error whenever attestations are
present in the dist directory, like so:

```
Checking dist/svgcheck-0.9.0.tar.gz: PASSED
Checking dist/svgcheck-0.9.0.tar.gz.publish.attestation: ERROR    InvalidDistribution: Unknown distribution format:
         'svgcheck-0.9.0.tar.gz.publish.attestation'
```

This fixes the behavior of `twine check` by having it
skip attestations in the input list, like it does with
`.asc` signatures. To do this, I reused the `_split_inputs`
helper that was added with #1095, meaning that `twine upload`
and `twine check` now have the same input splitting/filtering
logic.

See pypa/gh-action-pypi-publish#283
for some additional breakage context.

Signed-off-by: William Woodruff <william@yossarian.net>
@efriis
Copy link
Author

efriis commented Oct 31, 2024

Got it. Would the recommendation be to combine the release and test release workflows just into a single one? I believe we separated them to make sure the test release step could never publish to pypi.org. When we registered our publishers, we have the test.pypi entries locked to _test_release.yml, and pypi entries locked to _release.yml.

@efriis
Copy link
Author

efriis commented Oct 31, 2024

appreciate the quick look @woodruffw !

@facutuesca
Copy link
Contributor

facutuesca commented Oct 31, 2024

Got it. Would the recommendation be to combine the release and test release workflows just into a single one? I believe we separated them to make sure the test release step could never publish to pypi.org. When we registered our publishers, we have the test.pypi entries locked to _test_release.yml, and pypi entries locked to _release.yml.

@efriis I think you can keep them in separate files, and make _release.yml depend on _test_release.yml by using the workflow_run event. That way, _test_release is no longer called by _release, but rather _release runs after _test_release is successful, which means _test_release is no longer used as a reusable workflow.

That way you should be able to keep the scope for the PyPI Trusted Publisher to just _release.yml, and still be able to run the test workflow before it.

@facutuesca
Copy link
Contributor

There's a tracking issue somewhere in the Warehouse repo discussing this

For visibility: pypi/warehouse#11096

@webknjaz
Copy link
Member

Got it. Would the recommendation be to combine the release and test release workflows just into a single one? I believe we separated them to make sure the test release step could never publish to pypi.org. When we registered our publishers, we have the test.pypi entries locked to _test_release.yml, and pypi entries locked to _release.yml.

@efriis you can achieve this by naming the environments pypi and testpypi, which is something I strongly recommend.

@webknjaz
Copy link
Member

webknjaz commented Nov 1, 2024

Thanks! I wonder where this is coming from..

Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'
Error in sitecustomize; set PYTHONVERBOSE for traceback:
ModuleNotFoundError: No module named 'coverage'

Looks that's from a left over old test file, It's unrelated to this issue.

@kesara I'm confused since it appears under the step running this action. And it's running in a container. Are you saying that you're leaving something on disk that ends up in the container as well?

Oh, wait… You're running the build within the same job as publishing. Giving OIDC privileges to your build tool chain is insecure and may lead to various impersonation / privilege elevation scenarios. Using trusted publishing this way is discouraged.

FWIW, this suggests that you may be leaving various incompatible artifacts in the dist/ folder, which is known to break twine in the past. So it's kinda related.

Try following the guide closely, only keeping the download step and the upload step in the publishing job: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#publishing-the-distribution-to-pypi. Perhaps, using a clean environment would help you avoid the issue of illegal files in dist/ breaking things.

@webknjaz
Copy link
Member

webknjaz commented Nov 1, 2024

@kesara if what William suggests is correct, https://github.com/marketplace/actions/pypi-publish#disabling-metadata-verification might be a workaround too.

@webknjaz
Copy link
Member

webknjaz commented Nov 1, 2024

@woodruffw so is this report caused only caused by reusable workflows? Can you confirm that the logs are consistent with this explanation, so I could go ahead and close the issue?

jnooree added a commit to seoklab/nurikit that referenced this issue Nov 1, 2024
@kesara
Copy link

kesara commented Nov 1, 2024

@webknjaz Thanks. I'll review publishing workflow.

kesara added a commit to ietf-tools/svgcheck that referenced this issue Nov 1, 2024
@woodruffw
Copy link
Member

@woodruffw so is this report caused only caused by reusable workflows? Can you confirm that the logs are consistent with this explanation, so I could go ahead and close the issue?

Yep, that's correct -- what's happening here is that attestations against the "build config" identity, which is the top-level initiating workflow. In non-reusable cases that might be something like release-event -> release.yml, but in the reusable workflow case it might be something like release-event -> release.yml -> pypi-publish.yml, where the attestation expects release.yml but the user has pypi-publish.yml configured as their trusted publisher.

mara004 added a commit to pypdfium2-team/pypdfium2 that referenced this issue Dec 19, 2024
@mara004
Copy link

mara004 commented Dec 19, 2024

pypdfium2 was also hit by the same issue as @ThiefMaster.
See https://github.com/pypdfium2-team/pypdfium2/actions/runs/12419035500/job/34673587139

Breakage in the publish step is quite disturbing for project maintainers TBH.
What's wrong with chaining TestPyPI/PyPI uploads? Isn't that how TestPyPI is meant to be used?
If not, some clearer guidance what downstreams should do/not do would be appreciated.

@woodruffw
Copy link
Member

What's wrong with chaining TestPyPI/PyPI uploads? Isn't that how TestPyPI is meant to be used?

TestPyPI isn't intended to be used in serial with PyPI -- it's an ephemeral index that you can use to test your workflows.

One common pattern I've seen with TestPyPI is to publish to it on push: main and similar events. But it's generally not a good idea (IMO) to publish to it during normal release flows, since it's not guaranteed to preserve account or package states (much less have any degree of uptime).

In terms of resources:

@mara004
Copy link

mara004 commented Dec 21, 2024

TestPyPI isn't intended to be used in serial with PyPI

Hmm, I guess the reason why we're doing that in pypdfium2 is to make sure all wheel tags are valid / accepted by PyPI (we're setting custom tags by overloading bdist_wheel's get_tag(), so this is relevant to us).
Unfortunately, twine check doesn't seem to warn about invalid wheel names (e.g. you can tag a wheel as maylinux [sic!] and twine says PASSED anyway).

I understand this isn't currently intentional, but I'm not yet convinced guarding the real PyPI with a prior TestPyPI upload is per se a bad idea. Consider the trouble it would cause to upload a broken release directly to PyPI.

@woodruffw
Copy link
Member

but I'm not yet convinced guarding the real PyPI with a prior TestPyPI upload is per se a bad idea.

There are at least two reasons why it's a bad idea:

  1. From a stability perspective: TestPyPI is not a permanent or stable index, and there's no guarantee that accounts on it are retained/not periodically deleted. If you assume that a TestPyPI upload will always succeed before the actual PyPI upload, you're opening yourself up to failures when TestPyPI decides to wipe its DB.
  2. From a security perspective: uploading the same artifacts in the same workflow to both indices means that there's no domain separation between the two. This is especially important in the context of attestations, which currently do not record the "intended" index for a file's upload, but will in the future. Doing this will make it harder for an attacker to perform "mix and match" confusion-style attacks between different indices.

Furthermore, as a corollary to (1): there's no guarantee that TestPyPI performs exactly the same (or even compatibly similar) upload checks as PyPI. This happens to be currently the case, but relying on it as a stronger version of twine check doesn't guarantee that "accepted to TestPyPI" means "will be accepted to PyPI."

Consider the trouble it would cause to upload a broken release directly to PyPI.

I agree that it's not ideal, but I'm curious whether (or why) the existing yank/deletion processes are insufficient for your purposes.

@asmeurer
Copy link

There's a pretty big disparity between how most users think of TestPyPI and how the packaging maintainers think of it (this is not the first instance I've noticed of this).

Most users think of it as a way to test the full release process in a sandbox before doing the final release. If this isn't what it is, you should really consider making it into that, because that's what's useful to people. The principle issue is that pushing to PyPI is a destructive process, since releases cannot be deleted once they are pushed, so it's useful to have something to run a "test deployment" on (I admit the current TestPyPI doesn't actually completely work for this because releases cannot be deleted from it either, but it's better than nothing).

@davidism
Copy link

davidism commented Dec 22, 2024

it's useful to have something to run a "test deployment" on

If you have "upload to test pypi" then "upload to pypi" as sequential steps in a single job, as long as uploading to test pypi passes you won't have time to verify it, it will move on to uploading to pypi. If uploading to test pypi fails, it would also fail on pypi, so you're not gaining anything from the check.

If you want to use it as a true "upload to test, then check things before uploading to pypi", then you would use two separate jobs, each with their own approval environment, which is exactly what the solution to this problem is too.

Most users think of it as a way to test the full release process in a sandbox

I was also in this camp (and may have unintentionally advocated it by using the pattern in Flask), but even before this issue I was starting to realize that it's not actually helping the way I thought it was.

@mara004
Copy link

mara004 commented Dec 22, 2024

If you have "upload to test pypi" then "upload to pypi" as sequential steps in a single job, as long as uploading to test pypi passes you won't have time to verify it, it will move on to uploading to pypi. If uploading to test pypi fails, it would also fail on pypi, so you're not gaining anything from the check.

If you want to use it as a true "upload to test, then check things before uploading to pypi", then you would use two separate jobs

No, if uploading to testpypi fails, our workflow stops there. You don't need two separate jobs for that.

@davidism
Copy link

davidism commented Dec 22, 2024

Right, but it would also fail uploading to PyPI, so there's no reason to see if it fails on test pypi first. There's also no guarantee that test pypi has the same version deployed as pypi, so it may not even fail in the same way. Test PyPI also doesn't have the same uptime/performance specs as PyPI, so it can fail even if the actual release would succeed.

I understand why people think failing a test pypi upload before trying pypi is good, I also thought that, but it's not actually helping.

@mara004
Copy link

mara004 commented Dec 22, 2024

Right, but it would also fail uploading to PyPI, so there's no reason to see if it fails on test pypi first

Yes there is, to notice and fix the issue before actually uploading to PyPI, to avoid having a broken release & tag lying around, which is dirty and confusing to downstreams.
In pypdfium2, the TestPyPI step has already saved us from this twice, which is good enough for me to keep it in.

@asmeurer
Copy link

Right, but it would also fail uploading to PyPI, so there's no reason to see if it fails on test pypi first. There's also no guarantee that test pypi has the same version deployed as pypi, so it may not even fail in the same way. Test PyPI also doesn't have the same uptime/performance specs as PyPI, so it can fail even if the actual release would succeed.

This is not true. I've had testpypi fail because of a broken build, which saved me because it never got to the real upload phase. The issue (IIRC) was that one release file that was uploaded was fine, and "created" the release, but a subsequent file had some issue.

Of course, as noted, there's no way to delete releases from testpypi, so after doing this I had to temporarily disable the testpypi to make the actual release work, so it's still not actually ideal for this particular use-case.

@asmeurer
Copy link

(And in case, it wasn't clear, if PyPI actually let you delete and redo broken releases, this wouldn't be nearly so big of an issue. I've never understood why there has always been such a strong pushback against allowing that)

@mara004
Copy link

mara004 commented Dec 22, 2024

This is not true. I've had testpypi fail because of a broken build, which saved me because it never got to the real upload phase. The issue (IIRC) was that one release file that was uploaded was fine, and "created" the release, but a subsequent file had some issue.

Exactly, same here.

(And in case, it wasn't clear, if PyPI actually let you delete and redo broken releases, this wouldn't be nearly so big of an issue. I've never understood why there has always been such a strong pushback against allowing that)

In this case, I disagree. It would be really confusing and insecure to allow re-uploading deleted releases on PyPI. A good testing ground is the better solution IMHO - then there's no need to open that pandora's box.
However, for that purpose, you can argue it should be allowed to re-upload a release on TestPyPI, because as you say, currently TestPyPI is only a one-time guard (with the next attempt having to go to PyPI directly), which is not ideal.

@ThiefMaster
Copy link

Random idea: Why not have atomic publishing? That way either the whole release fails or succeeds, but you'd never end up with something partially published where just some files are missing.

This would also help with cases where a release runs into quota limits (which happened e.g. for an uv release a few months ago, where they had to yank the whole release while waiting for a quota increase...).

@davidism
Copy link

davidism commented Dec 22, 2024

The issue (IIRC) was that one release file that was uploaded was fine, and "created" the release, but a subsequent file had some issue.

If the upload partially succeeds, but some of the files were bad, you can use skip-existing: true to continue uploading after you fix the build for those files. Only successfully uploaded files are kept, you can't end up in a situation where an invalid file is uploaded.

I've had a release partially succeed too. In fact, it got past test pypi, then failed on pypi. But I could fix it and keep going. Test pypi didn't help.

@woodruffw
Copy link
Member

. It would be really confusing and insecure to allow re-uploading deleted releases on PyPI. A good testing ground is the better solution IMHO

Yes, this is essentially the rationale: immutable files are an important security invariant on the index itself. I agree that the community would benefit from a better automatable testing ground, which TestPyPI itself can probably never be. I don't have super strong opinions on what that should be, other than that twine check is a great pre-existing UX that should probably provide stronger guarantees.

Random idea: Why not have atomic publishing? That way either the whole release fails or succeeds, but you'd never end up with something partially published where just some files are missing.

We're currently working on that, stay tuned 🙂

playpauseandstop added a commit to playpauseandstop/pre-commit-run-hook-entry that referenced this issue Dec 27, 2024
playpauseandstop added a commit to playpauseandstop/pre-commit-run-hook-entry that referenced this issue Dec 27, 2024
weiji14 added a commit to GenericMappingTools/pygmt that referenced this issue Jan 3, 2025
Have a dedicated build distribution job, and split the publish to TestPyPI and PyPI jobs, to workaround attestation file issue. Xref pypa/gh-action-pypi-publish#283
berquist added a commit to berquist/pymolresponse that referenced this issue Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
invalid This doesn't seem right question Further information is requested
Projects
None yet
Development

No branches or pull requests

10 participants