Inline publish step into release.yml to fix PyPI attestations - #173
Merged
Conversation
…call chain PyPI Trusted Publishing attestations are incompatible with reusable workflow chains. When release.yml invoked publish.yml via workflow_call, the OIDC token's job_workflow_ref pointed at publish.yml while the Sigstore cert's Build Config URI pointed at release.yml; PyPI ties both to the same publisher and rejected the attestation as a 400. This bit v0.10.0 (auth passed, attestation verification failed) -- see pypa/gh-action-pypi-publish#166 and PyPI's docs on reusable workflows. PR #171 fixed the older 'stale checkout SHA' bug, which let the build correctly produce flights-0.10.0.* -- but exposed this attestation issue as the next layer. Adding release.yml as a second Trusted Publisher on PyPI doesn't help because PyPI still matches the OIDC token's job_workflow_ref (publish.yml) to the publisher and validates the cert (release.yml) against that publisher. Fix: - release.yml: inline the build/twine/upload steps as a new 'publish' job with environment: pypi and id-token: write. Run the test matrix first via test.yml workflow_call (no OIDC, so no attestation concern). - publish.yml: drop the workflow_call entry point and the 'ref' input. Keep the release: published and workflow_dispatch entry points -- those are now exclusively for manual recovery, TestPyPI smoke tests, and the manual GitHub Release fallback. - docs/guides/release.md: document the new architecture, the PyPI prerequisite (two Trusted Publishers: release.yml AND publish.yml), and add a troubleshooting entry for the attestation failure. Required PyPI config change: add release.yml as a Trusted Publisher (workflow=release.yml, environment=pypi). Keep publish.yml's existing publisher for manual paths.
Contributor
…st/lint steps Address review feedback on PR #173: 1. Duplicate-publish guard. release.yml now publishes inline AND creates the tag/GitHub Release via the bot. Guard publish.yml's pypi-publish job so a release event only auto-publishes when the release was created by a human (github.actor != github-actions[bot]); workflow_dispatch publishes only when the operator selects the pypi environment. This prevents a second, racing publish that would 400 with "File already exists" if a release-event path ever fires (e.g. if release.yml is switched to a PAT for branch protection). The workflow_dispatch recovery path and human-created-release fallback both still work. 2. Remove the single-version "Run tests" step (and the equally redundant ruff "Check code quality" step) from publish.yml's release-build job. Both are already covered by the test.yml matrix that gates the job via needs: [test] (lint.yml + railway-build + the 4-version pytest matrix). Apply the same cleanup to release.yml's inline publish job for consistency. release-build and the inline publish job now just build, twine-check, and publish.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refactors the release pipeline to inline the publish step directly into
release.ymlinstead of callingpublish.ymlviaworkflow_call. This resolves a critical issue where PyPI's Trusted Publishing attestations fail when the publish job is invoked through a reusable workflow chain.Problem
PyPI's Trusted Publishing requires that the OIDC token's
job_workflow_refand the Sigstore certificate'sBuild Config URIboth point to the same workflow. Whenrelease.ymlcallspublish.ymlviaworkflow_call, these fields point to different workflows, causing PyPI to reject the attestation with a 400 error.Key Changes
release.yml:workflow_callinvocation ofpublish.ymltestjob that runs the full test matrix against the new tag (viatest.yml)publishjob with build and publish steps previously inpublish.ymlpublish.yml:workflow_calltrigger and itsinputs(environment, ref)release: publishedandworkflow_dispatchrefparameter passing (now uses defaultgithub.ref)release.yml)docs/guides/release.md:Implementation Details
release.ymlincludes all necessary steps: checkout, uv setup, dependencies, code quality checks, build, package check, and PyPI uploadpublish.ymlremains available for manual recovery and TestPyPI testinghttps://claude.ai/code/session_01YE3uzAHtuh4Exz8ZStnhmF
Greptile Summary
This PR refactors the release pipeline to fix PyPI Trusted Publishing attestation failures that occurred when
release.ymlcalledpublish.ymlviaworkflow_call. The fix inlines the build and publish steps directly intorelease.ymland adds clear documentation explaining the architectural constraint.release.yml: Adds atestjob (viatest.yml) and an inlinepublishjob with all build/upload steps, eliminating the reusable-workflow chain that broke OIDC attestation matching.publish.yml: Dropsworkflow_calltrigger andrefinput, simplifying it to a standalone recovery/manual workflow — but retainsrelease: publishedwithout filtering out bot-created releases, creating a race withrelease.yml's publish job.docs/guides/release.md: Updated to document the independent-workflow architecture, two required PyPI Trusted Publishers, and a step-by-step recovery procedure.Confidence Score: 3/5
The core attestation fix in release.yml is correct, but publish.yml will still fire on every automated release and race to upload the same package version to PyPI.
Every time release.yml completes successfully it programmatically creates a GitHub Release, which triggers publish.yml's release:published handler. Both pipelines independently build and attempt to upload the same version; whichever loses the race gets a PyPI 400 File already exists rejection, producing a failed workflow run on every release. The fix is a one-line actor guard on the pypi-publish job.
.github/workflows/publish.yml — the release:published trigger and the pypi-publish job condition need an actor check to avoid the duplicate-upload race.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Dev as Developer participant ReleaseYML as release.yml participant TestYML as test.yml participant GHRelease as GitHub Release participant PublishYML as publish.yml participant PyPI as PyPI Dev->>ReleaseYML: workflow_dispatch (bump) ReleaseYML->>ReleaseYML: Bump version, commit, tag, push ReleaseYML->>GHRelease: gh release create vX.Y.Z GHRelease-->>PublishYML: release:published event (⚠️ race) ReleaseYML->>TestYML: "workflow_call (ref=tag)" TestYML-->>ReleaseYML: tests pass ReleaseYML->>PyPI: build + pypa/gh-action-pypi-publish PublishYML->>TestYML: workflow_call (no ref) TestYML-->>PublishYML: tests pass PublishYML->>PyPI: build + pypa/gh-action-pypi-publish (⚠️ duplicate!) PyPI-->>PublishYML: 400 File already existsComments Outside Diff (1)
.github/workflows/publish.yml, line 55-57 (link)publish.ymlThe
release-buildjob alreadyneeds: [test], wheretestcallstest.ymland runs the full 4-version matrix. TheRun testsstep here repeats a subset of those tests on a single Python version, adding several minutes to everypublish.ymlinvocation with no additional signal.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(release): inline build+publish into ..." | Re-trigger Greptile