-
Notifications
You must be signed in to change notification settings - Fork 0
280 lines (252 loc) · 11.2 KB
/
Copy pathpython-release.yml
File metadata and controls
280 lines (252 loc) · 11.2 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
268
269
270
271
272
273
274
275
276
277
278
279
280
# Publish the Fernsicht Python SDK to PyPI.
#
# Trigger: push of a tag matching `py/v*` (e.g., `py/v0.1.0`).
# The tag version MUST match the `version = "…"` line in
# publishers/python/pyproject.toml (the preflight job asserts this
# before anything else runs).
#
# Authentication: PyPI OIDC "trusted publisher" — no long-lived
# API token. The pending publisher config on pypi.org lists:
# Owner: MuteJester
# Repository: Fernsicht
# Workflow: python-release.yml (this file's basename)
# Environment: (any)
# Match those EXACTLY or PyPI refuses the token mint.
#
# Phase 0 safety rails applied: concurrency, preflight (tests +
# version assert), SHA-pinned actions.
name: python-release
on:
push:
tags:
- 'py/v*'
concurrency:
# One publish per tag at a time. PyPI is immutable, so double-
# publishing would just produce a hard error on the second run;
# still, queue rather than cancel to preserve audit-log clarity.
group: release-python-${{ github.ref_name }}
cancel-in-progress: false
permissions:
contents: read
# Required for PyPI OIDC trusted-publisher token mint.
id-token: write
jobs:
# ------------------------------------------------------------------
# Preflight — tests + tag↔manifest version assertion. Cheap gate.
# ------------------------------------------------------------------
preflight:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.vars.outputs.version }}
tag: ${{ steps.vars.outputs.tag }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin v4.2.2
- name: Resolve version from tag
id: vars
run: |
# GITHUB_REF = "refs/tags/py/v0.1.0".
# tag — FULL git tag ("py/v0.1.0"), for SLSA upload-tag-name.
# version — stripped ("0.1.0"), for PyPI + pyproject.toml match.
# PyPI accepts PEP 440 versions (incl. pre-releases like
# "0.1.0rc1"); pyproject.toml's `version = "..."` must match.
tag="${GITHUB_REF#refs/tags/}"
version="${tag#py/v}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Assert tag matches pyproject.toml version
env:
EXPECTED: ${{ steps.vars.outputs.version }}
run: |
set -e
ACTUAL="$(grep -E '^version' publishers/python/pyproject.toml | head -1 | cut -d'"' -f2)"
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::tag says version=$EXPECTED but publishers/python/pyproject.toml says $ACTUAL"
echo "Bump publishers/python/pyproject.toml before tagging, or retag."
exit 1
fi
echo "tag/manifest agree on version=$EXPECTED"
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # pin v5.6.0
with:
python-version: '3.12'
- name: Install + test
working-directory: publishers/python
run: |
python -m pip install -e .
python -m pip install pytest
pytest -q tests/
# ------------------------------------------------------------------
# Build — produce sdist + wheel. Runs after preflight green.
# ------------------------------------------------------------------
build:
needs: preflight
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
digests: ${{ steps.digest-hash.outputs.digests }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin v4.2.2
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # pin v5.6.0
with:
python-version: '3.12'
- name: Build sdist + wheel
working-directory: publishers/python
run: |
python -m pip install --upgrade build
python -m build
ls -la dist/
- name: Smoke-install the wheel
# Defense-in-depth: install the wheel we just built into a
# clean venv and confirm the top-level import works. Catches
# packaging mistakes (missing files, wrong include rules)
# BEFORE we publish to an immutable registry.
working-directory: publishers/python
run: |
python -m venv /tmp/wheelcheck
/tmp/wheelcheck/bin/python -m pip install dist/fernsicht-*.whl
/tmp/wheelcheck/bin/python -c "from fernsicht import blick, manual; print('import OK')"
# Phase 6.1 — CycloneDX SBOM for the Python SDK. Scans the
# source tree so pyproject.toml + its resolved deps are in the
# bill of materials. Written to a SEPARATE directory from
# `dist/` because pypa/gh-action-pypi-publish uploads the whole
# contents of `packages-dir`; the SBOM is release metadata, not
# a PyPI distribution file.
- name: Prepare SBOM output directory
# sbom-action doesn't mkdir -p the output path's parent
# directory; create it explicitly so the action's file write
# doesn't fail on missing ancestor.
run: mkdir -p publishers/python/sbom
- name: Generate CycloneDX SBOM
uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # pin v0.18.0
with:
path: publishers/python
format: cyclonedx-json
artifact-name: sbom-python-${{ needs.preflight.outputs.version }}.cdx.json
output-file: publishers/python/sbom/sbom-python-${{ needs.preflight.outputs.version }}.cdx.json
upload-artifact: false
upload-release-assets: false
# Digest-hashes for SLSA provenance. The SLSA reusable workflow
# wants base64-encoded `sha256sum` output.
- name: Compute digests for SLSA
id: digest-hash
working-directory: publishers/python
run: |
digests="$( \
(cd dist && sha256sum fernsicht-*.whl fernsicht-*.tar.gz) \
| base64 -w0)"
echo "digests=$digests" >> "$GITHUB_OUTPUT"
- name: Upload dist as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # pin v4.6.2
with:
name: python-dist
path: publishers/python/dist/
if-no-files-found: error
retention-days: 7
- name: Upload SBOM as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # pin v4.6.2
with:
name: python-sbom
path: publishers/python/sbom/
if-no-files-found: error
retention-days: 30
# ------------------------------------------------------------------
# Publish — OIDC-authenticated upload to PyPI. Runs ONLY on a green
# build. This is the commit point: PyPI is immutable after success.
# ------------------------------------------------------------------
publish:
needs: [preflight, build]
runs-on: ubuntu-latest
timeout-minutes: 10
# Write permission for the GH release step (SBOM + SLSA target).
permissions:
contents: write
id-token: write # PyPI OIDC trusted publisher
# The environment name is purely informational for the audit log
# + any required approval gate. Matches the "(any)" env selector
# in the PyPI trusted-publisher config.
environment:
name: pypi
url: https://pypi.org/project/fernsicht/${{ needs.preflight.outputs.version }}
steps:
- name: Download dist artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # pin v4.3.0
with:
name: python-dist
path: dist/
- name: Download SBOM artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # pin v4.3.0
with:
name: python-sbom
path: sbom/
- name: Publish to PyPI
# No `password` / `user` / `api-token` — the action mints an
# OIDC token from the id-token: write permission and exchanges
# it with PyPI for a short-lived upload token.
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # pin v1.12.4
with:
# Only distribution files (wheel + sdist) live in dist/. The
# SBOM is in a separate sbom/ dir so it doesn't get uploaded
# to PyPI (PyPI rejects non-distribution files).
packages-dir: dist/
skip-existing: false
# `print-hash: true` prints SHA256 of each uploaded file so
# we have a record in logs matching what PyPI shows.
print-hash: true
# Create a GH release for the Python SDK so the SBOM + SLSA
# provenance have a stable home outside PyPI (PyPI doesn't yet
# host attestations — PEP 740 is work-in-progress). Security-
# conscious users download + verify from here.
- name: Create GitHub release for attestations
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # pin v2.2.1
with:
tag_name: py/v${{ needs.preflight.outputs.version }}
name: fernsicht Python ${{ needs.preflight.outputs.version }}
draft: false
prerelease: ${{ contains(needs.preflight.outputs.version, '-') }}
generate_release_notes: true
body: |
# fernsicht Python ${{ needs.preflight.outputs.version }}
**Primary install channel: PyPI.**
```bash
pip install fernsicht==${{ needs.preflight.outputs.version }}
```
This GitHub Release hosts attestations (SBOM + SLSA v1.0
provenance) for the PyPI-published artifacts. Nothing to
download unless you're doing supply-chain verification.
## Verify provenance
```bash
slsa-verifier verify-artifact \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/${{ github.repository }} \
--source-tag py/v${{ needs.preflight.outputs.version }} \
fernsicht-${{ needs.preflight.outputs.version }}-py3-none-any.whl
```
files: |
sbom/sbom-python-*.cdx.json
# ------------------------------------------------------------------
# SLSA v1.0 provenance for the Python SDK artifacts. Attached to a
# GH release (not PyPI — PyPI doesn't accept attestations yet as of
# the time of writing; they're working on it via PEP 740).
# ------------------------------------------------------------------
provenance:
needs: [preflight, build, publish]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 # tag pin (SHA pins rejected by SLSA self-identity check; v2.1.0 tag is protected by SLSA maintainers)
with:
base64-subjects: ${{ needs.build.outputs.digests }}
upload-assets: true
upload-tag-name: ${{ needs.preflight.outputs.tag }}
# See cli-release.yml for rationale; required even for public
# repos due to a v2.0.0 private-detection quirk.
private-repository: true
# Non-blocking for the same reason as cli-release.yml. Python
# release's primary artifact is PyPI (immutable); SLSA
# provenance is a GH-release-only attachment.
continue-on-error: true