From 6617106d9e39b9297242a8f192a50f7c06136299 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 23 Aug 2026 03:41:42 +0200 Subject: [PATCH 1/3] fix(ci): stop publishing to PyPI through a reusable workflow PyPI does not accept a reusable workflow as a trusted publisher -- pypi/warehouse#11096, unresolved. Both publish jobs here called reqstool/.github's python-publish-to-pypi.yml via workflow_call, so every real publish attempt failed at the OIDC exchange with invalid-publisher regardless of what the trusted publisher config named -- confirmed today, and it explains why publish-dev-to-testpypi.yml has been failing on every push to main since the 2026-08-16 migration. Switches both jobs to reqstool/.github's new actions/publish-to-pypi composite action, called directly from a job defined in this workflow. A composite action doesn't have the same problem: the OIDC claim is about which workflow file the job runs in, not what its steps reference. The two-index environment derivation (testpypi -> test, pypi -> stable) that the reusable workflow computed internally now has to be explicit per job -- a composite action can't set the caller's environment:. Needs reqstool/.github#92 merged first. Signed-off-by: Jimisola Laursen --- .github/workflows/publish-dev-to-testpypi.yml | 11 +++++--- .github/workflows/release.yml | 26 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish-dev-to-testpypi.yml b/.github/workflows/publish-dev-to-testpypi.yml index 2656b21..4786e74 100644 --- a/.github/workflows/publish-dev-to-testpypi.yml +++ b/.github/workflows/publish-dev-to-testpypi.yml @@ -22,8 +22,13 @@ jobs: publish-to-test-pypi: needs: build - uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main + runs-on: ubuntu-latest + environment: + name: test + url: https://test.pypi.org permissions: id-token: write - with: - target: testpypi + steps: + - uses: reqstool/.github/.github/actions/publish-to-pypi@main + with: + target: testpypi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86a7409..c389e53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,12 +97,17 @@ jobs: publish-to-testpypi: needs: [prepare, assets] - uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main + runs-on: ubuntu-latest + environment: + name: test + url: https://test.pypi.org permissions: id-token: write - with: - target: testpypi - artifact: dist-tagged + steps: + - uses: reqstool/.github/.github/actions/publish-to-pypi@main + with: + target: testpypi + artifact: dist-tagged # PyPI is the only step here that cannot be undone: a version can be yanked but # never replaced. A release candidate stops at Test PyPI -- pip needs --pre to @@ -110,12 +115,17 @@ jobs: publish-to-pypi: needs: [prepare, publish-to-testpypi] if: ${{ needs.prepare.outputs.prerelease != 'true' }} - uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main + runs-on: ubuntu-latest + environment: + name: stable + url: https://pypi.org permissions: id-token: write - with: - target: pypi - artifact: dist-tagged + steps: + - uses: reqstool/.github/.github/actions/publish-to-pypi@main + with: + target: pypi + artifact: dist-tagged # Last, deliberately. Everything above can fail, and until this runs nothing # resolving "the latest release" can see what was built -- the release is still From 881163bbe80fbd166c123fae0635785e0baa4c04 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 23 Aug 2026 03:50:03 +0200 Subject: [PATCH 2/3] fix(ci): drop Test PyPI entirely rather than fix its trusted publisher PyPI accepts a pre-release version identifier (0.3.0rc1) on the real index directly, and pip ignores it without --pre -- there was never a need to route a release candidate to a separate staging index to hold it safely. Test PyPI existed here only as that safety net. publish-to-pypi now runs unconditionally instead of skipping for a release candidate, which also makes this consistent with what npm and Maven Central already do. publish-dev-to-testpypi.yml is deleted -- it's been failing on every push to main since the 2026-08-16 migration anyway (see reqstool/.github#92), and there's nowhere left for it to publish to. Needs reqstool/.github#92 merged first. Signed-off-by: Jimisola Laursen --- .github/workflows/publish-dev-to-testpypi.yml | 34 ----------------- .github/workflows/release.yml | 38 +++++-------------- 2 files changed, 10 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/publish-dev-to-testpypi.yml diff --git a/.github/workflows/publish-dev-to-testpypi.yml b/.github/workflows/publish-dev-to-testpypi.yml deleted file mode 100644 index 4786e74..0000000 --- a/.github/workflows/publish-dev-to-testpypi.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Publish dev build to Test PyPI - -# The continuous dev feed: every push to main lands on Test PyPI as a `.devN` -# build. Separate from the release flow, which publishes the tagged version to -# Test PyPI and then PyPI -- both use skip-existing, so they cannot collide. - -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: read - -jobs: - build: - name: Reuse build - uses: ./.github/workflows/build.yml - permissions: - contents: read - - publish-to-test-pypi: - needs: build - runs-on: ubuntu-latest - environment: - name: test - url: https://test.pypi.org - permissions: - id-token: write - steps: - - uses: reqstool/.github/.github/actions/publish-to-pypi@main - with: - target: testpypi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c389e53..ef55d87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,26 +95,13 @@ jobs: version: ${{ needs.prepare.outputs.version }} artifact: dist-tagged - publish-to-testpypi: - needs: [prepare, assets] - runs-on: ubuntu-latest - environment: - name: test - url: https://test.pypi.org - permissions: - id-token: write - steps: - - uses: reqstool/.github/.github/actions/publish-to-pypi@main - with: - target: testpypi - artifact: dist-tagged - # PyPI is the only step here that cannot be undone: a version can be yanked but - # never replaced. A release candidate stops at Test PyPI -- pip needs --pre to - # see a prerelease anyway. + # never replaced. Publishes release candidates too, with their pre-release + # identifier -- pip ignores them without --pre, so there is nothing unsafe + # about it landing on the real index; there is no separate staging index to + # route them to instead. Matches npm and Maven Central's existing behavior. publish-to-pypi: - needs: [prepare, publish-to-testpypi] - if: ${{ needs.prepare.outputs.prerelease != 'true' }} + needs: [prepare, assets] runs-on: ubuntu-latest environment: name: stable @@ -124,7 +111,6 @@ jobs: steps: - uses: reqstool/.github/.github/actions/publish-to-pypi@main with: - target: pypi artifact: dist-tagged # Last, deliberately. Everything above can fail, and until this runs nothing @@ -132,16 +118,12 @@ jobs: # a prerelease. Promotion itself is one API call against a release that already # has its artifacts. # - # The guard is `no job failed`, not the default `every job succeeded`: a release - # candidate deliberately skips the publish jobs that a real release runs, and a - # skipped dependency would otherwise cascade and skip this too -- leaving the - # candidate unpromoted, which is right, and every *real* release unpromoted the - # moment any optional job is skipped, which is not. - # - # `!inputs.dry-run` has to be spelled out for the same reason: on a dry run - # every job above is skipped, and "nothing failed" would otherwise be true. + # The guard is `no job failed`, not the default `every job succeeded`: on a dry + # run every job above is skipped, which would make plain success() false too -- + # `!inputs.dry-run` is what actually gates this job then, and `no job failed` + # is what confirms nothing above it errored on a real run. promote: - needs: [prepare, assets, publish-to-testpypi, publish-to-pypi] + needs: [prepare, assets, publish-to-pypi] if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} uses: reqstool/.github/.github/workflows/common-release-promote.yml@main permissions: From c5a391f3763c5a568d8bbecbd46449a1d22dbce6 Mon Sep 17 00:00:00 2001 From: Jimisola Laursen Date: Sun, 23 Aug 2026 15:32:08 +0200 Subject: [PATCH 3/3] fix(ci): pin the publish-to-pypi action reference to a commit CodeQL flagged actions/unpinned-tag: @main is a mutable ref, so a compromised or force-pushed reqstool/.github main would execute through this reference with no review. Pinned to the commit main pointed at when this action was added, matching the org's existing `@ # main YYYY-MM-DD` convention for third-party/internal action references elsewhere in this repo. Signed-off-by: Jimisola Laursen --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef55d87..caefc0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,7 +109,7 @@ jobs: permissions: id-token: write steps: - - uses: reqstool/.github/.github/actions/publish-to-pypi@main + - uses: reqstool/.github/.github/actions/publish-to-pypi@b10b898cd5a1d552a578dbe4f170f84fb8f98b6c # main 2026-08-23 with: artifact: dist-tagged