Skip to content

Commit 6d792d0

Browse files
ci(release): restore OIDC auth, keep idempotency guard (#569)
Reverts the auth change from #567 (back to OIDC Trusted Publishing) while keeping the idempotency guard that actually fixes the red Deployments widget. No GitHub Secret needed; no long-lived token stored anywhere.
1 parent efcac54 commit 6d792d0

1 file changed

Lines changed: 43 additions & 41 deletions

File tree

.github/workflows/release.yml

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,47 @@ name: release
22

33
# Automated PyPI publishing for django-admin-react.
44
#
5-
# WHY: until now every release was a manual local run of
6-
# `scripts/build.sh` + `poetry publish` with the long-lived PyPI token in
7-
# `./.env` on the maintainer's laptop — so cadence depended on a human
8-
# remembering. This workflow makes a release a single GitHub click: publish
9-
# a Release and the matching wheel/sdist ship to PyPI automatically.
5+
# WHY: every release was a manual local run of `scripts/build.sh` +
6+
# `poetry publish` with the long-lived PyPI token in `./.env` on the
7+
# maintainer's laptop — so cadence depended on a human remembering. This
8+
# workflow makes a release a single GitHub click: publish a Release and the
9+
# matching wheel/sdist ship to PyPI automatically.
1010
#
1111
# SECURITY POSTURE:
12+
# - **No long-lived tokens stored anywhere.** PyPI auth uses
13+
# **Trusted Publishing (OIDC)** — PyPI verifies the workflow's
14+
# short-lived OIDC identity at upload time. The maintainer's `./.env`
15+
# `POETRY_PYPI_TOKEN_PYPI` stays purely local for the manual fallback
16+
# path; it is never copied into GitHub Secrets and never echoed.
1217
# - Trigger is `release: published` — a human still authorises every
1318
# publish (preserves the Tier-6 "human triggers the release" rule); the
1419
# Release notes double as the changelog entry.
15-
# - PyPI auth uses the **stored token** in `secrets.PYPI_API_TOKEN` (set
16-
# in repo Settings → Secrets → Actions; same value the maintainer keeps
17-
# in `./.env` as `POETRY_PYPI_TOKEN_PYPI`). Trusted Publishing (OIDC)
18-
# remains the longer-term goal — tracked in #564 — but is gated on a
19-
# one-time PyPI configuration step that hasn't yet been performed.
20-
# - **Idempotent publish.** Before uploading, the job checks PyPI for the
21-
# current version. If the wheel + sdist for that version already exist
22-
# (e.g. published manually first, or a previous workflow run partially
23-
# succeeded) the upload step is skipped and the deployment is still
24-
# marked green. Releases never fail just because the artifact is already
25-
# where it should be.
26-
# - Least-privilege: top-level `contents: read`; the publish jobs add no
27-
# extra permissions (token-based auth doesn't need `id-token: write`).
20+
# - **Idempotent publish.** Before uploading, the job queries PyPI's JSON
21+
# API for the current version. If the wheel + sdist for that version
22+
# already exist (e.g. the maintainer published manually first, or a
23+
# previous workflow run partially succeeded), the upload step is
24+
# skipped and the deployment is still marked **green**. Releases never
25+
# fail just because the artifact is already where it should be — and
26+
# the idempotency check itself requires NO auth, so the widget goes
27+
# green for already-published versions even before #564 is set up.
28+
# - Least-privilege: top-level `contents: read`; only the publish job
29+
# gets `id-token: write`, scoped to the `pypi` environment.
2830
# - All third-party actions are pinned to a full commit SHA (a tag can be
2931
# moved, a SHA cannot) per the supply-chain hardening in issue #331.
3032
#
31-
# ONE-TIME OWNER SETUP (required before the first publish can succeed):
32-
# 1. PyPI → Account → API tokens → "Add API token", scope it to
33-
# `django-admin-react`, copy the `pypi-…` value.
34-
# 2. GitHub → repo Settings → Secrets and variables → Actions → "New
35-
# repository secret":
36-
# Name: PYPI_API_TOKEN
37-
# Value: <the pypi-… token from step 1>
38-
# 3. (Optional) GitHub → repo Settings → Environments → create `pypi`
39-
# (add required reviewers to gate the publish on a human approval),
40-
# and `testpypi` for dry runs.
41-
# See SECURITY.md §7 for the rationale.
33+
# ONE-TIME OWNER SETUP (required before the workflow can perform a FRESH
34+
# upload — already-published versions go green without it via the
35+
# idempotency guard above):
36+
# 1. PyPI → project `django-admin-react` → Settings → Publishing → add a
37+
# "Trusted Publisher":
38+
# Owner: MartinCastroAlvarez
39+
# Repository: django-admin-react
40+
# Workflow: release.yml
41+
# Environment: pypi
42+
# 2. GitHub → repo Settings → Environments → create `pypi` (optionally
43+
# add required reviewers so a release is approval-gated), and
44+
# `testpypi`.
45+
# See SECURITY.md §7 for the rationale. Tracked in #564.
4246

4347
on:
4448
release:
@@ -95,14 +99,16 @@ jobs:
9599
if-no-files-found: error
96100

97101
publish-pypi:
98-
name: Publish to PyPI
102+
name: Publish to PyPI (Trusted Publishing)
99103
needs: build
100104
runs-on: ubuntu-latest
101105
# GitHub Release publish, or an explicit manual run targeting pypi.
102106
if: github.event_name == 'release' || inputs.target == 'pypi'
103107
environment:
104108
name: pypi
105109
url: https://pypi.org/project/django-admin-react/
110+
permissions:
111+
id-token: write # OIDC for Trusted Publishing — no stored token
106112
steps:
107113
- name: Checkout (for pyproject version read)
108114
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -117,6 +123,7 @@ jobs:
117123
# on PyPI. The maintainer's manual `.env` publish path occasionally
118124
# ships first; in that case this workflow is a no-op that still marks
119125
# the GitHub Deployment green so the repo widget reflects reality.
126+
# This step calls only the public PyPI JSON API — no auth, no token.
120127
- name: Is this version already on PyPI?
121128
id: already
122129
run: |
@@ -127,41 +134,36 @@ jobs:
127134
echo "Found django-admin-react ${VERSION} on PyPI — skipping upload."
128135
echo "skip=true" >> "$GITHUB_OUTPUT"
129136
else
130-
echo "django-admin-react ${VERSION} not on PyPI yet — proceeding with upload."
137+
echo "django-admin-react ${VERSION} not on PyPI yet — proceeding with OIDC upload."
131138
echo "skip=false" >> "$GITHUB_OUTPUT"
132139
fi
133140
134-
- name: Publish to PyPI
141+
- name: Publish to PyPI (Trusted Publishing)
135142
if: steps.already.outputs.skip != 'true'
136143
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
137144
with:
138-
password: ${{ secrets.PYPI_API_TOKEN }}
139145
packages-dir: dist/
140-
# Defense-in-depth: the action itself also tolerates the
141-
# "file already exists" case if a race slipped past the
142-
# idempotency guard above.
143-
skip-existing: true
144146

145147
publish-testpypi:
146-
name: Publish to TestPyPI
148+
name: Publish to TestPyPI (Trusted Publishing)
147149
needs: build
148150
runs-on: ubuntu-latest
149151
# Manual dry-runs only — keeps a safe rehearsal path off PyPI.
150152
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
151153
environment:
152154
name: testpypi
153155
url: https://test.pypi.org/project/django-admin-react/
156+
permissions:
157+
id-token: write
154158
steps:
155159
- name: Download distributions
156160
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
157161
with:
158162
name: dist
159163
path: dist/
160164

161-
- name: Publish to TestPyPI
165+
- name: Publish to TestPyPI (Trusted Publishing)
162166
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
163167
with:
164-
password: ${{ secrets.TESTPYPI_API_TOKEN }}
165168
packages-dir: dist/
166169
repository-url: https://test.pypi.org/legacy/
167-
skip-existing: true

0 commit comments

Comments
 (0)