-
Notifications
You must be signed in to change notification settings - Fork 0
169 lines (153 loc) · 6.6 KB
/
publish.yml
File metadata and controls
169 lines (153 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
name: publish
# Automated PyPI publishing for django-admin-react.
#
# WHY: every release was a manual local run of `scripts/build.sh` +
# `poetry publish` with the long-lived PyPI token in `./.env` on the
# maintainer's laptop — so cadence depended on a human remembering. This
# workflow makes a release a single GitHub click: publish a Release and the
# matching wheel/sdist ship to PyPI automatically.
#
# SECURITY POSTURE:
# - **No long-lived tokens stored anywhere.** PyPI auth uses
# **Trusted Publishing (OIDC)** — PyPI verifies the workflow's
# short-lived OIDC identity at upload time. The maintainer's `./.env`
# `POETRY_PYPI_TOKEN_PYPI` stays purely local for the manual fallback
# path; it is never copied into GitHub Secrets and never echoed.
# - Trigger is `release: published` — a human still authorises every
# publish (preserves the Tier-6 "human triggers the release" rule); the
# Release notes double as the changelog entry.
# - **Idempotent publish.** Before uploading, the job queries PyPI's JSON
# API for the current version. If the wheel + sdist for that version
# already exist (e.g. the maintainer published manually first, or a
# previous workflow run partially succeeded), the upload step is
# skipped and the deployment is still marked **green**. Releases never
# fail just because the artifact is already where it should be — and
# the idempotency check itself requires NO auth, so the widget goes
# green for already-published versions even before #564 is set up.
# - Least-privilege: top-level `contents: read`; only the publish job
# gets `id-token: write`, scoped to the `pypi` environment.
# - All third-party actions are pinned to a full commit SHA (a tag can be
# moved, a SHA cannot) per the supply-chain hardening in issue #331.
#
# ONE-TIME OWNER SETUP (required before the workflow can perform a FRESH
# upload — already-published versions go green without it via the
# idempotency guard above):
# 1. PyPI → project `django-admin-react` → Settings → Publishing → add a
# "Trusted Publisher":
# Owner: MartinCastroAlvarez
# Repository: django-admin-react
# Workflow: publish.yml
# Environment: pypi
# 2. GitHub → repo Settings → Environments → create `pypi` (optionally
# add required reviewers so a release is approval-gated), and
# `testpypi`.
# See SECURITY.md §7 for the rationale. Tracked in #564.
on:
release:
types: [published]
workflow_dispatch:
inputs:
target:
description: "Publish target"
type: choice
default: testpypi
options:
- testpypi
- pypi
permissions:
contents: read
jobs:
build:
name: Build SPA + wheel/sdist
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 22
cache: pnpm
cache-dependency-path: frontend/pnpm-lock.yaml
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
- name: Install Poetry
run: pipx install poetry
- name: Build (Vite SPA bundle, then wheel + sdist)
run: bash scripts/build.sh
- name: Upload distributions
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist
path: dist/
if-no-files-found: error
publish-pypi:
name: Publish to PyPI (Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# GitHub Release publish, or an explicit manual run targeting pypi.
if: github.event_name == 'release' || inputs.target == 'pypi'
environment:
name: pypi
url: https://pypi.org/project/django-admin-react/
permissions:
id-token: write # OIDC for Trusted Publishing — no stored token
steps:
- name: Checkout (for pyproject version read)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download distributions
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: dist/
# Idempotency guard — never fail just because the artifact is already
# on PyPI. The maintainer's manual `.env` publish path occasionally
# ships first; in that case this workflow is a no-op that still marks
# the GitHub Deployment green so the repo widget reflects reality.
# This step calls only the public PyPI JSON API — no auth, no token.
- name: Is this version already on PyPI?
id: already
run: |
set -euo pipefail
VERSION=$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if curl -fsS "https://pypi.org/pypi/django-admin-react/${VERSION}/json" >/dev/null 2>&1; then
echo "Found django-admin-react ${VERSION} on PyPI — skipping upload."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "django-admin-react ${VERSION} not on PyPI yet — proceeding with OIDC upload."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to PyPI (Trusted Publishing)
if: steps.already.outputs.skip != 'true'
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
packages-dir: dist/
publish-testpypi:
name: Publish to TestPyPI (Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# Manual dry-runs only — keeps a safe rehearsal path off PyPI.
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
environment:
name: testpypi
url: https://test.pypi.org/project/django-admin-react/
permissions:
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: dist
path: dist/
- name: Publish to TestPyPI (Trusted Publishing)
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
with:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/