Repository navigation
267 lines (244 loc) · 12.1 KB
/
Copy pathrelease.yml
File metadata and controls
267 lines (244 loc) · 12.1 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# =============================================================================
# Release & publish workflow (v1.0 — W1.1 / W1.2 / W2.3)
#
# A published GitHub Release builds and signs artifacts. Production uploads
# additionally require the existing PyPI Trusted Publisher and enabled repo gate.
# A pushed tag alone does not trigger publication. Manual choices are TestPyPI,
# build-only, or a production authentication-only check that uploads nothing.
#
# What it does WHEN a Release is published:
# build — builds sdist + wheel, uploads them as workflow artifacts.
# pypi-publish — publishes those artifacts to PyPI via OIDC Trusted Publishing
# (NO API token, NO stored secret). GATED on the repo variable
# PYPI_TRUSTED_PUBLISHER == 'true': until that is set, this job
# is SKIPPED cleanly (gray) so a Release still builds + signs +
# attaches bundles without a red publish failure. On a manual
# workflow_dispatch with publish-target=testpypi it targets
# TestPyPI instead.
# sign — signs the sdist + wheel with Sigstore keyless (ambient OIDC ->
# Fulcio, same trust model as sigstore-fixture.yml / integrity-gate.yml)
# and attaches the .sigstore bundles to the GitHub Release assets.
# This is the "heal thyself" fix: the tool that signs everyone
# else's locks now signs its own release artifacts.
#
# Pin discipline: every `uses:` is pinned to a full 40-char commit SHA with a
# `# vX.Y.Z` comment (enforced by tests/test_workflow_pins.py). The actions/* and
# sigstore SHAs are the SAME pins already used by the other workflows in this repo.
# =============================================================================
name: Release
on:
# Primary, safe trigger: only fires once a GitHub Release is *published*.
# A pushed tag by itself does NOT publish a Release and therefore does NOT
# trigger this workflow.
release:
types: [published]
# Manual TestPyPI/build-only paths and an authentication-only production check.
workflow_dispatch:
inputs:
publish-target:
description: "TestPyPI upload, build-only, or production OIDC verification (no upload)"
required: true
default: "testpypi"
type: choice
options:
- testpypi
- none
- verify-pypi
publisher-checked:
description: "verify-pypi only: inspected existing project publisher and no matching pending publisher in owner account"
type: boolean
required: true
default: false
# Least-privilege at the top level; each job widens scope locally only as needed.
permissions:
contents: read
jobs:
# --------------------------------------------------------------------------
# Job 1: build sdist + wheel and publish them as workflow artifacts so the
# publish and sign jobs consume the exact same bytes (no rebuild drift).
# --------------------------------------------------------------------------
build:
name: Build sdist + wheel
if: github.event_name != 'workflow_dispatch' || github.event.inputs.publish-target != 'verify-pypi'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python 3.11
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
cache: pip
- name: Install build frontend
run: python -m pip install --upgrade "build==1.2.2.post1"
- name: Build sdist + wheel
run: python -m build --sdist --wheel --outdir dist/
- name: Show built artifacts
run: ls -l dist/
- name: Upload dist artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: dist
path: dist/
if-no-files-found: error
- name: Set up Node 22
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: npm
cache-dependency-path: packages/lock-ts/package-lock.json
- name: Test and pack TypeScript verifier
working-directory: packages/lock-ts
run: |
npm ci
npm test
mkdir -p ../../node-dist
npm pack --pack-destination ../../node-dist
- name: Checksum Python and TypeScript release artifacts
run: |
python - <<'PY'
from hashlib import sha256
from pathlib import Path
artifacts = sorted([*Path("dist").glob("*.whl"), *Path("dist").glob("*.tar.gz"), *Path("node-dist").glob("*.tgz")])
Path("node-dist/SHA256SUMS").write_text("".join(f"{sha256(p.read_bytes()).hexdigest()} {p.name}\n" for p in artifacts))
PY
- name: Upload TypeScript and checksum artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: lock-ts
path: node-dist/
if-no-files-found: error
# --------------------------------------------------------------------------
# Job 2: publish to PyPI via OIDC Trusted Publishing.
#
# No API token, no stored secret: pypa/gh-action-pypi-publish mints a short-lived
# OIDC token from this job's `id-token: write` and exchanges it for a PyPI upload
# token. PyPI must have a Trusted Publisher (pending or active) for `mcp-warden-cli`
# pointing at owner=DataScience-EngineeringExperts, repo=mcp-warden, workflow=release.yml — see
# RELEASING.md. Until that exists, this job fails CLOSED (publish denied), it
# never falls back to anything insecure.
#
# On a `release: published` run -> production PyPI.
# On a manual `workflow_dispatch` with publish-target=testpypi -> TestPyPI only.
# publish-target=none builds only; verify-pypi skips the build and upload jobs.
# --------------------------------------------------------------------------
pypi-publish:
name: Publish to PyPI (OIDC Trusted Publishing)
needs: build
runs-on: ubuntu-latest
# id-token: write is scoped to THIS job only (never granted at workflow top level).
permissions:
id-token: write # REQUIRED for OIDC Trusted Publishing — mints the token
contents: read
# Gate: only run when OIDC Trusted Publishing is explicitly enabled via the
# repo variable PYPI_TRUSTED_PUBLISHER == 'true'. While the variable is unset
# (or any value other than 'true'), this job is SKIPPED cleanly (gray, not red)
# so a published Release still builds + Sigstore-signs + attaches bundles
# without a red "publish" failure. Set it once the Trusted Publisher exists:
# gh variable set PYPI_TRUSTED_PUBLISHER --body true (see RELEASING.md).
# The event-type guard is preserved: skip entirely on a manual run that opted
# out of publishing.
if: >-
vars.PYPI_TRUSTED_PUBLISHER == 'true' &&
(github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish-target == 'testpypi'))
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
# Production PyPI — only on a published Release. Trusted Publishing (OIDC).
- name: Publish to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/
# No `password:` — OIDC Trusted Publishing is used (no stored secret).
# Re-running a Release for an already-published version no-ops on the
# duplicate instead of failing red (e.g. token-published 1.0.0 / 1.0.1).
skip-existing: true
# TestPyPI — only on a manual dry run. Same OIDC mechanism, different index.
# Requires a SEPARATE Trusted Publisher on test.pypi.org (optional; see RELEASING.md).
- name: Publish to TestPyPI (manual dry run)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish-target == 'testpypi'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/
# Re-running a dry run no-ops on an already-uploaded TestPyPI version.
skip-existing: true
# Authentication-only production check: no build, upload, signing or stored token.
# Uses this SAME workflow identity and no deployment environment, like publication.
pypi-verify:
name: Verify PyPI OIDC (no upload)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish-target == 'verify-pypi'
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
env:
WARDEN_PYPI_PUBLISHER_CONFIRMED: ${{ github.event.inputs.publisher-checked }}
steps:
- name: Require reviewed main
if: github.ref != 'refs/heads/main'
run: |
echo '::error::Production verification requires reviewed main.'
exit 1
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python 3.11
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Verify production OIDC exchange without uploading
run: python scripts/verify_pypi_oidc.py
# --------------------------------------------------------------------------
# Job 3: sign the release artifacts with Sigstore keyless ("heal thyself", W2.3).
#
# Same keyless trust model as sigstore-fixture.yml / integrity-gate.yml: the
# job's ambient OIDC mints a short-lived Fulcio cert (issuer
# https://token.actions.githubusercontent.com), signs each dist file, and — on a
# `release: published` run — attaches the resulting .sigstore bundle(s) to the
# GitHub Release assets (release-signing-artifacts, the action's default).
#
# We also self-verify (`verify: true`) against THIS workflow's own identity so a
# broken run can never publish a bad bundle, mirroring the e2e self-check pattern.
#
# Only runs for a real published Release (signing release assets requires a
# Release to attach to). Skipped on manual TestPyPI dry runs.
# --------------------------------------------------------------------------
sign:
name: Sign release artifacts (Sigstore keyless)
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'release'
permissions:
id-token: write # REQUIRED for ambient OIDC -> Fulcio
contents: write # REQUIRED to attach .sigstore bundles to the Release assets
steps:
- name: Download dist artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist/
- name: Download TypeScript and checksum artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: lock-ts
path: dist/
- name: Sign release artifacts and attach bundles to the Release
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
with:
inputs: ./dist/*.tar.gz ./dist/*.whl ./dist/*.tgz ./dist/SHA256SUMS
# Self-check: verify what we just signed against THIS workflow's own
# identity so a broken run never publishes a bad bundle.
verify: true
verify-cert-identity: "https://github.com/DataScience-EngineeringExperts/mcp-warden/.github/workflows/release.yml@${{ github.ref }}"
verify-oidc-issuer: "https://token.actions.githubusercontent.com"
# release-signing-artifacts defaults to true: on a release event the
# .sigstore bundles are attached to the Release assets automatically.
release-signing-artifacts: true
upload-signing-artifacts: true