From edc789ee34ea4b47071b0d76aa447b22494a7de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dai=20L=C3=B3pez?= Date: Sat, 25 Jul 2026 14:44:47 -0600 Subject: [PATCH] ci(pypi): publish from a top-level job so trusted publishing can match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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- 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. --- .github/workflows/package-pypi.yml | 50 +++++++++--------------------- .github/workflows/release.yml | 37 ++++++++++++++++++++-- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/.github/workflows/package-pypi.yml b/.github/workflows/package-pypi.yml index 1c3859d..26c56ef 100644 --- a/.github/workflows/package-pypi.yml +++ b/.github/workflows/package-pypi.yml @@ -1,20 +1,23 @@ -# Vendored from tree-sitter/workflows/.github/workflows/package-pypi.yml, with -# OIDC trusted publishing enabled (upstream PR #54, unmerged): PYPI_API_TOKEN is -# optional and the publish job carries `id-token: write`. `workflow_call` only, -# so it never runs on its own. Re-sync from upstream if the build matrix drifts. +# Vendored from tree-sitter/workflows/.github/workflows/package-pypi.yml, build +# jobs only. Upstream's `package` job is deliberately ABSENT: PyPI matches a +# trusted publisher against job_workflow_ref, so a publish running here would +# present this file rather than release.yml, and PyPI does not accept a reusable +# workflow as a publisher (warehouse#11096). The publish job lives in +# release.yml as `pypi-publish` and downloads the `dist-*` artifacts these jobs +# upload. Do NOT restore `package` when re-syncing from upstream — that +# reintroduces the `invalid-publisher` failure seen on the 3.0.0 release. +# `workflow_call` only, so it never runs on its own. +# +# The package-name and environment-name inputs are gone with the publish job: +# an environment-name that no longer selects the deployment environment would +# be a silent no-op on a security boundary. Both now live in release.yml. +# PYPI_API_TOKEN is declared but inert here; the rollback to classic token auth +# is the same secret consumed by release.yml's pypi-publish job. name: Publish package (pypi) on: workflow_call: inputs: - package-name: - description: The name of the package - default: ${{github.event.repository.name}} - type: string - environment-name: - description: The name of the environment - default: pypi - type: string python-version: description: The Python version default: ${{vars.PYTHON_VERSION || '3.11'}} @@ -137,26 +140,3 @@ jobs: path: dist/*.whl name: dist-wheels-${{matrix.os}} retention-days: 2 - - package: - name: Publish Python package - needs: [build_sdist, build_wheels] - runs-on: ubuntu-latest - environment: - name: ${{inputs.environment-name}} - url: https://pypi.org/project/${{inputs.package-name}}/ - permissions: - id-token: write - steps: - - name: Download build artifacts - uses: actions/download-artifact@v8 - with: - path: dist - pattern: dist-* - merge-multiple: true - - name: Check build artifacts - run: ls -l dist - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{secrets.PYPI_API_TOKEN}} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ced683..df1e658 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,11 +76,42 @@ jobs: pypi: needs: release-please if: needs.release-please.outputs.release_created == 'true' - # Passed to every job in the reusable workflow: build jobs checkout - # (contents), the publish job mints the OIDC token (id-token). permissions: contents: read - id-token: write uses: ./.github/workflows/package-pypi.yml with: generate: true + + # The publish step lives here, not in package-pypi.yml, because PyPI matches a + # trusted publisher against the job_workflow_ref claim — the file holding the + # publishing job. Inside a reusable workflow that claim names package-pypi.yml, + # and PyPI does not accept a reusable workflow as a publisher (warehouse#11096), + # which is what produced `invalid-publisher` on the 3.0.0 release. Only this job + # moved up; sdist and wheel building stay in package-pypi.yml, whose artifacts + # are scoped to the run and so download here unchanged. The release_created + # gate is stated explicitly rather than left to skip-propagation from `pypi`, + # so the condition survives an edit to that job. Register the publisher as + # workflow `release.yml`, environment `pypi`, matching the declaration below. + pypi-publish: + name: Publish Python package + needs: [release-please, pypi] + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/tree-sitter-gnuplot/ + permissions: + id-token: write + steps: + - name: Download build artifacts + uses: actions/download-artifact@v8 + with: + path: dist + pattern: dist-* + merge-multiple: true + - name: Check build artifacts + run: ls -l dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{secrets.PYPI_API_TOKEN}}