-
Notifications
You must be signed in to change notification settings - Fork 1
456 lines (431 loc) · 19.8 KB
/
python-cli-wheels.yml
File metadata and controls
456 lines (431 loc) · 19.8 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
name: Python CLI wheels
# Build, smoke-test, and publish pip-installable wheels for the `bca`
# command-line tool (distribution name `big-code-analysis-cli`, console
# command `bca`). See #408.
#
# This is the binary-distribution sibling of `python-wheels.yml`, which
# ships the PyO3 *library* bindings (`big-code-analysis`). The two are
# independent PyPI projects that happen to share maturin as a build
# backend:
#
# * `python-wheels.yml` → `import big_code_analysis` (extension wheel,
# abi3, one wheel per arch covering every CPython 3.x).
# * this workflow → `bca` on PATH (a `-b bin` wheel: the compiled
# binary is packaged as a console script).
#
# Design notes specific to the bin wheel:
#
# * **Not an extension module — abi3 is irrelevant.** A bin wheel is
# tagged `py3-none-<platform>`: one wheel per (OS, arch), but a single
# wheel covers every CPython 3.x (and PyPy) on that platform. There is
# no per-Python-version matrix and no libpython link.
#
# * **Bindings mode is fixed in `big-code-analysis-cli/pyproject.toml`**
# (`[tool.maturin] bindings = "bin"`). The crate is already a single
# `[[bin]] name = "bca"` with no pyo3/cdylib target, which is exactly
# the shape maturin auto-detects for bin bindings; the explicit key
# guards against a future PyO3 addition silently flipping the mode.
#
# * **Full grammar set is inherited from the crate.** The CLI crate
# pins `big-code-analysis` with `default-features = false, features =
# ["all-languages"]` (see #252), so a plain `-b bin` build already
# compiles every grammar in — no `[tool.maturin] features` wiring is
# needed here, and a default-features build cannot silently drop a
# grammar.
#
# * **manylinux_2_28** floor, matching `python-wheels.yml`: the MSRV
# (1.94) toolchain lives in the 2_28 container and RHEL/CentOS Stream
# 8 (glibc 2.28) is the oldest realistic target.
#
# * **Compliance artefacts ride in the wheel.** The per-binary
# `THIRD-PARTY-LICENSES-bca.md` (rendered by cargo-about, the same
# tool `release.yml` uses for the deb/rpm/archive TPLs) plus the
# workspace `LICENSE` are staged into the crate directory and picked
# up by `[project].license-files`, landing in the wheel's standard
# `.dist-info/licenses/`. The `bca` man pages are bundled for
# reference via `[tool.maturin] include`. maturin additionally emits a
# CycloneDX SBOM into `.dist-info/sboms/` automatically.
#
# * **PyPI Trusted Publishing** (OIDC), no long-lived token — identical
# posture to `python-wheels.yml`. The deployment environment is
# `pypi-cli`, intentionally distinct from the library's `pypi`
# environment so the two projects' Trusted-Publisher OIDC claims
# (matched on repo + workflow filename + environment) do not overlap.
on:
push:
# Numeric-prefix glob (not bare `v*`) so a word-prefixed debugging tag
# cannot reach the publish job — mirrors python-wheels.yml.
tags: ['v[0-9]*']
pull_request:
paths:
- 'big-code-analysis-cli/**'
- '.github/workflows/python-cli-wheels.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
# Every `uses:` is pinned to a commit SHA (trailing comment = the tag it
# points at); Dependabot bumps both in lockstep. SHAs are kept in sync
# with python-wheels.yml / release.yml.
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
PYTHON_VERSION_HOST: "3.12"
# Pinned toolchain so the macOS / Windows wheel binaries (built on the
# host, outside maturin's manylinux container) are reproducible from a
# tag rather than tracking a floating `stable`. Matches release.yml's
# RUST_TOOLCHAIN; the Linux legs build inside the 2_28 container with
# its own toolchain, unaffected by this value.
RUST_TOOLCHAIN: "1.94.0"
# cargo-about renders the per-binary TPL; pinned to the version
# release.yml uses so the deb/rpm/archive/wheel TPLs are byte-identical
# for a given tag.
CARGO_ABOUT_VERSION: "0.8.4"
jobs:
# Build the wheel matrix. PR-time builds are opt-in via the
# `python-cli-wheels` label to keep the cost off Rust-only PRs that
# merely brush a path-filter neighbour. 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-cli-wheels')
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
manylinux: "2_28"
wheel_tag: manylinux_2_28_x86_64
- target: aarch64-unknown-linux-gnu
runs-on: ubuntu-24.04-arm
manylinux: "2_28"
wheel_tag: manylinux_2_28_aarch64
- target: x86_64-apple-darwin
runs-on: macos-latest
manylinux: "auto"
# Glob (the macOS minor version varies) so the verify step
# below distinguishes x86_64 from arm64 — a bare `macosx`
# would accept a wrong-arch wheel from a cross-build slip.
wheel_tag: 'macosx_*_x86_64'
- target: aarch64-apple-darwin
runs-on: macos-latest
manylinux: "auto"
wheel_tag: 'macosx_*_arm64'
- target: x86_64-pc-windows-msvc
runs-on: windows-latest
manylinux: "auto"
wheel_tag: win_amd64
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 45
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 }}
# Host toolchain — needed for the cargo-about staging step below.
# The Linux legs build the binary inside maturin-action's
# manylinux_2_28 container (its own toolchain), but cargo-about runs
# on the host; the `targets:` add lets cargo-about's
# `--filter-platform` resolve a cross target (e.g. x86_64 on the
# arm64 macOS runner) without a full target install.
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # stable tip
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
targets: ${{ matrix.target }}
- name: Install cargo-about
uses: taiki-e/install-action@50b4a718b59c718df4ef27a3b445f86cd57b9f00 # v2.80.0
with:
tool: cargo-about@${{ env.CARGO_ABOUT_VERSION }}
# Stage the compliance + doc artefacts into the CLI crate directory
# so maturin picks them up (TPL + LICENSE via
# `[project].license-files`; man pages via `[tool.maturin] include`).
# These are .gitignore'd; the canonical sources live at the repo
# root and `man/`. cargo-about is target-filtered so the TPL reflects
# the dependency closure actually shipped in this wheel.
- name: Stage license + man-page artefacts
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
cargo about generate --locked \
--config about.toml \
--target "$TARGET" \
--manifest-path big-code-analysis-cli/Cargo.toml \
about.hbs \
> big-code-analysis-cli/THIRD-PARTY-LICENSES-bca.md
test -s big-code-analysis-cli/THIRD-PARTY-LICENSES-bca.md
cp LICENSE big-code-analysis-cli/LICENSE
# Bundle only the `bca` pages — `bca-web.1` belongs to the web
# server binary, which this wheel does not ship.
mkdir -p big-code-analysis-cli/man
for f in man/bca*.1; do
[ "$f" = "man/bca-web.1" ] && continue
cp "$f" big-code-analysis-cli/man/
done
test -f big-code-analysis-cli/man/bca.1
test ! -f big-code-analysis-cli/man/bca-web.1
# maturin-action builds the bin wheel. On Linux it pulls the
# manylinux_2_28 container matching ${{ matrix.target }}; on
# macOS/Windows the `manylinux: auto` value is a no-op and the build
# runs natively. `--strip` drops debug symbols from the binary;
# `--locked` honours the workspace Cargo.lock byte-for-byte so the
# wheel's transitive-dep versions are reproducible from the tag.
# Bindings mode is set in pyproject (`bindings = "bin"`).
- name: Build bin wheel (${{ matrix.target }})
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: big-code-analysis-cli
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --release --strip --locked --out dist
# Catch a tag / interpreter / arch regression here instead of at
# PyPI upload time. A bin wheel must be `py3-none-<platform>`: the
# `py3-none` segment proves it was not built as a per-version
# extension wheel (the most plausible regression if the pyproject
# bindings key were lost), and `${{ matrix.wheel_tag }}` (which
# carries the architecture, e.g. `macosx_*_x86_64`) proves the
# platform AND arch match this leg — so a cross-build that silently
# emitted the host arch fails here rather than only at the publish
# job's inventory.
- name: Verify wheel is py3-none / ${{ matrix.wheel_tag }}
shell: bash
working-directory: big-code-analysis-cli
env:
WHEEL_TAG: ${{ matrix.wheel_tag }}
run: |
set -euo pipefail
ls -la dist/
shopt -s nullglob
all=(dist/*.whl)
if [[ ${#all[@]} -ne 1 ]]; then
echo "::error::expected exactly one wheel in dist/, found ${#all[@]}"
exit 1
fi
name=$(basename "${all[0]}")
# WHEEL_TAG is matrix-controlled and may contain a `*` glob
# (the macOS minor version varies), so it is intentionally
# unquoted here to act as a case pattern, not a literal.
# shellcheck disable=SC2254
case "$name" in
*-py3-none-*${WHEEL_TAG}*.whl) : ;;
*)
echo "::error::wheel '$name' is not py3-none-*${WHEEL_TAG}*"
exit 1
;;
esac
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cli-wheels-${{ matrix.target }}
path: big-code-analysis-cli/dist/*.whl
if-no-files-found: error
# Source distribution. PyPI fallback for niche architectures and a
# reproducibility anchor. A `pip install` from this sdist rebuilds the
# binary, so it needs a Rust toolchain on the consumer side — expected
# for a bin crate; the prebuilt wheels above cover the common platforms.
sdist:
name: sdist
if: >-
github.event_name != 'pull_request'
|| contains(github.event.pull_request.labels.*.name, 'python-cli-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 }}
# No artefact staging: the TPL/LICENSE/man pages are `format =
# "wheel"` / generated, so they are intentionally absent from the
# sdist (a source snapshot). The workspace Cargo.lock travels in the
# tarball, so a downstream rebuild resolves identical dep versions.
- name: Build sdist
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: big-code-analysis-cli
command: sdist
args: --out dist
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cli-sdist
path: big-code-analysis-cli/dist/*.tar.gz
if-no-files-found: error
# Install each wheel into a fresh runner Python environment on a runner
# of the matching platform and exercise the binary end-to-end. The
# acceptance criteria
# (#408) require `bca --version` and a parse that proves the
# `all-languages` grammar set shipped. The x86_64-apple-darwin wheel is
# cross-built on the arm64 macOS runner and cannot be executed here
# (GitHub's macOS pool is arm64); its structural integrity is covered by
# the build job's verify step, mirroring release.yml's handling of the
# non-executable aarch64-windows lane.
smoke-test:
name: smoke-test (${{ matrix.target }})
needs: build
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
- target: aarch64-unknown-linux-gnu
runs-on: ubuntu-24.04-arm
- target: aarch64-apple-darwin
runs-on: macos-latest
- target: x86_64-pc-windows-msvc
runs-on: windows-latest
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 15
steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION_HOST }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cli-wheels-${{ matrix.target }}
path: dist
- name: Install wheel + assert bca on PATH
shell: bash
env:
PYTHONUNBUFFERED: "1"
# On a tag push this is the release version (`vX.Y.Z`); empty
# on PR / workflow_dispatch runs, where there is no tag to
# compare against.
EXPECTED_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
set -euo pipefail
ls -la dist/
python -m pip install --upgrade pip
# --no-index makes an accidental PyPI fallback (e.g. a wheel
# whose platform tag does not match this runner) fail loudly
# rather than silently installing a stale published version.
python -m pip install --no-index --find-links=dist big-code-analysis-cli
# The console script lands in the setup-python interpreter's
# scripts dir, which is on PATH. Capture the version rather than
# piping to `head` (a `| head -n1` would SIGPIPE the producer
# under pipefail — see release.yml's note).
ver_out=$(bca --version)
echo "$ver_out"
# On a tag build, prove the binary reports the lockstep release
# version (maturin reads `version.workspace = true`, so the
# wheel version must equal the tag minus its leading `v`).
if [[ -n "$EXPECTED_TAG" ]]; then
want="${EXPECTED_TAG#v}"
case "$ver_out" in
*"$want"*) : ;;
*) echo "::error::bca --version '$ver_out' does not contain tag version $want"; exit 1 ;;
esac
fi
bca list-metrics names >/dev/null
# Parse two unrelated languages to prove the all-languages
# grammar set is compiled in, not just the host language.
printf 'def add(a, b):\n if a > b:\n return a\n return b\n' > smoke.py
printf 'fn main() { if true { println!("x"); } }\n' > smoke.rs
py_cc=$(bca --paths smoke.py metrics -O json | python -c "import sys,json; print(json.load(sys.stdin)['metrics']['cyclomatic']['sum'])")
rs_cc=$(bca --paths smoke.rs metrics -O json | python -c "import sys,json; print(json.load(sys.stdin)['metrics']['cyclomatic']['sum'])")
test "$py_cc" = "3.0" || { echo "::error::python cyclomatic.sum=$py_cc, expected 3.0"; exit 1; }
test "$rs_cc" = "3.0" || { echo "::error::rust cyclomatic.sum=$rs_cc, expected 3.0"; exit 1; }
echo "smoke OK"
# Aggregate gate so branch protection can require a single check name.
# Every dependency is label-gated at PR time, so on an unlabelled PR all
# `needs` resolve to `skipped`; the predicate fails on `skipped` too
# (not just failure/cancelled) so an unlabelled run cannot green-tick
# the required check without having built anything — same wrinkle as
# python-wheels.yml's `wheels` gate.
cli-wheels:
name: cli-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 numeric `v*` tag push with NO pre-release
# suffix. `!contains(github.ref, '-')` mirrors release.yml's prerelease
# rule exactly (it classifies any tag matching `*-*` as a prerelease
# and skips the crates.io publish), so a single tag cannot land a
# prerelease on PyPI while skipping crates.io. A release tag
# (`refs/tags/v1.2.0`) has no hyphen; any suffix (`-rc1`, `-beta2`,
# `-pre1`, …) does. The Trusted Publisher for `big-code-analysis-cli`
# must be registered on PyPI (repo + this workflow filename + the
# `pypi-cli` environment) before the first tagged release.
publish:
name: publish to PyPI
needs: [build, sdist, smoke-test]
if: >-
github.event_name == 'push'
&& startsWith(github.ref, 'refs/tags/v')
&& !contains(github.ref, '-')
runs-on: ubuntu-latest
environment:
name: pypi-cli
url: https://pypi.org/project/big-code-analysis-cli/
permissions:
# A job-level `permissions:` block replaces the workflow default
# rather than augmenting it; restate `contents: read` for any future
# step that needs repo access.
contents: read
# Exchanged for a one-off PyPI upload credential — no long-lived
# token. `attestations: write` powers the PEP 740 Sigstore
# attestations the publish action generates by default.
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: cli-wheels-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cli-sdist
path: dist
- name: Inventory artefacts
run: |
set -euo pipefail
ls -la dist/
# Refuse to publish a half-matrix release: assert every
# platform wheel plus the sdist arrived. A download-artifact
# silently dropping one leg would otherwise leave PyPI users on
# that platform with no wheel.
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 "manylinux_2_28 x86_64 wheel" dist/*-py3-none-manylinux_2_28_x86_64.whl
require "manylinux_2_28 aarch64 wheel" dist/*-py3-none-manylinux_2_28_aarch64.whl
require "macOS x86_64 wheel" dist/*-py3-none-macosx_*_x86_64.whl
require "macOS arm64 wheel" dist/*-py3-none-macosx_*_arm64.whl
require "Windows x86_64 wheel" dist/*-py3-none-win_amd64.whl
require "sdist tarball" dist/*.tar.gz
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
packages-dir: dist
attestations: true