-
Notifications
You must be signed in to change notification settings - Fork 1
542 lines (510 loc) · 25.2 KB
/
python-wheels.yml
File metadata and controls
542 lines (510 loc) · 25.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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
name: Python wheels
# Build, smoke-test, and publish manylinux wheels for the
# `big-code-analysis-py` PyO3 bindings. Phase 7/9 of the umbrella
# Python-bindings effort (#103); see #271 for the rationale.
#
# Design notes captured from the 2026 best-practices research:
#
# * **manylinux_2_28** is the floor. Rust 1.64+ requires glibc ≥ 2.17
# (so manylinux2014 is the absolute minimum), but the project's MSRV
# is 1.94 and the maturin-action default for both x86_64 and aarch64
# is `2_28`. RHEL/CentOS Stream 8 (glibc 2.28) is the oldest distro
# that any realistic deployment target still ships, and `2_28`
# containers carry the modern toolchain we need without forcing
# consumers off any current LTS.
#
# * **abi3-py312** produces a single stable-ABI wheel per architecture
# that works on CPython 3.12, 3.13, 3.14+. PyO3 0.28.3 supports the
# limited-API flag and a local
# `cargo check -p big-code-analysis-py --no-default-features
# --features pyo3/extension-module,pyo3/abi3-py312`
# builds clean against the bindings' current surface (dict-
# returning functions, custom exceptions, no generators / no
# async — the features that would force the non-abi3 fallback).
# `--no-default-features` is mandatory: the crate's `default =
# ["pyo3/auto-initialize"]` is documented-incompatible with abi3.
# The flag is wired through pyproject.toml's
# `[tool.maturin].features` so `maturin develop` and the wheel
# build use the identical Cargo feature set — but note that
# `cargo test --workspace --all-features` continues to use
# `pyo3/auto-initialize` (not abi3), so the enforcement gate is
# the `python-test` job in ci.yml + `make pre-commit`, not
# `cargo test`.
#
# * **Native ARM runners** (`ubuntu-24.04-arm`) replace QEMU emulation
# for the aarch64 build. ARM standard runners went GA for private
# repositories in January 2026 (this repo is private); the cross-
# build via QEMU was the only path before then but is now an order
# of magnitude slower with no upside. The maturin-action's
# `manylinux: 2_28` mode pulls the matching ARM-native manylinux
# container automatically when the workflow runs on an ARM host.
#
# * **PyPI Trusted Publishing** (PEP 740) over a long-lived API
# token. The `publish` job mints a short-lived GitHub OIDC token
# and exchanges it inside pypa/gh-action-pypi-publish for a one-off
# PyPI upload credential. Attestations (Sigstore-signed) are
# generated and uploaded automatically by the action since v1.14.0,
# so the dedicated `sigstore/gh-action-sigstore-python` step the
# #271 issue proposed is redundant — keeping it out of this
# workflow avoids signing the same artefacts twice with conflicting
# identities. The PyPI side requires a Trusted Publisher entry
# pointing at this repository + this workflow + the `pypi`
# deployment environment; the `pypi` env is intentionally distinct
# from the `release` env used by crates.io publishes in
# `release.yml` so the two registries' OIDC claims do not overlap.
on:
push:
# Narrowed from `v*` to `v[0-9]*` — the original glob matches any
# tag starting with `v` (e.g. a debugging tag like `verify-grammar-
# sync` would trigger the publish job and irrevocably push a wheel
# to PyPI under whatever workspace version the tagged commit
# carries). `v[0-9]*` requires a digit after the leading `v`, which
# rules out word-prefixed tags while still accepting `v0.1.0`,
# `v10.0.0-rc1`, etc. The publish job's `if:` below redundantly
# checks the same shape so a manual `workflow_dispatch` on a
# `v[0-9]…` ref still cannot publish.
tags: ['v[0-9]*']
pull_request:
paths:
- 'big-code-analysis-py/**'
- '.github/workflows/python-wheels.yml'
workflow_dispatch:
# Default to read-only; the publish job escalates to id-token: write.
permissions:
contents: read
# Mirror ci.yml: cancel in-progress runs for ad-hoc refs, but never
# cancel a tag build (release artefacts must complete) or a main build
# (the wheel matrix is the smoke surface for the bindings on push).
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
# Every `uses:` below is pinned to a commit SHA to neutralise tag-
# rewriting supply-chain attacks. The trailing comment is the tag the
# SHA points at; Dependabot bumps both in lockstep. The SHAs were
# verified against `gh api repos/<owner>/<repo>/git/refs/tags/<tag>`
# (or the action's annotated-tag object) at the time of writing.
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
# Build host's Python — only used to invoke maturin's CLI; the
# in-container Python that compiles the abi3 wheel comes from the
# manylinux_2_28 image.
PYTHON_VERSION_HOST: "3.12"
jobs:
# Build the wheel matrix. The PR-time gate keeps the cost off
# Rust-only PRs that happen to touch a path filter neighbour —
# contributors opt their PR in by applying the `python-wheels` label.
# Tag pushes and workflow_dispatch always run.
build:
name: build (${{ matrix.target }})
if: >-
github.event_name != 'pull_request'
|| contains(github.event.pull_request.labels.*.name, 'python-wheels')
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
arch: x86_64
- target: aarch64-unknown-linux-gnu
runs-on: ubuntu-24.04-arm
arch: aarch64
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 45
steps:
# No submodules — the bindings' fixtures are in-tree.
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
submodules: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION_HOST }}
# maturin-action pulls the manylinux_2_28 container that matches
# ${{ matrix.target }} and prepends `--manylinux 2_28` to the
# maturin invocation (maturin's `--manylinux` is an alias of
# `--compatibility` with `ArgAction::Append`). The action's
# `manylinux:` input is the only place we set the platform tag;
# adding a redundant `--compatibility manylinux_2_28` to `args:`
# would double-append the value and either error or emit a
# multi-tag wheel filename that the verification step rejects.
# `--strip` removes debug symbols (the .pyi stub carries the
# public name table, so IDE support is unaffected). `--locked`
# forces cargo to honour the workspace Cargo.lock byte-for-byte
# so the wheel's transitive-dep versions are reproducible from
# the tagged commit — matching the `cargo publish --locked`
# contract `release.yml` uses for the crates.io publishes.
- name: Build wheel (manylinux_2_28 / ${{ matrix.arch }} / abi3)
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: big-code-analysis-py
target: ${{ matrix.target }}
manylinux: "2_28"
args: --release --strip --locked --out dist
# Inspect the produced filename so an abi3 / manylinux /
# architecture regression shows up here instead of at PyPI
# upload time. The glob is scoped to `${{ matrix.arch }}` so a
# wrong-arch wheel (e.g. maturin-action silently ignoring
# `target:`) produced on this runner does NOT pass: the
# aarch64 leg refuses an x86_64 wheel and vice versa. The
# `cp312-abi3` segment catches per-version wheels emitted when
# the abi3 feature drops out of pyproject.toml.
- name: Verify wheel is abi3 + manylinux_2_28 / ${{ matrix.arch }}
working-directory: big-code-analysis-py
env:
EXPECTED_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
ls -la dist/
shopt -s nullglob
matches=(dist/*cp312-abi3-manylinux_2_28_"${EXPECTED_ARCH}".whl)
if [[ ${#matches[@]} -eq 0 ]]; then
echo "::error::No wheel matches cp312-abi3-manylinux_2_28_${EXPECTED_ARCH}.whl in dist/"
ls dist/
exit 1
fi
# Defensive: refuse to upload if the directory holds an
# extra wheel for the *other* arch — that would mean the
# cross-build accidentally produced two wheels and the
# artifact upload would carry both, polluting the publish
# job's `pattern: wheels-*` merge.
extras=(dist/*.whl)
if [[ ${#extras[@]} -ne ${#matches[@]} ]]; then
echo "::error::dist/ contains wheels other than ${EXPECTED_ARCH}; refusing to upload"
ls dist/
exit 1
fi
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: wheels-${{ matrix.arch }}
path: big-code-analysis-py/dist/*.whl
if-no-files-found: error
# Source distribution. Required by PyPI as a fallback for niche
# architectures and as a reproducibility anchor (the wheel is binary;
# the sdist captures the exact source tree the wheel was built from).
sdist:
name: sdist
if: >-
github.event_name != 'pull_request'
|| contains(github.event.pull_request.labels.*.name, 'python-wheels')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
submodules: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION_HOST }}
# `maturin sdist` only bundles the source tree (it does not
# compile, so `--locked` is rejected by the subcommand). The
# workspace `Cargo.lock` is included in the tarball anyway, so a
# downstream `pip install --no-binary :all:` rebuild from the
# sdist resolves to the same transitive-dep versions the
# published wheel was built against — the lockfile travels with
# the source.
- name: Build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: big-code-analysis-py
command: sdist
args: --out dist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sdist
path: big-code-analysis-py/dist/*.tar.gz
if-no-files-found: error
# Smoke test: pull the built wheel onto a clean runner of the same
# architecture, install it from the dist directory (no PyPI round-
# trip), and exercise enough of the API to catch dynamic-linker
# failures (`ImportError`, missing symbols) and trivial regressions
# in the JSON output shape. Both Python 3.12 and 3.13 are exercised
# so the abi3 forward-compatibility claim is verified end-to-end —
# an abi3 wheel that loads on the build's 3.12 but not the runner's
# 3.13 is the most plausible silent regression in this matrix.
smoke-test:
name: smoke-test (${{ matrix.arch }}, py${{ matrix.python }})
needs: build
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
runs-on: ubuntu-latest
- arch: aarch64
runs-on: ubuntu-24.04-arm
python: ["3.12", "3.13"]
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 15
steps:
# No `actions/checkout` — the smoke step reads only the
# downloaded wheel artefact and runs an inline Python heredoc;
# no repo file (fixture, helper, doc) is touched. Skipping the
# checkout saves ~10-20 s per matrix leg on aarch64 (the
# native-ARM runner's clone over LFS-adjacent submodules takes
# the wall-clock penalty even when we skip submodules).
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: wheels-${{ matrix.arch }}
path: dist
- name: Install wheel
run: |
set -euo pipefail
ls -la dist/
python -m pip install --upgrade pip
# Install only from the local dist; --no-index makes accidental
# PyPI fallback (e.g. when the wheel name does not match the
# current interpreter) fail loudly rather than silently install
# a stale published version.
python -m pip install --no-index --find-links=dist big-code-analysis
# Exercise the public API surface that #103 enumerates:
# analyze_source returns a dict, flatten_spaces yields function
# records, to_sarif emits SARIF JSON, language_for_file resolves
# extensions. If any of these regress the smoke fails before
# the wheel can publish.
#
# Assertions are deliberately VALUE-bearing, not just shape-
# bearing — a binding regression that returned `{"spaces": []}`
# would satisfy a bare `isinstance(result, dict)` check but
# fail the `records[*]["name"] == "add"` check below.
- name: Import + analyse smoke
env:
# Force unbuffered stdout so failure assertions show up
# immediately in the action log instead of after the python
# process has been torn down.
PYTHONUNBUFFERED: "1"
run: |
python - <<'PY'
import json
import tempfile
from pathlib import Path
import big_code_analysis as bca
# Sanity: package + extension loaded.
assert bca.__version__, repr(bca.__version__)
assert "python" in bca.supported_languages()
# Extensions are bare names (no leading dot) — match the
# `language_extensions` contract in the stub. The CLI used
# to expose dotted forms; the binding does not.
assert "py" in bca.language_extensions("python"), bca.language_extensions("python")
# `language_for_file` reads the file (parity with `analyze`,
# #318), so a stub path that does not exist raises
# FileNotFoundError. Materialise an empty fixture in a
# tempdir so the assertion exercises the extension table
# path rather than crashing on I/O.
with tempfile.TemporaryDirectory() as td:
fixture = Path(td) / "foo.py"
fixture.write_bytes(b"")
resolved = bca.language_for_file(fixture)
assert resolved == "python", resolved
# `language` is a positional-only parameter on
# `analyze_source`; passing it as a kwarg raises TypeError.
src = "def add(a, b):\n if a > b:\n return a\n return b\n"
result = bca.analyze_source(src, "python")
assert isinstance(result, dict), type(result)
assert result.get("kind") == "unit", result.get("kind")
# The unit-level cyclomatic is 3 for this fixture (one
# function spanning the file + one `if` branch). A binding
# regression that produced null / zero metrics would slip
# past `isinstance(result, dict)` but trip this assertion.
cyclomatic_sum = result["metrics"]["cyclomatic"]["sum"]
assert cyclomatic_sum == 3.0, f"expected unit cyclomatic.sum == 3.0, got {cyclomatic_sum}"
# `flatten_spaces` yields one record per space; for this
# fixture that is the unit + the `add` function. The
# function record's cyclomatic.sum is 2 (1 entry + 1 if).
records = list(bca.flatten_spaces(result))
assert records, "flatten_spaces returned no records"
add_records = [r for r in records if r.get("name") == "add"]
assert add_records, f"no 'add' function in records: {[r.get('name') for r in records]}"
assert add_records[0].get("kind") == "function", add_records[0].get("kind")
assert add_records[0].get("cyclomatic.sum") == 2.0, add_records[0].get("cyclomatic.sum")
# SARIF must be a well-formed 2.1.0 document with at least
# one run carrying the bca tool driver. A stub
# `{"version": "2.1.0"}` (the most plausible regression of
# an empty-run SARIF writer) would fail the runs check.
sarif_obj = json.loads(bca.to_sarif(result))
assert sarif_obj.get("version") == "2.1.0", sarif_obj.get("version")
runs = sarif_obj.get("runs") or []
assert len(runs) == 1, f"expected 1 SARIF run, got {len(runs)}"
driver = runs[0].get("tool", {}).get("driver", {})
assert driver.get("name"), f"SARIF run missing tool.driver.name: {driver}"
# analyze_batch's never-raise contract: a missing path in
# the batch must be returned as an AnalysisError instance
# interleaved with the successful dicts, never as an
# exception. A binding regression that switched batch back
# to raising would crash here; one that dropped error
# records entirely would shrink `len(batch)` below the
# input count and trip the assertion.
with tempfile.TemporaryDirectory() as td:
good = Path(td) / "good.py"
good.write_text("def f():\n pass\n")
missing = Path(td) / "does-not-exist.py"
batch = bca.analyze_batch([good, missing])
assert len(batch) == 2, f"expected 2 batch results, got {len(batch)}"
# Good path → dict.
assert isinstance(batch[0], dict), type(batch[0])
# Missing path → AnalysisError with `error_kind == "IoError"`.
err = batch[1]
assert isinstance(err, bca.AnalysisError), type(err)
assert err.error_kind == "IoError", err.error_kind
assert str(missing) in err.path, (err.path, str(missing))
print("smoke OK:", bca.__version__)
PY
# pip-audit deliberately NOT run here. The pre-publish wheel
# has zero Python runtime deps (the binary surface is the
# PyO3 extension only) and pip-audit's only useful pre-publish
# target — "is the just-built version vulnerable?" — cannot
# be answered because that version is not yet on PyPI. A
# naive `python -m pip install pip-audit && pip-audit` would
# also pollute the smoke venv with pip-audit's transitive
# deps (requests, packaging, cyclonedx-python-lib, …) and
# red the gate on every CVE filed against any of them. The
# right place for a dependency audit is a post-publish job
# against the actual PyPI listing once the wheel acquires
# runtime deps; opening that as a follow-up.
# Aggregate gate so branch protection can require a single check
# name regardless of how the matrix expands. Mirrors the `ci` job in
# ci.yml — with one wrinkle. Unlike ci.yml's `ci` job, every
# dependency here is gated on the `python-wheels` PR label, so on a
# PR without the label all needs resolve to `skipped`. The naive
# `contains(needs.*.result, 'failure') || ... 'cancelled')` from
# ci.yml's pattern would then return false and the aggregate would
# report success despite zero wheels having been built — defeating
# the gate. The predicate is widened to also fail on `skipped` so
# an unlabelled PR-time run does not silently green-tick the
# required check.
#
# Tag pushes and `workflow_dispatch` runs always exercise every
# leg (no `if:` gating on those events), so widening the predicate
# does not over-flag those paths.
wheels:
name: wheels
if: always()
needs:
- build
- sdist
- smoke-test
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Fail if any dependency did not succeed
if: >-
contains(needs.*.result, 'failure')
|| contains(needs.*.result, 'cancelled')
|| contains(needs.*.result, 'skipped')
run: exit 1
# PyPI publish — only on a `v*` tag push. The `pypi` deployment
# environment binds the OIDC `environment` claim that the PyPI
# Trusted Publisher matches against; protection rules on the
# environment (required reviewers, allowed tag patterns) are the
# right place to add a manual approval gate, not the workflow.
#
# Trusted Publisher must be registered on PyPI before the first
# release — see RELEASING.md → "PyPI Trusted Publisher setup".
# The first tagged release after registration is the canonical
# end-to-end test of the OIDC handshake; there is no static token
# to fall back on.
publish:
name: publish to PyPI
needs: [build, sdist, smoke-test]
# The `on.push.tags: ['v[0-9]*']` filter above is the primary
# gate: GitHub only fires this workflow on tag pushes that match
# the numeric-prefix glob, so word-prefixed tags like
# `verify-grammar-sync` cannot reach the publish job. The
# `event_name == 'push'` belt-and-braces excludes
# `workflow_dispatch` runs (which can be triggered against any
# ref, including a `v[0-9]…` tag, and would otherwise satisfy
# the `startsWith` check). GHA expressions do not have a
# substring/regex operator, so re-validating the digit-after-v
# constraint inside the `if:` would require an unwieldy
# ten-clause disjunction — the `on:` filter is the source of
# truth.
#
# Prerelease tags (`v0.1.0-rc1`, `-beta2`, `-alpha3`, …) are
# explicitly NOT published — matching the policy `release.yml`
# establishes for crates.io publishes (its "Cutting a pre-
# release" section). The two registries' publishing posture must
# stay aligned so a single tag does not simultaneously land a
# prerelease on PyPI and skip crates.io. The `contains` checks
# below cover the three suffixes the wider release pipeline
# recognises; any tag containing one of them in its ref name
# skips publish and only exercises the build + smoke matrix.
if: >-
github.event_name == 'push'
&& startsWith(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-rc')
&& !contains(github.ref, '-beta')
&& !contains(github.ref, '-alpha')
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/big-code-analysis/
permissions:
# Workflow-level `contents: read` does NOT cascade into a job
# that declares its own `permissions:` block — the block
# replaces the default rather than augmenting it. Restate
# `contents: read` so any future step here (e.g. an
# `actions/checkout` to read CHANGELOG.md for release notes)
# has the access the workflow-level default would have
# otherwise granted.
contents: read
# Trusted publishing exchanges this OIDC token for a one-off
# PyPI upload credential; the long-lived `PYPI_API_TOKEN`
# secret pattern this replaces is explicitly forbidden by #271.
id-token: write
# attestations: write is implied by id-token: write at the
# workflow level, but spelling it out keeps the PEP 740
# attestation pipeline obvious to a reviewer.
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: wheels-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: sdist
path: dist
- name: Inventory artefacts
run: |
set -euo pipefail
ls -la dist/
# Defensive: refuse to publish unless every architecture's
# wheel arrived (a download-artifact silently dropping one
# leg would otherwise publish a half-matrix release). The
# `::error::` annotations surface the failure in the GH
# Actions UI summary instead of being buried in stderr.
shopt -s nullglob
require() {
local label=$1
shift
local matches=("$@")
if [[ ${#matches[@]} -eq 0 ]]; then
echo "::error::Missing expected artefact: ${label}"
exit 1
fi
}
require "cp312-abi3 / x86_64 wheel" dist/*-cp312-abi3-manylinux_2_28_x86_64.whl
require "cp312-abi3 / aarch64 wheel" dist/*-cp312-abi3-manylinux_2_28_aarch64.whl
require "sdist tarball" dist/*.tar.gz
# pypa/gh-action-pypi-publish v1.14.0 turns on PEP 740 Sigstore
# attestations by default when invoked with id-token: write. No
# username / password / api-token inputs — the trusted-publisher
# exchange is the only auth path, and a missing TP registration
# surfaces here as an obvious HTTP 403 rather than a silent
# fallback to a token that does not exist.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: dist
# Explicit even though it is the default; the assertion is
# that we never published from anywhere except a verified
# OIDC-authenticated job.
attestations: true