Skip to content

ci(pypi): publish from a top-level job so trusted publishing can match#41

Merged
dpezto merged 1 commit into
mainfrom
ci/pypi-publish-toplevel
Jul 25, 2026
Merged

ci(pypi): publish from a top-level job so trusted publishing can match#41
dpezto merged 1 commit into
mainfrom
ci/pypi-publish-toplevel

Conversation

@dpezto

@dpezto dpezto commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Why

PyPI matches a registered trusted publisher against the job_workflow_ref OIDC claim — the workflow file containing the publishing job. workflow_ref sits in warehouse's __unchecked_claims__ and is never validated.

With the publish running inside package-pypi.yml, a workflow_call-only reusable workflow, that claim named the reusable file rather than release.yml. PyPI does not accept a reusable workflow as a publisher, per its own troubleshooting page and warehouse#11096. That is what returned invalid-publisher: valid token, but no corresponding publisher on the 3.0.0 release — and no value entered in the publisher form could have matched it, including the release.yml the error's workflow_ref field invites.

What moved

Only the publish job. Building is untouched.

Job Where Change
build_sdist package-pypi.yml none
build_wheels (6 runners) package-pypi.yml none
publish release.yml as pypi-publish hoisted

The build jobs still upload dist-sources and dist-wheels-<os>. Artifacts are scoped to the workflow run, so the hoisted job downloads them across the call boundary with the same pattern: dist-* and merge-multiple: true as before.

The hoisted job declares environment: pypi and permissions: id-token: write, and states the release_created gate explicitly rather than relying on skip-propagation from the pypi job, so the condition survives an edit there. The pypi reusable call correspondingly drops id-token: write, which it no longer needs.

Removed inputs

package-name and environment-name are removed along with the job that read them. An environment-name input that no longer selects the deployment environment would be a silent no-op on a security boundary — worse than its absence. Both values now live explicitly in release.yml.

PYPI_API_TOKEN stays declared but inert in the reusable workflow. The rollback to classic token auth is the same secret, now consumed by release.yml's pypi-publish job.

npm and crates are unchanged

Neither job is touched by this diff. npm's OIDC works from inside its reusable workflow: it reached Rekor and was rejected for duplicate content, which is a different failure from publisher matching, and needs no structural change.

Registration this expects

A pending publisher, since the project does not yet exist on PyPI:

Field Value
PyPI Project Name tree-sitter-gnuplot
Owner dpezto
Repository name tree-sitter-gnuplot
Workflow name release.yml
Environment name pypi

The environment must be pypi and not blank, matching the job's own environment: declaration — a mismatch in either direction reproduces invalid-publisher.

PyPI matches a registered trusted publisher against the job_workflow_ref
OIDC claim, which names the workflow file containing the publishing job;
workflow_ref is never checked. With the publish running inside
package-pypi.yml, that claim named the reusable workflow, and PyPI does
not accept a reusable workflow as a publisher (warehouse#11096). This is
what returned `invalid-publisher: valid token, but no corresponding
publisher` on the 3.0.0 release, and no value entered in the publisher
form could have matched it.

Only the publish job moves. Building is untouched: build_sdist and the
six-runner build_wheels matrix stay in package-pypi.yml and upload the
same dist-sources and dist-wheels-<os> artifacts. Those are scoped to the
workflow run, so the hoisted job downloads them across the call boundary
with the same pattern and merge-multiple settings as before.

The new job declares environment `pypi` and `id-token: write`, and states
the release_created gate explicitly rather than relying on
skip-propagation from the `pypi` job, so the condition survives an edit
there.

The package-name and environment-name inputs are removed along with the
job that read them: an environment-name input that no longer selects the
deployment environment would be a silent no-op on a security boundary.
PYPI_API_TOKEN stays declared but inert in the reusable workflow; the
rollback to classic token auth is the same secret, now consumed by
release.yml.

The npm and crates jobs are unchanged. npm's OIDC works from inside its
reusable workflow — it reached Rekor and was rejected for duplicate
content, a different failure from publisher matching.

Registration this expects: owner dpezto, repository tree-sitter-gnuplot,
workflow release.yml, environment pypi, as a pending publisher since the
project does not yet exist on PyPI.
Copilot AI review requested due to automatic review settings July 25, 2026 20:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added the size/l large — read carefully label Jul 25, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@dpezto
dpezto merged commit 94f8115 into main Jul 25, 2026
11 checks passed
@dpezto
dpezto deleted the ci/pypi-publish-toplevel branch July 25, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l large — read carefully

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants