-
Notifications
You must be signed in to change notification settings - Fork 0
596 lines (577 loc) · 30 KB
/
Copy pathci.yml
File metadata and controls
596 lines (577 loc) · 30 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
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
typos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: crate-ci/typos@4d9c206a77c041268485162b8e2579ad7a5cb9a3 # v1.50.0
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- name: Lint YAML
# Major-bound pin (like the other uvx tools in this workflow) so a
# yamllint 2.x can't add rules that red this gate without a deliberate
# bump. `uvx` provisions its own interpreter, so no separate
# setup-python step is needed.
run: uvx 'yamllint>=1,<2' .
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.12
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
run: |
set -euo pipefail
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -sSL --fail -o "$archive" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/$archive"
echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum --check --status
tar xzf "$archive" actionlint
install -m 0755 actionlint "$RUNNER_TEMP/actionlint"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Lint GitHub Actions workflows
# shellcheck is preinstalled on GitHub's Ubuntu runners; actionlint
# detects it on PATH and lints every Bash `run:` block as well.
run: actionlint
lint:
name: ruff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Rust toolchain: needed so `uv sync` can build the PyO3 extension via maturin.
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- run: uv run ruff format --check .
- run: uv run ruff check .
typecheck:
name: mypy + stubtest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- run: uv run mypy
# stubtest checks the .pyi against the compiled module at runtime — it catches
# signature drift (params/types/defaults) the AST-based drift guard in
# tests/test_api_surface.py cannot see. The allowlist suppresses only
# irreducible PyO3 / stub-only conventions (see stubtest-allowlist.txt).
# --ignore-disjoint-bases keeps the gate stable across mypy upgrades: the
# dual-base exceptions (e.g. Timeout(ProcessError, TimeoutError)) are PEP 800
# disjoint-base candidates a future mypy could start flagging.
- run: uv run python -m mypy.stubtest processkit --ignore-disjoint-bases --allowlist stubtest-allowlist.txt
pyright:
name: pyright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# A separate job (not an extra step in `typecheck` above): pyright and
# mypy disagree often enough (overloads/Literal/protocols/Awaitable
# patterns) that a pyright failure must never be masked by, or
# muddled with, the mypy/stubtest signal in one job's log.
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Checks the public typing surface in strict mode — see `[tool.pyright]`
# in pyproject.toml for the exact include scope and narrow diagnostic
# exceptions.
- run: uv run pyright
rust-lint:
name: rust-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# PyO3's build script needs a Python interpreter to resolve its config (even
# for a check-only clippy build, which never links libpython). Pin one
# explicitly rather than relying on the runner image's preinstalled python.
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.x'
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
components: clippy, rustfmt
- run: cargo fmt --all --check
- run: cargo clippy --all-targets -- -D warnings
# Runs the crate's own `#[cfg(test)]` unit tests (pure helpers in
# convert.rs/supervisor.rs — no PyO3/GIL/process-spawn coverage; that's
# `tests/`'s job, see docs/internals.md). Deliberately NOT
# `--features extension-module`/`--all-features`: that feature makes the
# crate a non-linkable Python extension, which `cargo test`'s own binary
# can't run.
- run: cargo test
rust-test-windows:
name: rust-test (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Build/install the extension exactly as a normal local checkout does.
- run: uv run maturin develop
# `cargo test --all-targets`, the command CONTRIBUTING.md documents for
# developers — but with every system Python hidden first, which is the only
# way this job can check the thing it exists for.
#
# Run plainly here, it would prove nothing: `windows-latest` ships a
# preinstalled CPython on PATH (the rust-lint job above pins its own
# interpreter rather than rely on it), and that directory contains
# python3.dll — exactly the DLL PyO3's abi3 build links. The loader would
# find it whether .cargo/config.toml's runner shim did its job, silently
# degraded, or was deleted, so the fix could rot here undetected.
#
# scripts/rust-test-no-system-python.ps1 strips every directory offering a
# python*.exe/python3*.dll from PATH and verifies none is left, which makes
# the shim the only possible source of the DLL, and sets
# PROCESSKIT_RUNNER_REQUIRE_PYTHON so the shim's deliberately fail-open
# lookup is fatal here rather than a diagnostic nobody reads. Either way a
# broken shim is now a red build: exit 3 if it cannot resolve the
# interpreter, STATUS_DLL_NOT_FOUND if it stops putting it on PATH.
#
# It also leaves PYO3_PYTHON unset on purpose, which covers the other
# documented promise: a bare `cargo test` needs no interpreter to BUILD
# either, because PyO3 0.29 configures this crate's abi3-py310 build for
# Windows without one. The next step pins PYO3_PYTHON itself and so pays
# for one extra PyO3 rebuild — the price of two checks that can fail
# instead of one that cannot.
- run: pwsh ./scripts/rust-test-no-system-python.ps1
# The explicit script alias stays supported (justfile, CONTRIBUTING.md), so
# keep it covered too — with PATH untouched, the way a developer runs it.
# It pins PYO3_PYTHON to uv's interpreter, so it checks the other end: the
# build following uv's choice instead of whatever `python` comes first.
- run: pwsh ./scripts/cargo-test-windows.ps1
test:
# One matrix over (OS × interpreter): the empty `python` entry runs the abi3
# (GIL) build via the project's pinned interpreter; `3.14t` provisions the
# free-threaded CPython (PEP 703) and builds the version-specific (non-abi3)
# extension, exercising the suite with the GIL disabled.
name: test (${{ matrix.os }}, ${{ matrix.python || 'abi3' }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-latest]
python: ['', '3.14t']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load exact release toolchain snapshot
if: matrix.os == 'windows-11-arm' && matrix.python == '3.14t'
shell: bash
run: python scripts/release/toolchain.py export-github-env >> "$GITHUB_ENV"
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# `uv run` provisions the interpreter, syncs the env (building the Rust
# extension via `maturin develop`), then runs pytest. The `--python` flag is
# added only for the free-threaded entry.
#
# Coverage (`--cov`, from the `[tool.coverage.run]` config in pyproject.toml)
# is measured on exactly one representative leg (ubuntu-latest, abi3/GIL):
# the pure-Python surface being measured has no OS-specific or free-
# threading-specific code paths, so instrumenting all 10 legs would only add
# overhead for an identical number. `--cov` (no path argument) picks up
# `[tool.coverage.run] source` from pyproject.toml; `parallel = true` there
# plus pytest-cov's built-in xdist support combine each worker's
# data so the total isn't undercounted.
- name: Run tests
if: >-
(matrix.os != 'ubuntu-latest' || matrix.python != '') &&
(matrix.os != 'windows-11-arm' || matrix.python != '3.14t')
run: uv run ${{ matrix.python && format('--python {0} ', matrix.python) }}pytest
- name: Build and test the native Windows ARM64 free-threaded wheel
if: matrix.os == 'windows-11-arm' && matrix.python == '3.14t'
shell: bash
env:
CIBW_ARCHS_WINDOWS: ARM64
CIBW_BUILD: cp314t-win_arm64
CIBW_TEST_COMMAND: >-
python -m pytest {project}/tests
--ignore={project}/tests/test_api_reference.py
--ignore={project}/tests/test_docs_snippets.py
CIBW_TEST_REQUIRES: >-
pytest~=9.1
pytest-timeout~=2.4
pytest-xdist~=3.8
hypothesis~=6.156
griffelib~=2.0
# uv has no native Windows ARM64 3.14t standalone build. cibuildwheel
# provisions the ARM64 NuGet interpreter used by the release matrix, so
# this tests the actual wheel instead of an x86_64 build under emulation.
# The ignored documentation guards require an editable source package
# and remain enforced by every ordinary test leg.
run: uvx "cibuildwheel==${CIBUILDWHEEL_VERSION}" --output-dir wheelhouse
- name: Run tests with coverage
if: matrix.os == 'ubuntu-latest' && matrix.python == ''
run: uv run pytest --cov --cov-report=term-missing --cov-report=xml --cov-report=html
- name: Stress the last-await interpreter-finalization boundary
if: matrix.os == 'ubuntu-latest' && matrix.python == ''
# The former bridge failed only in a narrow loaded window: its detached
# completion thread could still be inside Python after the await resumed.
# Sixteen concurrent short-lived interpreters over sixteen rounds keeps
# the exact "last act is await" shape under sustained scheduler pressure
# without making the entire cross-platform test matrix pay for it.
env:
PROCESSKIT_FINALIZATION_WORKERS: '16'
PROCESSKIT_FINALIZATION_ROUNDS: '16'
run: >-
uv run pytest
tests/test_hardening.py::test_last_async_await_survives_interpreter_finalization
-p no:xdist
-o addopts="-ra --strict-markers --strict-config --import-mode=importlib"
# Coverage measures Python only -- see the `[tool.coverage.run]` comment in
# pyproject.toml. A blocking threshold (`fail_under`, see
# `[tool.coverage.report]` in pyproject.toml) is enforced: `--cov` fails
# this step (and the job) if coverage drops below it. The baseline has
# been established from several runs' worth of history; the threshold
# sits a small margin below the measured percentage so it catches real
# regressions without flapping on xdist/measurement noise.
- name: Publish coverage summary
if: matrix.os == 'ubuntu-latest' && matrix.python == ''
run: |
{
echo '## Coverage (Python only)'
# Literal Markdown backticks inside single quotes, not command substitution.
# shellcheck disable=SC2016
printf '%s\n' \
'Not measured here: the compiled `_processkit` extension (see its Rust tests).' \
'Subprocess-only CLI drivers are excluded; see `[tool.coverage.run]` in pyproject.toml.'
echo '```'
uv run coverage report
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload coverage artifact
if: matrix.os == 'ubuntu-latest' && matrix.python == ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
path: |
htmlcov/
coverage.xml
if-no-files-found: error
test-privileged:
name: test (linux, privileged root + delegated cgroup v2)
runs-on: ubuntu-latest
# The only CI lane that actually exercises two Linux-only, root/cgroup-v2-
# gated paths the `test` matrix above silently skips everywhere (GH-hosted
# runners are never root and never have a delegated cgroup v2):
#
# 1. Real privilege-drop enforcement --
# test_privilege_drop_args_actually_drop_privilege (gated on
# `os.geteuid() != 0`).
# 2. cgroup v2 resource-limit enforcement -- ProcessGroup(max_memory=...,
# max_processes=..., cpu_quota=...) resolving to the `cgroup_v2`
# mechanism (not the POSIX `process_group` fallback), with the limits
# actually accepted by the kernel.
#
# Both run inside the existing Docker harness (compose.yaml / docker/Dockerfile)
# via `docker compose run --build --rm ...`, as two separate services/steps: a
# documented processkit-rs limitation (the "Linux cgroup caveat" on
# `Command::uid()`'s doc comment) means a privilege-dropped Command can never
# succeed under an active cgroup_v2 delegation (it tries to join its own
# root-owned auto-created cgroup AFTER the OS has dropped it to the
# unprivileged uid, which EACCESs) -- so the two paths need different
# container privilege postures, not just different pytest selections. Each
# step's JUnit report is checked by scripts/ci-privileged-guard.py so a
# silently-skipped gated test (wrong euid, no cgroup delegation, a
# process_group fallback, ...) fails the job instead of passing vacuously.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Root, no cgroup delegation -- privilege-drop enforcement
run: |
mkdir -p artifacts
docker compose run --build --rm -v "$PWD/artifacts:/app/artifacts" test-root \
uv run pytest --junitxml=artifacts/root-junit.xml
- name: Guard against a silently-skipped privilege-drop test
run: >-
python3 scripts/ci-privileged-guard.py artifacts/root-junit.xml
tests/test_command.py::test_privilege_drop_args_actually_drop_privilege
- name: Privileged root, delegated cgroup v2 -- resource-limit enforcement
# scripts/ci-privileged-check.sh re-parents into the real cgroup-v2 root
# (needs compose.yaml's `privileged: true` + `cgroup: host` on the `test`
# service), asserts the mechanism is actually cgroup_v2, then runs the
# suite (minus the privilege-drop test -- see the job comment above).
#
# continue-on-error: unlike privilege-drop above (which only needs the
# container to run as root -- host-independent), delegated cgroup v2
# depends on the GH-hosted runner's host cgroup version / cgroup
# namespace mode / controller availability, none of which has been
# empirically confirmed on actual GH infrastructure yet (verified so
# far only locally, in a Docker Desktop-family engine on a developer
# machine -- never yet on a real GH-hosted runner). This mechanism has
# historically needed post-hoc CI fixes. This is a conscious, documented
# decision (not an oversight): keep this step non-blocking for its first
# real GH-hosted run(s) so a delegation failure specific to GH's runner
# posture doesn't block pushes while it's being confirmed. Once a real
# GH Actions run confirms the mechanism resolves to cgroup_v2 here,
# remove `continue-on-error` from this step and the guard step below to
# restore it as a full blocking gate.
continue-on-error: true
run: |
docker compose run --build --rm -v "$PWD/artifacts:/app/artifacts" test \
bash scripts/ci-privileged-check.sh
- name: Guard against a silently-skipped / process_group-fallback cgroup v2 run
# Also continue-on-error, for the same reason as the step above: if the
# cgroup v2 delegation step failed (or was skipped) before producing a
# JUnit report, this guard would fail on the missing/incomplete file.
# Remove alongside the continue-on-error above once confirmed on a real
# GH-hosted run.
continue-on-error: true
run: >-
python3 scripts/ci-privileged-guard.py artifacts/privileged-junit.xml
tests/test_process_group.py::test_resource_limited_group_runs
tests/test_process_group.py::test_update_limits_replaces_each_axis_on_live_group
tests/test_process_group.py::test_group_cpu_quota_kwarg_accepted
tests/test_process_group.py::test_group_reports_a_mechanism
test-musl:
name: test (musl / Alpine, full suite)
runs-on: ubuntu-latest
# Run the REAL (real-subprocess) pytest suite against a musl build inside an
# Alpine container -- not just the import smoke test (scripts/smoke.py) the
# published musllinux wheels get. The project builds and ships musllinux
# wheels (build-musllinux above + the release matrix) and Alpine/musl is a
# very common CI/container base, but until now the full suite only ever ran
# on glibc (the `test` matrix / `test-privileged`) -- so a musl-specific
# divergence in the syscall / signal / privilege-drop layer (which differs
# from glibc) could ship in a wheel while every green check was glibc-only.
# The sibling processkit-rs crate added its own `test-musl` lane for exactly
# this reason; this is the binding's equivalent.
#
# Kept in this PR-gating workflow (not nightly-hardening.yml) on purpose: musl
# is a *shipped* platform, so a regression should block the merge that
# introduces it, not surface a night later in a workflow that gates nothing.
# The cost is bounded and not a new category for this gate -- CI jobs run
# concurrently (the PR-gate wall-clock is the slowest job, not the sum), and
# this workflow already runs a docker-compose Linux suite on every PR
# (test-privileged) plus a full musl Rust compile on every PR (build-musllinux
# via cibuildwheel); a debug `maturin develop` build + pytest is the same
# shape and no slower than those.
#
# Runs via `docker compose run` from the host (like test-privileged), NOT a
# job-level `container:`: GitHub injects a glibc `node` into job containers to
# run JS actions (checkout / setup-uv), and that binary does not execute on a
# musl Alpine container -- so a container-per-job posture is fragile here,
# while the host-driven compose harness keeps checkout on the glibc runner and
# confines musl to the container.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run the full suite on musl (Alpine)
# `--build` compiles the musl extension, then the image's default CMD
# (`uv run pytest`) runs the full suite as root inside the Alpine
# container. Root (Docker's default container user) means os.geteuid()==0,
# so the privilege-drop test runs on musl here too; cgroup enforcement
# falls back to `process_group` (unprivileged container), which the
# resource-limit tests tolerate -- see the `test-musl` service in
# compose.yaml.
run: docker compose run --build --rm test-musl
event-loops:
# Dedicated job for tests/test_event_loops.py: turns docs/event-loops.md's
# "uvloop and anyio-on-asyncio are fully supported / works today" claim
# into a checked contract by rerunning a representative async subset
# (aoutput, astart + streaming, cancellation, aoutput_all, the readiness
# helpers) under the real uvloop and anyio-on-asyncio runtimes -- not a
# mocked event loop. Kept separate from `test` above (not folded into its
# matrix): the ordinary PR-gate `test` job must not require uvloop/anyio
# as dependencies, so only this job installs the optional `event-loops`
# dependency group (`uv sync --group event-loops`); `test` never does.
#
# uvloop ships no Windows wheels, so its `pyproject.toml` dependency-group
# entry is marked `sys_platform != 'win32'` -- on windows-latest, `uv sync`
# here installs anyio only, and the uvloop-parametrized tests in
# test_event_loops.py self-skip with an explicit `skipif` reason (visible
# in the job's test summary), rather than silently vanishing. On the other
# two OSes, uvloop is a hard member of the group: if it ever failed to
# install there, the `uv sync` step below would fail loudly, so this run
# can't pass vacuously with uvloop silently missing.
name: event-loops (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- run: uv sync --group event-loops
- name: Run the event-loop compatibility matrix
run: uv run pytest tests/test_event_loops.py
build:
name: build (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load exact release toolchain snapshot
if: matrix.os == 'ubuntu-latest'
shell: bash
run: python scripts/release/toolchain.py export-github-env >> "$GITHUB_ENV"
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Verify the abi3 wheel (cp310+) builds on every target OS, not just Linux —
# a one-OS build would not catch a Windows/macOS packaging break. A debug
# build (no --release) exercises the same abi3 tag / cdylib link / packaging
# path much faster. The optimized (LTO) wheels are built + smoke-tested by
# cibuildwheel: musl x86_64 here (build-musllinux), and the FULL matrix
# (every OS/arch, incl. Windows/macOS) pre-pivot in the release flow — so a
# release-profile-only break can't reach PyPI, it just surfaces at release.
- run: uv run maturin build --out dist
# The sdist is platform-independent; build it once.
- if: matrix.os == 'ubuntu-latest'
run: uv run maturin sdist --out dist
# Assert the release cibuildwheel selector resolves to exactly the intended
# wheel families per libc — the abi3 GIL wheel (cp310) and the free-threaded
# cp314t wheel, for both manylinux (glibc) and musllinux (musl) — so a
# renamed/dropped/re-skipped identifier surfaces here, not at release. (The
# musllinux wheels are actually built + smoke-tested in the build-musllinux
# job below; this only checks the selector resolves.)
- if: matrix.os == 'ubuntu-latest'
name: Verify cibuildwheel build selector
run: |
ids=$(uvx "cibuildwheel==${CIBUILDWHEEL_VERSION}" --print-build-identifiers --platform linux)
echo "$ids"
for fam in cp310-manylinux cp310-musllinux cp314t-manylinux cp314t-musllinux; do
echo "$ids" | grep -q "^$fam" || { echo "::error::$fam wheel not in cibuildwheel selector"; exit 1; }
done
build-musllinux:
# Actually build + smoke-test the musllinux (Alpine/musl) wheels. This is the
# only thing on a push that exercises the musl Rust toolchain path —
# cibuildwheel otherwise runs only in the dispatch-only release flows, so a
# musl-specific break (the before-all hook, maturin's cdylib crt-static
# handling) would otherwise surface only at release. Builds both wheel families
# for x86_64 in the Alpine container (Rust is installed inside it by the
# before-all hook in pyproject.toml — no host toolchain needed); aarch64 musl
# builds natively at release on the ubuntu-24.04-arm runner.
name: build (musllinux x86_64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Load exact release toolchain snapshot
shell: bash
run: python scripts/release/toolchain.py export-github-env >> "$GITHUB_ENV"
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
- name: Build + smoke-test musllinux wheels
# Pin the exact two families (abi3 GIL + free-threaded). CIBW_BUILD
# OVERRIDES pyproject's build selector, so a wildcard like *-musllinux_x86_64
# would also pull cp311–cp314 (redundant abi3-reuse smoke runs).
env:
CIBW_BUILD: "cp310-musllinux_x86_64 cp314t-musllinux_x86_64"
run: uvx "cibuildwheel==${CIBUILDWHEEL_VERSION}" --platform linux --output-dir wheelhouse
rust-audit:
name: cargo-deny
runs-on: ubuntu-latest
# Supply-chain gate for the Rust dependency tree (pyo3, tokio, processkit,
# ...) that gets compiled into every wheel -- the pip-audit job below only
# covers the Python-side (dev/build) dependencies. Kept in this PR-gating
# workflow (not nightly-hardening.yml): the compiled extension ships to
# every user as-is, so a newly-disclosed RustSec advisory or a license/ban
# violation should block the next merge rather than wait for the nightly
# schedule -- the same reasoning already applied to pip-audit here. No
# network access beyond what pip-audit already needs (fetching the
# rust-secure-code/advisory-db + crates.io index, both cached by the
# action between runs).
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Scan the dependency tree for security advisories, banned/duplicate
# crates, licenses outside the permissive allow-list, and disallowed
# sources (deny.toml). Adapted from the sibling ProcessKit-rs
# repository's own cargo-deny gate.
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with:
command: check advisories bans licenses sources
audit:
name: pip-audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true
# Audit the locked dependency tree. Exporting the lockfile (--no-emit-project
# drops the editable binding itself) lets pip-audit scan the deps without
# building the Rust extension — so no rust-toolchain / compile step is needed.
# The project has no runtime deps, so the dev groups ARE the audit surface:
# --all-groups makes that explicit (not reliant on uv's default-includes-dev),
# and --locked pins the scan to the committed uv.lock (failing loudly if stale).
- run: >-
uv export --no-emit-project --all-groups --locked
--format requirements-txt --output-file requirements-audit.txt
- run: uvx 'pip-audit>=2,<3' -r requirements-audit.txt