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.
Files changed (83) hide show
  1. tooprolix-0.5.1/.gitattributes +2 -0
  2. tooprolix-0.5.1/.github/FUNDING.yml +4 -0
  3. tooprolix-0.5.1/.github/expected-tag-jobs.txt +41 -0
  4. tooprolix-0.5.1/.github/workflows/build-artifacts.yml +587 -0
  5. tooprolix-0.5.1/.github/workflows/ci.yml +395 -0
  6. tooprolix-0.5.1/.github/workflows/release-contract.yml +30 -0
  7. tooprolix-0.5.1/.github/workflows/release-plz.yml +189 -0
  8. tooprolix-0.5.1/.gitignore +45 -0
  9. tooprolix-0.5.1/CHANGELOG.md +172 -0
  10. tooprolix-0.5.1/CONTRIBUTING.md +273 -0
  11. tooprolix-0.5.1/Cargo.lock +994 -0
  12. tooprolix-0.5.1/Cargo.toml +186 -0
  13. tooprolix-0.5.1/LICENSE +21 -0
  14. tooprolix-0.5.1/Makefile +269 -0
  15. tooprolix-0.5.1/PKG-INFO +124 -0
  16. tooprolix-0.5.1/README.md +100 -0
  17. tooprolix-0.5.1/THIRD-PARTY-LICENSES.html +1558 -0
  18. tooprolix-0.5.1/about.hbs +20 -0
  19. tooprolix-0.5.1/about.toml +13 -0
  20. tooprolix-0.5.1/assets/tooprolix.gif +0 -0
  21. tooprolix-0.5.1/assets/tooprolix.png +0 -0
  22. tooprolix-0.5.1/build.rs +198 -0
  23. tooprolix-0.5.1/docs/cli-contract.md +201 -0
  24. tooprolix-0.5.1/docs/rules-and-configuration.md +186 -0
  25. tooprolix-0.5.1/examples/panic_is_a_controlled_exit.rs +22 -0
  26. tooprolix-0.5.1/pyproject.toml +337 -0
  27. tooprolix-0.5.1/release-plz.toml +82 -0
  28. tooprolix-0.5.1/rust-toolchain.toml +23 -0
  29. tooprolix-0.5.1/scripts/check_artifact.py +262 -0
  30. tooprolix-0.5.1/scripts/check_release_manifest.py +111 -0
  31. tooprolix-0.5.1/scripts/check_tag_jobs.py +254 -0
  32. tooprolix-0.5.1/scripts/coverage_report.py +337 -0
  33. tooprolix-0.5.1/scripts/install-smoke.sh +288 -0
  34. tooprolix-0.5.1/scripts/transform_readme.py +380 -0
  35. tooprolix-0.5.1/src/cli.rs +1877 -0
  36. tooprolix-0.5.1/src/config.rs +1083 -0
  37. tooprolix-0.5.1/src/detect/duplicate.rs +2533 -0
  38. tooprolix-0.5.1/src/detect/volume.rs +616 -0
  39. tooprolix-0.5.1/src/detect.rs +23 -0
  40. tooprolix-0.5.1/src/extract.rs +2178 -0
  41. tooprolix-0.5.1/src/finding.rs +913 -0
  42. tooprolix-0.5.1/src/lib.rs +41 -0
  43. tooprolix-0.5.1/src/main.rs +16 -0
  44. tooprolix-0.5.1/src/rules.rs +1422 -0
  45. tooprolix-0.5.1/src/snapshots/tooprolix__extract__tests__extracts_the_python_fixture.snap +69 -0
  46. tooprolix-0.5.1/tests/adversarial_bench.rs +650 -0
  47. tooprolix-0.5.1/tests/cli.rs +3779 -0
  48. tooprolix-0.5.1/tests/detect.rs +123 -0
  49. tooprolix-0.5.1/tests/extract.rs +51 -0
  50. tooprolix-0.5.1/tests/fixtures/broken/long_docstring.py +25 -0
  51. tooprolix-0.5.1/tests/fixtures/broken/syntax_error.py +5 -0
  52. tooprolix-0.5.1/tests/fixtures/clean/ledger.py +11 -0
  53. tooprolix-0.5.1/tests/fixtures/clean/settlement.py +4 -0
  54. tooprolix-0.5.1/tests/fixtures/dup-corpus/client.py +5 -0
  55. tooprolix-0.5.1/tests/fixtures/dup-corpus/config.py +27 -0
  56. tooprolix-0.5.1/tests/fixtures/dup-corpus/legacy.py +21 -0
  57. tooprolix-0.5.1/tests/fixtures/dup-corpus/poller.py +4 -0
  58. tooprolix-0.5.1/tests/fixtures/dup-corpus/worker.py +4 -0
  59. tooprolix-0.5.1/tests/fixtures/extract/sample.py +63 -0
  60. tooprolix-0.5.1/tests/fixtures/optout/comment_blanket.py +21 -0
  61. tooprolix-0.5.1/tests/fixtures/optout/comment_marked.py +21 -0
  62. tooprolix-0.5.1/tests/fixtures/optout/comment_mistyped.py +21 -0
  63. tooprolix-0.5.1/tests/fixtures/optout/comment_unmarked.py +20 -0
  64. tooprolix-0.5.1/tests/fixtures/optout/comment_wrong_code.py +21 -0
  65. tooprolix-0.5.1/tests/fixtures/optout/docstring_marked.py +23 -0
  66. tooprolix-0.5.1/tests/fixtures/optout/docstring_unmarked.py +22 -0
  67. tooprolix-0.5.1/tests/ruff_compatibility.rs +129 -0
  68. tooprolix-0.5.1/tests/unit/test_bench.py +422 -0
  69. tooprolix-0.5.1/tests/unit/test_check_artifact.py +376 -0
  70. tooprolix-0.5.1/tests/unit/test_check_release_manifest.py +85 -0
  71. tooprolix-0.5.1/tests/unit/test_check_tag_jobs.py +452 -0
  72. tooprolix-0.5.1/tests/unit/test_classification_artifact.py +725 -0
  73. tooprolix-0.5.1/tests/unit/test_coverage_report.py +508 -0
  74. tooprolix-0.5.1/tests/unit/test_duplicates.py +255 -0
  75. tooprolix-0.5.1/tests/unit/test_filters.py +165 -0
  76. tooprolix-0.5.1/tests/unit/test_measure.py +314 -0
  77. tooprolix-0.5.1/tests/unit/test_restatement_verdict.py +233 -0
  78. tooprolix-0.5.1/tests/unit/test_sample_clusters.py +305 -0
  79. tooprolix-0.5.1/tests/unit/test_transform_readme.py +419 -0
  80. tooprolix-0.5.1/tests/unit/test_units.py +615 -0
  81. tooprolix-0.5.1/tests/unit/test_workflow_contracts.py +1070 -0
  82. tooprolix-0.5.1/tests/volume_corpus.rs +275 -0
  83. tooprolix-0.5.1/uv.lock +411 -0
@@ -0,0 +1,2 @@
1
+ LICENSE text eol=lf
2
+ THIRD-PARTY-LICENSES.html text eol=lf
@@ -0,0 +1,4 @@
1
+ # These are supported funding model platforms
2
+
3
+ patreon: sgolyshevskii
4
+ custom: ['https://boosty.to/golyshevskii/donate']
@@ -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