Skip to content

Commit a39c450

Browse files
authored
fix(ci): stop publishing to PyPI through a reusable workflow (#102)
* 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 <jimisola@jimisola.com> * 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 <jimisola@jimisola.com> * 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 `@<sha> # main YYYY-MM-DD` convention for third-party/internal action references elsewhere in this repo. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> --------- Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent be3cdf6 commit a39c450

2 files changed

Lines changed: 18 additions & 55 deletions

File tree

.github/workflows/publish-dev-to-testpypi.yml

Lines changed: 0 additions & 29 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -95,43 +95,35 @@ jobs:
9595
version: ${{ needs.prepare.outputs.version }}
9696
artifact: dist-tagged
9797

98-
publish-to-testpypi:
99-
needs: [prepare, assets]
100-
uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main
101-
permissions:
102-
id-token: write
103-
with:
104-
target: testpypi
105-
artifact: dist-tagged
106-
10798
# PyPI is the only step here that cannot be undone: a version can be yanked but
108-
# never replaced. A release candidate stops at Test PyPI -- pip needs --pre to
109-
# see a prerelease anyway.
99+
# never replaced. Publishes release candidates too, with their pre-release
100+
# identifier -- pip ignores them without --pre, so there is nothing unsafe
101+
# about it landing on the real index; there is no separate staging index to
102+
# route them to instead. Matches npm and Maven Central's existing behavior.
110103
publish-to-pypi:
111-
needs: [prepare, publish-to-testpypi]
112-
if: ${{ needs.prepare.outputs.prerelease != 'true' }}
113-
uses: reqstool/.github/.github/workflows/python-publish-to-pypi.yml@main
104+
needs: [prepare, assets]
105+
runs-on: ubuntu-latest
106+
environment:
107+
name: stable
108+
url: https://pypi.org
114109
permissions:
115110
id-token: write
116-
with:
117-
target: pypi
118-
artifact: dist-tagged
111+
steps:
112+
- uses: reqstool/.github/.github/actions/publish-to-pypi@b10b898cd5a1d552a578dbe4f170f84fb8f98b6c # main 2026-08-23
113+
with:
114+
artifact: dist-tagged
119115

120116
# Last, deliberately. Everything above can fail, and until this runs nothing
121117
# resolving "the latest release" can see what was built -- the release is still
122118
# a prerelease. Promotion itself is one API call against a release that already
123119
# has its artifacts.
124120
#
125-
# The guard is `no job failed`, not the default `every job succeeded`: a release
126-
# candidate deliberately skips the publish jobs that a real release runs, and a
127-
# skipped dependency would otherwise cascade and skip this too -- leaving the
128-
# candidate unpromoted, which is right, and every *real* release unpromoted the
129-
# moment any optional job is skipped, which is not.
130-
#
131-
# `!inputs.dry-run` has to be spelled out for the same reason: on a dry run
132-
# every job above is skipped, and "nothing failed" would otherwise be true.
121+
# The guard is `no job failed`, not the default `every job succeeded`: on a dry
122+
# run every job above is skipped, which would make plain success() false too --
123+
# `!inputs.dry-run` is what actually gates this job then, and `no job failed`
124+
# is what confirms nothing above it errored on a real run.
133125
promote:
134-
needs: [prepare, assets, publish-to-testpypi, publish-to-pypi]
126+
needs: [prepare, assets, publish-to-pypi]
135127
if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
136128
uses: reqstool/.github/.github/workflows/common-release-promote.yml@main
137129
permissions:

0 commit comments

Comments
 (0)