tooprolix 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tooprolix-0.5.1/.gitattributes +2 -0
- tooprolix-0.5.1/.github/FUNDING.yml +4 -0
- tooprolix-0.5.1/.github/expected-tag-jobs.txt +41 -0
- tooprolix-0.5.1/.github/workflows/build-artifacts.yml +587 -0
- tooprolix-0.5.1/.github/workflows/ci.yml +395 -0
- tooprolix-0.5.1/.github/workflows/release-contract.yml +30 -0
- tooprolix-0.5.1/.github/workflows/release-plz.yml +189 -0
- tooprolix-0.5.1/.gitignore +45 -0
- tooprolix-0.5.1/CHANGELOG.md +172 -0
- tooprolix-0.5.1/CONTRIBUTING.md +273 -0
- tooprolix-0.5.1/Cargo.lock +994 -0
- tooprolix-0.5.1/Cargo.toml +186 -0
- tooprolix-0.5.1/LICENSE +21 -0
- tooprolix-0.5.1/Makefile +269 -0
- tooprolix-0.5.1/PKG-INFO +124 -0
- tooprolix-0.5.1/README.md +100 -0
- tooprolix-0.5.1/THIRD-PARTY-LICENSES.html +1558 -0
- tooprolix-0.5.1/about.hbs +20 -0
- tooprolix-0.5.1/about.toml +13 -0
- tooprolix-0.5.1/assets/tooprolix.gif +0 -0
- tooprolix-0.5.1/assets/tooprolix.png +0 -0
- tooprolix-0.5.1/build.rs +198 -0
- tooprolix-0.5.1/docs/cli-contract.md +201 -0
- tooprolix-0.5.1/docs/rules-and-configuration.md +186 -0
- tooprolix-0.5.1/examples/panic_is_a_controlled_exit.rs +22 -0
- tooprolix-0.5.1/pyproject.toml +337 -0
- tooprolix-0.5.1/release-plz.toml +82 -0
- tooprolix-0.5.1/rust-toolchain.toml +23 -0
- tooprolix-0.5.1/scripts/check_artifact.py +262 -0
- tooprolix-0.5.1/scripts/check_release_manifest.py +111 -0
- tooprolix-0.5.1/scripts/check_tag_jobs.py +254 -0
- tooprolix-0.5.1/scripts/coverage_report.py +337 -0
- tooprolix-0.5.1/scripts/install-smoke.sh +288 -0
- tooprolix-0.5.1/scripts/transform_readme.py +380 -0
- tooprolix-0.5.1/src/cli.rs +1877 -0
- tooprolix-0.5.1/src/config.rs +1083 -0
- tooprolix-0.5.1/src/detect/duplicate.rs +2533 -0
- tooprolix-0.5.1/src/detect/volume.rs +616 -0
- tooprolix-0.5.1/src/detect.rs +23 -0
- tooprolix-0.5.1/src/extract.rs +2178 -0
- tooprolix-0.5.1/src/finding.rs +913 -0
- tooprolix-0.5.1/src/lib.rs +41 -0
- tooprolix-0.5.1/src/main.rs +16 -0
- tooprolix-0.5.1/src/rules.rs +1422 -0
- tooprolix-0.5.1/src/snapshots/tooprolix__extract__tests__extracts_the_python_fixture.snap +69 -0
- tooprolix-0.5.1/tests/adversarial_bench.rs +650 -0
- tooprolix-0.5.1/tests/cli.rs +3779 -0
- tooprolix-0.5.1/tests/detect.rs +123 -0
- tooprolix-0.5.1/tests/extract.rs +51 -0
- tooprolix-0.5.1/tests/fixtures/broken/long_docstring.py +25 -0
- tooprolix-0.5.1/tests/fixtures/broken/syntax_error.py +5 -0
- tooprolix-0.5.1/tests/fixtures/clean/ledger.py +11 -0
- tooprolix-0.5.1/tests/fixtures/clean/settlement.py +4 -0
- tooprolix-0.5.1/tests/fixtures/dup-corpus/client.py +5 -0
- tooprolix-0.5.1/tests/fixtures/dup-corpus/config.py +27 -0
- tooprolix-0.5.1/tests/fixtures/dup-corpus/legacy.py +21 -0
- tooprolix-0.5.1/tests/fixtures/dup-corpus/poller.py +4 -0
- tooprolix-0.5.1/tests/fixtures/dup-corpus/worker.py +4 -0
- tooprolix-0.5.1/tests/fixtures/extract/sample.py +63 -0
- tooprolix-0.5.1/tests/fixtures/optout/comment_blanket.py +21 -0
- tooprolix-0.5.1/tests/fixtures/optout/comment_marked.py +21 -0
- tooprolix-0.5.1/tests/fixtures/optout/comment_mistyped.py +21 -0
- tooprolix-0.5.1/tests/fixtures/optout/comment_unmarked.py +20 -0
- tooprolix-0.5.1/tests/fixtures/optout/comment_wrong_code.py +21 -0
- tooprolix-0.5.1/tests/fixtures/optout/docstring_marked.py +23 -0
- tooprolix-0.5.1/tests/fixtures/optout/docstring_unmarked.py +22 -0
- tooprolix-0.5.1/tests/ruff_compatibility.rs +129 -0
- tooprolix-0.5.1/tests/unit/test_bench.py +422 -0
- tooprolix-0.5.1/tests/unit/test_check_artifact.py +376 -0
- tooprolix-0.5.1/tests/unit/test_check_release_manifest.py +85 -0
- tooprolix-0.5.1/tests/unit/test_check_tag_jobs.py +452 -0
- tooprolix-0.5.1/tests/unit/test_classification_artifact.py +725 -0
- tooprolix-0.5.1/tests/unit/test_coverage_report.py +508 -0
- tooprolix-0.5.1/tests/unit/test_duplicates.py +255 -0
- tooprolix-0.5.1/tests/unit/test_filters.py +165 -0
- tooprolix-0.5.1/tests/unit/test_measure.py +314 -0
- tooprolix-0.5.1/tests/unit/test_restatement_verdict.py +233 -0
- tooprolix-0.5.1/tests/unit/test_sample_clusters.py +305 -0
- tooprolix-0.5.1/tests/unit/test_transform_readme.py +419 -0
- tooprolix-0.5.1/tests/unit/test_units.py +615 -0
- tooprolix-0.5.1/tests/unit/test_workflow_contracts.py +1070 -0
- tooprolix-0.5.1/tests/volume_corpus.rs +275 -0
- tooprolix-0.5.1/uv.lock +411 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# EVERY JOB THAT MUST HAVE CONCLUDED `success` AT A `v*` TAG BEFORE PUBLISHING IS APPROVED.
|
|
2
|
+
#
|
|
3
|
+
# The expectation is pinned here so that shrinking it is a reviewable diff.
|
|
4
|
+
# `scripts/check_tag_jobs.py` compares it to a tag run's prerequisite jobs by SET EQUALITY, never
|
|
5
|
+
# "contains", and refuses an empty list outright — an expectation of nothing is satisfied by
|
|
6
|
+
# nothing. The environment-gated `Build artifacts / Publish to PyPI` job stays outside the list,
|
|
7
|
+
# but is not optional: the verifier requires exactly one pending instance before approval and one
|
|
8
|
+
# successful instance in `--postupload` mode.
|
|
9
|
+
#
|
|
10
|
+
# `[section]` headers are `workflow_name` as the Actions API reports it; the lines under them are
|
|
11
|
+
# `.name` values, not YAML job ids, which differ for a matrix job. Pairing the two is what stops a
|
|
12
|
+
# run of some other workflow satisfying the expectation by carrying the right job names.
|
|
13
|
+
#
|
|
14
|
+
# gh api repos/golyshevskii/tooprolix/actions/runs/<id>/jobs --paginate > ci.json
|
|
15
|
+
# python scripts/check_tag_jobs.py --tag-sha "$(git rev-parse 'v0.4.8^{commit}')" \
|
|
16
|
+
# --tag-name v0.4.8 ci.json artifacts.json
|
|
17
|
+
#
|
|
18
|
+
# A `v*` tag fires exactly two workflows: `CI` and `Build artifacts`. `Release-plz` triggers only on
|
|
19
|
+
# pushes to `main`, and `release contract` is `pull_request_target`.
|
|
20
|
+
#
|
|
21
|
+
# `coverage` is listed although it is NOT a required status check: it runs at the tag, and under set
|
|
22
|
+
# equality omitting it would fail an otherwise complete run. Required-check membership is decided by
|
|
23
|
+
# `ci-required`'s `needs:`.
|
|
24
|
+
#
|
|
25
|
+
# Run `check_tag_jobs.py` before approving the `pypi` environment. The manifest below is the last
|
|
26
|
+
# required preapproval job. After upload, run the same verifier with `--postupload`; Publish is
|
|
27
|
+
# deliberately not allowed to certify itself as a prerequisite.
|
|
28
|
+
|
|
29
|
+
[CI]
|
|
30
|
+
ci-python
|
|
31
|
+
ci-rust
|
|
32
|
+
cargo-doc
|
|
33
|
+
coverage
|
|
34
|
+
ci-required
|
|
35
|
+
|
|
36
|
+
[Build artifacts]
|
|
37
|
+
sdist (+ the wheel built from it)
|
|
38
|
+
wheel linux-x86_64
|
|
39
|
+
wheel macos-arm64
|
|
40
|
+
wheel windows-x86_64
|
|
41
|
+
PyPI release manifest
|
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
name: Build artifacts
|
|
2
|
+
|
|
3
|
+
# Pull requests and main builds are reversible: they build, smoke and retain artifacts but never
|
|
4
|
+
# publish. Only a successful `v*` tag build reaches the manifest and environment-gated PyPI jobs.
|
|
5
|
+
#
|
|
6
|
+
# Stated precisely, because the publishing task inherits this reasoning: a credential IS
|
|
7
|
+
# provisioned. `permissions: contents: read` gives the job a read-scoped `GITHUB_TOKEN`, and
|
|
8
|
+
# `actions/checkout` reads it to clone — `persist-credentials: false` only keeps it out of
|
|
9
|
+
# `.git/config` afterwards, it does not stop the checkout step receiving it. What is absent is
|
|
10
|
+
# no `secrets.*` is referenced anywhere in this file. The workflow default is read-only; the final
|
|
11
|
+
# publish job alone receives `id-token: write`, after the immutable manifest has been uploaded.
|
|
12
|
+
#
|
|
13
|
+
# # Two axes, decided separately: the BRANCH axis is filtered, the PATH axis is not
|
|
14
|
+
#
|
|
15
|
+
# `workflow_dispatch` alone is not enough: GitHub reads the dispatch definition from the default
|
|
16
|
+
# branch, so on a feature branch that adds this file manual dispatch is not available at all.
|
|
17
|
+
#
|
|
18
|
+
# THE BRANCH AXIS IS NARROWED TO `main`, `v*` AND PULL REQUESTS. Under the bare `push:` this file
|
|
19
|
+
# used to carry, the `v0.4.6` cycle ran six full artifact matrices of which at most two were
|
|
20
|
+
# load-bearing — about 204 of its 226 billed minutes rebuilt trees already built or never released.
|
|
21
|
+
# The cause is the branch axis: GitHub delivers a branch-CREATION push for every release-plz branch,
|
|
22
|
+
# so the base tip is built before the release commit even exists.
|
|
23
|
+
#
|
|
24
|
+
# `pull_request` is the preflight, and what it grades is named rather than assumed: it checks out
|
|
25
|
+
# `refs/pull/N/merge`, the MERGE tree, which is not necessarily the tree that gets tagged (for
|
|
26
|
+
# `v0.4.6`, `3a1f67f` against `5d0ceef`). The `v*` tag is the authoritative matrix. The alternative
|
|
27
|
+
# `push: branches: ['release-plz-**']` was rejected because a branch pattern also matches the
|
|
28
|
+
# branch-CREATION event — the exact duplicate this filter exists to delete.
|
|
29
|
+
#
|
|
30
|
+
# THE `paths:` FILTER WAS REMOVED, AND ITS REMOVAL IS THE FIX. It listed five files —
|
|
31
|
+
# `Cargo.toml`, `pyproject.toml`, this workflow, and the two scripts — under a header claiming those
|
|
32
|
+
# five "decide what an artifact contains". They do not: `README.md` is the description PyPI renders,
|
|
33
|
+
# `build.rs` decides the date, `src/**` and `Cargo.lock` are the binary, `LICENSE` and `docs/**` ride
|
|
34
|
+
# in the archives. A README-only change — **precisely the change that breaks the transformer** —
|
|
35
|
+
# skipped this workflow entirely, so not one of its checks could go red on it.
|
|
36
|
+
#
|
|
37
|
+
# Replacing the list with a longer list would only move the boundary: the next file nobody thought
|
|
38
|
+
# of has the same effect, and the list rots silently because nothing tells you it is short. An
|
|
39
|
+
# unfiltered trigger cannot be wrong about what matters. The cost is named rather than hidden: four
|
|
40
|
+
# more jobs per push, roughly doubling the compile work `ci.yml` already does. Buy the filter back
|
|
41
|
+
# only with a measurement showing that cost is real.
|
|
42
|
+
#
|
|
43
|
+
# # The supported matrix, and why it is three platforms rather than seventeen
|
|
44
|
+
#
|
|
45
|
+
# `linux x86_64`, `macOS arm64`, `Windows x86_64`, plus an sdist for everything else (Decisions
|
|
46
|
+
# #19.3). All three run natively on free hosted runners, so **every published wheel is smoke-tested
|
|
47
|
+
# on the operating system it claims**. Our reference, ruff, publishes 17 wheels of which 5 have no
|
|
48
|
+
# install-smoke at all — its `macos-x86_64` job has no `Test` step whatsoever — and that is the
|
|
49
|
+
# reason its width is deliberately NOT copied. Linux aarch64 and macOS x86_64 would mean QEMU or an
|
|
50
|
+
# untested wheel; neither is taken on speculation.
|
|
51
|
+
#
|
|
52
|
+
# # The floor of each platform is DECLARED, not inherited from the runner
|
|
53
|
+
#
|
|
54
|
+
# A wheel that builds on the newest runner and is tagged for the oldest OS its platform tag names
|
|
55
|
+
# fails at the user's dynamic loader, which is the one failure no gate here would otherwise see:
|
|
56
|
+
#
|
|
57
|
+
# * linux — `manylinux: 2_17`, i.e. glibc >= 2.17 (CentOS 7 / Ubuntu 14.04). The tag the wheel
|
|
58
|
+
# carries is checked below rather than assumed.
|
|
59
|
+
# * macOS — `MACOSX_DEPLOYMENT_TARGET: "11.0"`. maturin defaults to exactly this for
|
|
60
|
+
# aarch64-apple-darwin (observed in its own output) and 11.0 is also the first macOS with Apple
|
|
61
|
+
# silicon, so nothing older can run an arm64 build anyway. It is written down because a default
|
|
62
|
+
# is not a promise.
|
|
63
|
+
# * windows — x86_64-pc-windows-msvc. There is no deployment-target knob; the floor is whatever
|
|
64
|
+
# the MSVC runtime the toolchain links against supports, which is Windows 10 for the pinned
|
|
65
|
+
# Rust. Said out loud rather than left blank.
|
|
66
|
+
|
|
67
|
+
on:
|
|
68
|
+
workflow_dispatch:
|
|
69
|
+
pull_request:
|
|
70
|
+
push:
|
|
71
|
+
branches: [main]
|
|
72
|
+
tags: ['v*']
|
|
73
|
+
|
|
74
|
+
permissions:
|
|
75
|
+
contents: read
|
|
76
|
+
|
|
77
|
+
# Keyed on the COMMIT, not the ref. One logical event used to deliver two pushes to one new ref
|
|
78
|
+
# seconds apart — release-plz creates the branch at the base tip and pushes the release commit.
|
|
79
|
+
# Keyed on the ref, the later run cancels the earlier one; on PR #37 that killed the release
|
|
80
|
+
# commit's build while the base tip's survived.
|
|
81
|
+
#
|
|
82
|
+
# The branch filter above now stops those pushes reaching this workflow, so that race no longer
|
|
83
|
+
# fires. THE KEY STILL DOES NOT GO BACK TO `github.ref`: keying on the commit is correct whatever
|
|
84
|
+
# events are accepted. The event is also part of the key: duplicate runs of one event and SHA still
|
|
85
|
+
# collapse, while a manual dispatch at a tag SHA cannot cancel the authoritative tag-push run.
|
|
86
|
+
# Reverting either dimension would re-arm one of those races.
|
|
87
|
+
# Enforced by test_an_artifact_build_is_only_cancelled_by_the_same_event_and_commit.
|
|
88
|
+
concurrency:
|
|
89
|
+
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.sha }}
|
|
90
|
+
cancel-in-progress: true
|
|
91
|
+
|
|
92
|
+
env:
|
|
93
|
+
# The distribution floor, so every install-smoke below exercises the lowest interpreter
|
|
94
|
+
# `requires-python` promises. Not a matrix: the wheel is `py3-none-<platform>`, so only the
|
|
95
|
+
# installing interpreter is worth moving.
|
|
96
|
+
PYTHON_VERSION: "3.11"
|
|
97
|
+
# Pinned, exactly as ruff pins it. maturin decides the wheel tag and the archive layout, so an
|
|
98
|
+
# unpinned "latest" would let the shape of the published artifact change without a commit here.
|
|
99
|
+
MATURIN_VERSION: v1.14.1
|
|
100
|
+
CARGO_INCREMENTAL: "0"
|
|
101
|
+
CARGO_NET_RETRY: "10"
|
|
102
|
+
CARGO_TERM_COLOR: always
|
|
103
|
+
RUSTUP_MAX_RETRIES: "10"
|
|
104
|
+
|
|
105
|
+
jobs:
|
|
106
|
+
sdist:
|
|
107
|
+
name: sdist (+ the wheel built from it)
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
timeout-minutes: 30
|
|
110
|
+
steps:
|
|
111
|
+
- name: Checkout
|
|
112
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
113
|
+
with:
|
|
114
|
+
persist-credentials: false
|
|
115
|
+
|
|
116
|
+
- name: Install uv
|
|
117
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
118
|
+
with:
|
|
119
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
120
|
+
enable-cache: true
|
|
121
|
+
|
|
122
|
+
- name: Install cargo-about
|
|
123
|
+
uses: taiki-e/install-action@18b1216eba7f8039b0f8d131d5473787f0edce68 # v2.85.3
|
|
124
|
+
with:
|
|
125
|
+
tool: cargo-about@0.9.1
|
|
126
|
+
fallback: none
|
|
127
|
+
|
|
128
|
+
- name: The third-party licence bundle matches Cargo.lock
|
|
129
|
+
run: |
|
|
130
|
+
set -euo pipefail
|
|
131
|
+
cargo fetch --locked
|
|
132
|
+
cargo about generate --frozen --fail --output-file "$RUNNER_TEMP/THIRD-PARTY-LICENSES.html" about.hbs
|
|
133
|
+
diff -u THIRD-PARTY-LICENSES.html "$RUNNER_TEMP/THIRD-PARTY-LICENSES.html"
|
|
134
|
+
|
|
135
|
+
# THE ONLY WAY A RELEASE ARTIFACT LEARNS WHAT IT WAS BUILT FROM.
|
|
136
|
+
#
|
|
137
|
+
# `tooprolix --version` prints the COMMIT date, resolved by build.rs as SOURCE_DATE_EPOCH ->
|
|
138
|
+
# git -> `unknown`. An sdist carries no `.git`, and since `add-version-and-rules-commands` it
|
|
139
|
+
# will not borrow a surrounding repository's either — so without this line the wheel built
|
|
140
|
+
# from the sdist below prints `tooprolix 0.4.1 (unknown)`. Measured, not predicted: that is
|
|
141
|
+
# exactly what it printed, and it passed every other assertion in the smoke.
|
|
142
|
+
#
|
|
143
|
+
# `>> "$GITHUB_ENV"` so it reaches the maturin steps, which run in their own shells.
|
|
144
|
+
- name: Export SOURCE_DATE_EPOCH from the commit
|
|
145
|
+
run: |
|
|
146
|
+
echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
|
147
|
+
echo "commit date: $(git log -1 --format=%cs)"
|
|
148
|
+
|
|
149
|
+
# Before maturin reads `readme = "README.md"`. The script rewrites EVERY relative address
|
|
150
|
+
# into an absolute GitHub URL and FAILS THE BUILD if the README is not the shape it expects —
|
|
151
|
+
# `twine check` validates the METADATA envelope and never the links inside the description.
|
|
152
|
+
- name: Rewrite the README for the PyPI project page
|
|
153
|
+
run: uv run --no-project --with 'readme_renderer[md]' python scripts/transform_readme.py README.md
|
|
154
|
+
|
|
155
|
+
- name: Build sdist
|
|
156
|
+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
157
|
+
with:
|
|
158
|
+
maturin-version: ${{ env.MATURIN_VERSION }}
|
|
159
|
+
command: sdist
|
|
160
|
+
args: --out dist
|
|
161
|
+
|
|
162
|
+
# AC1: the sdist must be self-sufficient. A wheel built from the unpacked tarball, in a
|
|
163
|
+
# directory with no `.git` and no repository around it, is the only honest test of that — a
|
|
164
|
+
# `cargo package --list` shows what is inside but not whether it builds.
|
|
165
|
+
#
|
|
166
|
+
# THE WHEEL THIS STEP PRODUCES IS EVIDENCE, NOT A PUBLISHABLE ARTIFACT, and its own tag says
|
|
167
|
+
# so: measured in run 30605245528 it came out `py3-none-manylinux_2_34_x86_64`, because it is
|
|
168
|
+
# built directly on the runner rather than in the manylinux 2_17 container the matrix leg uses.
|
|
169
|
+
# glibc 2.34 is a far higher floor than the one this project promises. It lives in
|
|
170
|
+
# `dist-from-sdist/` and never in `dist/` for exactly that reason. Whoever adds the publish job
|
|
171
|
+
# uploads `dist/` and the matrix artifacts — never this one.
|
|
172
|
+
- name: Build a wheel FROM the sdist
|
|
173
|
+
run: |
|
|
174
|
+
set -euo pipefail
|
|
175
|
+
mkdir -p from-sdist && tar xzf dist/tooprolix-*.tar.gz -C from-sdist
|
|
176
|
+
test ! -e from-sdist/tooprolix-*/.git
|
|
177
|
+
uvx "maturin==${MATURIN_VERSION#v}" build --release --locked \
|
|
178
|
+
--manifest-path from-sdist/tooprolix-*/Cargo.toml \
|
|
179
|
+
--out dist-from-sdist --compatibility pypi
|
|
180
|
+
|
|
181
|
+
# EXACTLY ONE FILE PER DIRECTORY, OR THIS FAILS. `$(echo dist/*.whl)` looks like "the
|
|
182
|
+
# wheel" and is really a space-joined list: run 30604813542 had a stray wheel in `dist/` and
|
|
183
|
+
# handed uv `<macos wheel> <linux wheel>` as one path, which then failed with a URL-encoded
|
|
184
|
+
# space in it. That is a guard failing open — the step before it had already "passed" on the
|
|
185
|
+
# same two-file string, because `basename` on it returns the last one. Counting first is what
|
|
186
|
+
# makes the later steps grade one named artifact instead of whatever the glob swept up.
|
|
187
|
+
- name: Resolve the artifacts, exactly one of each
|
|
188
|
+
run: |
|
|
189
|
+
set -euo pipefail
|
|
190
|
+
for pair in "SDIST:dist/*.tar.gz" "WHEEL_FROM_SDIST:dist-from-sdist/*.whl"; do
|
|
191
|
+
name="${pair%%:*}"
|
|
192
|
+
# shellcheck disable=SC2206
|
|
193
|
+
found=(${pair#*:})
|
|
194
|
+
if [ "${#found[@]}" -ne 1 ] || [ ! -f "${found[0]}" ]; then
|
|
195
|
+
echo "FAIL: expected exactly one ${pair#*:}, found ${#found[@]}: ${found[*]}" >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
echo "$name=${found[0]}" >> "$GITHUB_ENV"
|
|
199
|
+
echo "$name = ${found[0]}"
|
|
200
|
+
done
|
|
201
|
+
|
|
202
|
+
- name: twine check
|
|
203
|
+
run: uvx twine check --strict "$SDIST" "$WHEEL_FROM_SDIST"
|
|
204
|
+
|
|
205
|
+
# AC3 AND AC8, GATED IN CODE RATHER THAN OBSERVED IN A REPORT. `twine check --strict`
|
|
206
|
+
# validates the METADATA envelope and the description's markup; it never asks what the licence
|
|
207
|
+
# says, whether a LICENSE file is actually in the archive, whether the sdist carries the two
|
|
208
|
+
# documents the README links to, or which file the shipped description came from. All three
|
|
209
|
+
# exploits were measured green under every other gate here — see the script's own header.
|
|
210
|
+
#
|
|
211
|
+
# README.md on disk is the TRANSFORMED one at this point, which is the object AC8 names.
|
|
212
|
+
- name: The archives carry MIT, required licence files, docs/, and the transformed README
|
|
213
|
+
run: |
|
|
214
|
+
uv run --no-project python scripts/check_artifact.py "$SDIST" README.md
|
|
215
|
+
uv run --no-project python scripts/check_artifact.py "$WHEEL_FROM_SDIST" README.md
|
|
216
|
+
|
|
217
|
+
# AC4 for the source path, with an ORACLE rather than a shape check: the expected date is
|
|
218
|
+
# `git log -1 --format=%cs`, computed here from the checkout, so a binary built from any other
|
|
219
|
+
# commit fails. Without it the smoke accepted `tooprolix 0.4.1 (2000-01-01)` — measured.
|
|
220
|
+
- name: install-smoke the wheel built from the sdist
|
|
221
|
+
run: scripts/install-smoke.sh "$WHEEL_FROM_SDIST" "$(git log -1 --format=%cs)"
|
|
222
|
+
|
|
223
|
+
# THE NON-CIRCULAR HALF, AND IT PINS AN ACCEPTED LIMITATION RATHER THAN HIDING ONE.
|
|
224
|
+
#
|
|
225
|
+
# Every other step here runs with SOURCE_DATE_EPOCH exported, so "the sdist produces a dated
|
|
226
|
+
# binary" was only ever proved in an environment that handed it the date. A real
|
|
227
|
+
# `pip install tooprolix` from the sdist — what a user on a platform outside the matrix gets —
|
|
228
|
+
# has no such environment and no `.git`, so `build.rs` falls through to `unknown`.
|
|
229
|
+
#
|
|
230
|
+
# Measured, and ACCEPTED: `unknown` is what `build.rs` was designed to say when it cannot know
|
|
231
|
+
# its provenance, in preference to borrowing a surrounding repository's date (the bug task #7
|
|
232
|
+
# fixed). `--help` documents it to users. So the behaviour is pinned as an expectation instead
|
|
233
|
+
# of being left as something the matrix quietly never exercises: `env -u SOURCE_DATE_EPOCH`
|
|
234
|
+
# plus the literal expected date `unknown`. The day an offline sdist starts printing something
|
|
235
|
+
# else — a wall clock, a borrowed commit — this step goes red.
|
|
236
|
+
- name: install-smoke the sdist offline, where the date is honestly unknown
|
|
237
|
+
run: env -u SOURCE_DATE_EPOCH scripts/install-smoke.sh "$SDIST" unknown
|
|
238
|
+
|
|
239
|
+
- name: Upload
|
|
240
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
241
|
+
with:
|
|
242
|
+
name: artifacts-sdist
|
|
243
|
+
path: dist/*.tar.gz
|
|
244
|
+
|
|
245
|
+
# THE BINARIES THIS JOB UPLOADS ARE WHAT ACTUALLY GETS PUBLISHED, so this job proves for
|
|
246
|
+
# itself which commit it built. Change this workflow's checkout to `ref: main` and every leg
|
|
247
|
+
# builds the wrong tree while every REST job still self-reports `head_sha=<tag>` — the tag-job
|
|
248
|
+
# grader would accept it, because `head_sha` is a label the run attaches to itself.
|
|
249
|
+
#
|
|
250
|
+
# LAST, after the artifacts are built and smoked, so a second checkout inserted between the
|
|
251
|
+
# checkout and the build cannot leave a correct-looking record of a tree nobody built. Same
|
|
252
|
+
# step, same shell, as every job in `ci.yml`.
|
|
253
|
+
- name: The gates above ran on the commit this event names
|
|
254
|
+
shell: bash
|
|
255
|
+
run: |
|
|
256
|
+
set -euo pipefail
|
|
257
|
+
sha=$(git rev-parse HEAD)
|
|
258
|
+
echo "graded commit $sha, tree $(git rev-parse "HEAD^{tree}")"
|
|
259
|
+
if [ "$sha" != "${GITHUB_SHA:-}" ]; then
|
|
260
|
+
echo "FAIL: this job graded $sha, but the event names ${GITHUB_SHA:-<unset>}." >&2
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
# THE THREE WHEEL LEGS RUN ON THE EVENTS THAT PRECEDE A TAG, NOT ON EVERY PULL REQUEST.
|
|
265
|
+
#
|
|
266
|
+
# Per PR, running them everywhere costs sdist 3 + linux 3 + macos 20 (the x10 private-repo
|
|
267
|
+
# multiplier) + windows 6 = 32 billed minutes, of which the wheels are 29. An ordinary PR keeps
|
|
268
|
+
# `sdist`, which grades the README transform, the archive contents and the offline-sdist
|
|
269
|
+
# behaviour. A cross-platform break is still caught before the tag, because the release-plz PR
|
|
270
|
+
# runs the full matrix. Nothing is removed from the TAG matrix — each leg there is the only smoke
|
|
271
|
+
# test of the operating system its wheel tag claims.
|
|
272
|
+
#
|
|
273
|
+
# AN EVENT CONDITION, NOT A PATH ONE. A path filter would skip the wheels on a README-only change,
|
|
274
|
+
# precisely the change able to break the transformer that rewrites the description PyPI renders.
|
|
275
|
+
# `github.head_ref` is set only on `pull_request` and `startsWith` on the empty string is false,
|
|
276
|
+
# so the release-plz clause cannot leak into a push event.
|
|
277
|
+
#
|
|
278
|
+
# THESE LEGS MUST NEVER BE REGISTERED AS REQUIRED CHECKS. GitHub does not expand a skipped matrix
|
|
279
|
+
# job: on ordinary PR #52 it reported one `skipped` check named literally
|
|
280
|
+
# `wheel ${{ matrix.platform.name }}`, while release events report three platform-specific names.
|
|
281
|
+
# Any one of those contexts disappears on the other event. The required name is `ci-required`.
|
|
282
|
+
wheels:
|
|
283
|
+
needs: sdist
|
|
284
|
+
name: wheel ${{ matrix.platform.name }}
|
|
285
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
286
|
+
timeout-minutes: 30
|
|
287
|
+
if: >-
|
|
288
|
+
github.event_name == 'workflow_dispatch'
|
|
289
|
+
|| startsWith(github.ref, 'refs/tags/v')
|
|
290
|
+
|| startsWith(github.head_ref, 'release-plz-')
|
|
291
|
+
strategy:
|
|
292
|
+
# `fail-fast: false` so one red platform still leaves the other two measured. AC4's rule is
|
|
293
|
+
# "an artifact that could not be smoked is excluded from the matrix", and deciding that needs
|
|
294
|
+
# every platform's answer, not the first failure.
|
|
295
|
+
fail-fast: false
|
|
296
|
+
matrix:
|
|
297
|
+
platform:
|
|
298
|
+
- name: linux-x86_64
|
|
299
|
+
runner: ubuntu-latest
|
|
300
|
+
target: x86_64-unknown-linux-gnu
|
|
301
|
+
manylinux: "2_17"
|
|
302
|
+
# The tag the wheel is REQUIRED to carry, checked against the file maturin produced.
|
|
303
|
+
# "it built on the runner" is not a floor.
|
|
304
|
+
expect_tag: py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64
|
|
305
|
+
- name: macos-arm64
|
|
306
|
+
runner: macos-15
|
|
307
|
+
target: aarch64-apple-darwin
|
|
308
|
+
manylinux: auto
|
|
309
|
+
expect_tag: py3-none-macosx_11_0_arm64
|
|
310
|
+
- name: windows-x86_64
|
|
311
|
+
runner: windows-latest
|
|
312
|
+
target: x86_64-pc-windows-msvc
|
|
313
|
+
manylinux: auto
|
|
314
|
+
expect_tag: py3-none-win_amd64
|
|
315
|
+
steps:
|
|
316
|
+
- name: Checkout
|
|
317
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
318
|
+
with:
|
|
319
|
+
persist-credentials: false
|
|
320
|
+
|
|
321
|
+
- name: Install uv
|
|
322
|
+
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
323
|
+
with:
|
|
324
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
325
|
+
enable-cache: true
|
|
326
|
+
|
|
327
|
+
- name: Export SOURCE_DATE_EPOCH from the commit
|
|
328
|
+
shell: bash
|
|
329
|
+
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
|
330
|
+
|
|
331
|
+
- name: Rewrite the README for the PyPI project page
|
|
332
|
+
shell: bash
|
|
333
|
+
run: uv run --no-project --with 'readme_renderer[md]' python scripts/transform_readme.py README.md
|
|
334
|
+
|
|
335
|
+
- name: Build wheel
|
|
336
|
+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
|
|
337
|
+
with:
|
|
338
|
+
maturin-version: ${{ env.MATURIN_VERSION }}
|
|
339
|
+
target: ${{ matrix.platform.target }}
|
|
340
|
+
manylinux: ${{ matrix.platform.manylinux }}
|
|
341
|
+
args: --release --locked --out dist --compatibility pypi
|
|
342
|
+
# THE MANYLINUX WHEEL BUILDS INSIDE A DOCKER CONTAINER, AND THE JOB'S ENVIRONMENT DOES
|
|
343
|
+
# NOT FOLLOW IT IN. Found by the smoke rather than by reading the action: run 30605047058
|
|
344
|
+
# was green through `Build wheel`, the tag check and `twine check`, and then
|
|
345
|
+
#
|
|
346
|
+
# FAIL: --version printed 'tooprolix 0.4.1 (unknown)'
|
|
347
|
+
#
|
|
348
|
+
# — the linux artifact did not know what commit it was built from, while the natively
|
|
349
|
+
# built macOS wheel in the same run printed a real date. Without the version assertion in
|
|
350
|
+
# `scripts/install-smoke.sh` this run would have uploaded that wheel as a pass. This is
|
|
351
|
+
# ignored on the macOS and Windows legs, which use no container.
|
|
352
|
+
docker-options: -e SOURCE_DATE_EPOCH
|
|
353
|
+
env:
|
|
354
|
+
# Declared rather than inherited — see the header. maturin already defaults to 11.0 here;
|
|
355
|
+
# a default is not a promise, and this is the line that keeps the tag honest if it changes.
|
|
356
|
+
MACOSX_DEPLOYMENT_TARGET: "11.0"
|
|
357
|
+
|
|
358
|
+
# The count comes FIRST, and it is not tidiness. `$(echo dist/*.whl)` is a space-joined
|
|
359
|
+
# list, and `basename` on such a list silently answers with the last entry — so in run
|
|
360
|
+
# 30604813542 a check reported PASS on a directory holding a foreign macOS wheel beside the
|
|
361
|
+
# real one, and the failure only surfaced two steps later inside uv. A guard that grades
|
|
362
|
+
# whatever the glob swept up is not grading the artifact.
|
|
363
|
+
- name: Resolve the built wheel, exactly one
|
|
364
|
+
shell: bash
|
|
365
|
+
run: |
|
|
366
|
+
set -euo pipefail
|
|
367
|
+
# shellcheck disable=SC2206
|
|
368
|
+
built=(dist/*.whl)
|
|
369
|
+
if [ "${#built[@]}" -ne 1 ] || [ ! -f "${built[0]}" ]; then
|
|
370
|
+
echo "FAIL: expected exactly one wheel in dist/, found ${#built[@]}: ${built[*]}" >&2
|
|
371
|
+
exit 1
|
|
372
|
+
fi
|
|
373
|
+
echo "WHEEL=${built[0]}" >> "$GITHUB_ENV"
|
|
374
|
+
echo "built: ${built[0]}"
|
|
375
|
+
|
|
376
|
+
- name: twine check
|
|
377
|
+
shell: bash
|
|
378
|
+
run: uvx twine check --strict "$WHEEL"
|
|
379
|
+
|
|
380
|
+
# AC2 and AC3/AC8 in one committed gate — `scripts/check_artifact.py`, which the sdist job
|
|
381
|
+
# runs too and which has its own tests.
|
|
382
|
+
#
|
|
383
|
+
# `expect_tag` is declared in the matrix above rather than derived here, so it is fixed
|
|
384
|
+
# independently of whatever the build produced. The script requires BOTH the archive's
|
|
385
|
+
# `.dist-info/WHEEL` and the filename to agree with it: the in-archive read is what a resolver
|
|
386
|
+
# honours, the filename read is what catches a rename.
|
|
387
|
+
#
|
|
388
|
+
# It compares TAG SETS, not strings. `.dist-info/WHEEL` carries one `Tag:` line per tag
|
|
389
|
+
# while the filename carries PEP 425's compressed set, and run 30613111390 went red on the
|
|
390
|
+
# linux leg for exactly that difference in representation. macOS and Windows wheels are
|
|
391
|
+
# single-tag, which is why only CI could find it.
|
|
392
|
+
#
|
|
393
|
+
# WHAT THIS DOES NOT CHECK, said plainly: no symbol versions are audited. Nothing opens the
|
|
394
|
+
# ELF to compare its `GLIBC_*` requirements against the 2.17 the tag promises — **the
|
|
395
|
+
# manylinux build container IS the whole guarantee** for that, being a sysroot old enough that
|
|
396
|
+
# a newer symbol cannot be linked in the first place. If the linux leg ever stops going
|
|
397
|
+
# through `manylinux: 2_17`, this keeps passing while the promise breaks, and `auditwheel`
|
|
398
|
+
# becomes the missing step.
|
|
399
|
+
- name: The wheel is the promised tag and carries MIT, its LICENSE, and the transformed README
|
|
400
|
+
shell: bash
|
|
401
|
+
run: |
|
|
402
|
+
uv run --no-project python scripts/check_artifact.py "$WHEEL" README.md \
|
|
403
|
+
--expect-tag "${{ matrix.platform.expect_tag }}"
|
|
404
|
+
|
|
405
|
+
- name: install-smoke
|
|
406
|
+
shell: bash
|
|
407
|
+
run: scripts/install-smoke.sh "$WHEEL" "$(git log -1 --format=%cs)"
|
|
408
|
+
|
|
409
|
+
- name: Upload
|
|
410
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
411
|
+
with:
|
|
412
|
+
name: artifacts-${{ matrix.platform.name }}
|
|
413
|
+
path: dist/*.whl
|
|
414
|
+
|
|
415
|
+
- name: The gates above ran on the commit this event names
|
|
416
|
+
shell: bash
|
|
417
|
+
run: |
|
|
418
|
+
set -euo pipefail
|
|
419
|
+
sha=$(git rev-parse HEAD)
|
|
420
|
+
echo "graded commit $sha, tree $(git rev-parse "HEAD^{tree}")"
|
|
421
|
+
if [ "$sha" != "${GITHUB_SHA:-}" ]; then
|
|
422
|
+
echo "FAIL: this job graded $sha, but the event names ${GITHUB_SHA:-<unset>}." >&2
|
|
423
|
+
exit 1
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
release-manifest:
|
|
427
|
+
name: PyPI release manifest
|
|
428
|
+
needs: [sdist, wheels]
|
|
429
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
430
|
+
runs-on: ubuntu-latest
|
|
431
|
+
timeout-minutes: 10
|
|
432
|
+
steps:
|
|
433
|
+
- name: Checkout
|
|
434
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
435
|
+
with:
|
|
436
|
+
persist-credentials: false
|
|
437
|
+
|
|
438
|
+
- name: Download the four independently built artifacts
|
|
439
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
440
|
+
with:
|
|
441
|
+
pattern: artifacts-*
|
|
442
|
+
path: downloaded
|
|
443
|
+
|
|
444
|
+
- name: Assemble the immutable PyPI release candidate
|
|
445
|
+
shell: bash
|
|
446
|
+
run: |
|
|
447
|
+
set -euo pipefail
|
|
448
|
+
if [ "${GITHUB_EVENT_NAME:-}" != "push" ] \
|
|
449
|
+
|| [[ "${GITHUB_REF:-}" != refs/tags/v* || "${GITHUB_REF_NAME:-}" != v* ]]; then
|
|
450
|
+
echo "FAIL: release candidates require a v* tag push event." >&2
|
|
451
|
+
exit 1
|
|
452
|
+
fi
|
|
453
|
+
version="${GITHUB_REF_NAME#v}"
|
|
454
|
+
declared_version=$(sed -n '/^\[package\]/,/^\[/{s/^version = "\([^"]*\)"/\1/p;}' Cargo.toml)
|
|
455
|
+
if [ -z "$version" ] || [ "$version" != "$declared_version" ]; then
|
|
456
|
+
echo "FAIL: tag $GITHUB_REF_NAME does not match Cargo.toml version $declared_version." >&2
|
|
457
|
+
exit 1
|
|
458
|
+
fi
|
|
459
|
+
tag_sha=$(git rev-parse HEAD)
|
|
460
|
+
if [ "$tag_sha" != "${GITHUB_SHA:-}" ]; then
|
|
461
|
+
echo "FAIL: checkout $tag_sha does not match triggering SHA ${GITHUB_SHA:-<unset>}." >&2
|
|
462
|
+
exit 1
|
|
463
|
+
fi
|
|
464
|
+
tree_sha=$(git rev-parse 'HEAD^{tree}')
|
|
465
|
+
|
|
466
|
+
artifact_ids=(sdist linux-x86_64 macos-arm64 windows-x86_64)
|
|
467
|
+
filenames=(
|
|
468
|
+
"tooprolix-${version}.tar.gz"
|
|
469
|
+
"tooprolix-${version}-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
|
470
|
+
"tooprolix-${version}-py3-none-macosx_11_0_arm64.whl"
|
|
471
|
+
"tooprolix-${version}-py3-none-win_amd64.whl"
|
|
472
|
+
)
|
|
473
|
+
mapfile -t downloaded_entries < <(find downloaded -mindepth 1 -maxdepth 1 -print | sort)
|
|
474
|
+
if [ "${#downloaded_entries[@]}" -ne 4 ]; then
|
|
475
|
+
echo "FAIL: expected four downloaded artifacts, found ${#downloaded_entries[@]}." >&2
|
|
476
|
+
exit 1
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
mkdir dist
|
|
480
|
+
for index in "${!artifact_ids[@]}"; do
|
|
481
|
+
directory="downloaded/artifacts-${artifact_ids[$index]}"
|
|
482
|
+
expected="${filenames[$index]}"
|
|
483
|
+
mapfile -t members < <(find "$directory" -mindepth 1 -maxdepth 1 -print | sort)
|
|
484
|
+
if [ "${#members[@]}" -ne 1 ] || [ ! -f "${members[0]:-}" ] || [ "$(basename "${members[0]:-}")" != "$expected" ]; then
|
|
485
|
+
echo "FAIL: $directory must contain only $expected." >&2
|
|
486
|
+
exit 1
|
|
487
|
+
fi
|
|
488
|
+
cp -- "${members[0]}" "dist/$expected"
|
|
489
|
+
done
|
|
490
|
+
|
|
491
|
+
{
|
|
492
|
+
printf 'tag: %s\n' "$GITHUB_REF_NAME"
|
|
493
|
+
printf 'tag-target-sha: %s\n' "$tag_sha"
|
|
494
|
+
printf 'tree-sha: %s\n' "$tree_sha"
|
|
495
|
+
printf 'run-id: %s\n\n' "$GITHUB_RUN_ID"
|
|
496
|
+
printf 'size\tfilename\tsha256\n'
|
|
497
|
+
for filename in "${filenames[@]}"; do
|
|
498
|
+
size=$(wc -c < "dist/$filename" | tr -d ' ')
|
|
499
|
+
digest=$(sha256sum "dist/$filename" | cut -d ' ' -f 1)
|
|
500
|
+
printf '%s\t%s\t%s\n' "$size" "$filename" "$digest"
|
|
501
|
+
done
|
|
502
|
+
} > release-manifest.txt
|
|
503
|
+
cat release-manifest.txt
|
|
504
|
+
|
|
505
|
+
- name: Upload the immutable human-readable manifest
|
|
506
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
507
|
+
with:
|
|
508
|
+
name: release-manifest-${{ github.run_id }}
|
|
509
|
+
path: release-manifest.txt
|
|
510
|
+
|
|
511
|
+
- name: Upload the reusable release verifier
|
|
512
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
513
|
+
with:
|
|
514
|
+
name: release-verifier-${{ github.run_id }}
|
|
515
|
+
path: scripts/check_release_manifest.py
|
|
516
|
+
|
|
517
|
+
- name: Upload the immutable release candidate
|
|
518
|
+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
519
|
+
with:
|
|
520
|
+
name: release-candidate-${{ github.run_id }}
|
|
521
|
+
path: dist/*
|
|
522
|
+
|
|
523
|
+
- name: The gates above ran on the commit this event names
|
|
524
|
+
shell: bash
|
|
525
|
+
run: |
|
|
526
|
+
set -euo pipefail
|
|
527
|
+
sha=$(git rev-parse HEAD)
|
|
528
|
+
echo "graded commit $sha, tree $(git rev-parse "HEAD^{tree}")"
|
|
529
|
+
if [ "$sha" != "${GITHUB_SHA:-}" ]; then
|
|
530
|
+
echo "FAIL: this job graded $sha, but the event names ${GITHUB_SHA:-<unset>}." >&2
|
|
531
|
+
exit 1
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
publish-pypi:
|
|
535
|
+
name: Publish to PyPI
|
|
536
|
+
needs: [sdist, wheels, release-manifest]
|
|
537
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
538
|
+
runs-on: ubuntu-latest
|
|
539
|
+
timeout-minutes: 10
|
|
540
|
+
environment:
|
|
541
|
+
name: pypi
|
|
542
|
+
url: https://pypi.org/p/tooprolix
|
|
543
|
+
permissions:
|
|
544
|
+
contents: read
|
|
545
|
+
id-token: write
|
|
546
|
+
steps:
|
|
547
|
+
- name: Download the immutable manifest
|
|
548
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
549
|
+
with:
|
|
550
|
+
name: release-manifest-${{ github.run_id }}
|
|
551
|
+
path: .
|
|
552
|
+
|
|
553
|
+
- name: Download the release verifier from the manifest job
|
|
554
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
555
|
+
with:
|
|
556
|
+
name: release-verifier-${{ github.run_id }}
|
|
557
|
+
path: scripts
|
|
558
|
+
|
|
559
|
+
- name: Download the immutable release candidate
|
|
560
|
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
561
|
+
with:
|
|
562
|
+
name: release-candidate-${{ github.run_id }}
|
|
563
|
+
path: dist
|
|
564
|
+
|
|
565
|
+
- name: Verify the approved PyPI release candidate
|
|
566
|
+
shell: bash
|
|
567
|
+
run: |
|
|
568
|
+
set -euo pipefail
|
|
569
|
+
if [ "${GITHUB_EVENT_NAME:-}" != "push" ] \
|
|
570
|
+
|| [[ "${GITHUB_REF:-}" != refs/tags/v* || "${GITHUB_REF_NAME:-}" != v* ]]; then
|
|
571
|
+
echo "FAIL: publishing requires a v* tag push event." >&2
|
|
572
|
+
exit 1
|
|
573
|
+
fi
|
|
574
|
+
if ! grep -Fxq "tag: $GITHUB_REF_NAME" release-manifest.txt \
|
|
575
|
+
|| ! grep -Fxq "tag-target-sha: ${GITHUB_SHA:-}" release-manifest.txt \
|
|
576
|
+
|| ! grep -Fxq "run-id: $GITHUB_RUN_ID" release-manifest.txt; then
|
|
577
|
+
echo "FAIL: release manifest does not belong to this tag run." >&2
|
|
578
|
+
exit 1
|
|
579
|
+
fi
|
|
580
|
+
|
|
581
|
+
version="${GITHUB_REF_NAME#v}"
|
|
582
|
+
python scripts/check_release_manifest.py --version "$version" release-manifest.txt dist
|
|
583
|
+
|
|
584
|
+
- name: Publish the verified release candidate to PyPI
|
|
585
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 2026-07-28
|
|
586
|
+
with:
|
|
587
|
+
packages-dir: dist
|