Repository navigation
1338 lines (1307 loc) · 81.7 KB
/
Copy pathqualification.yml
File metadata and controls
1338 lines (1307 loc) · 81.7 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
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: qualification
# Design #359 WP-4: one PR workflow, one required status context (the `verdict` job, whose own
# `name:` is `qualification`), eighteen leaf jobs behind one router, one aggregate (#1017 added the
# two AArch64 jobs, #1061 `artifact-identity` and `artifact-record`, #1043 `gate-self-tests`).
# Stage 3 (§12) retired the four workflows this one replaced -- ci.yml, sdk.yml,
# browser-qualification.yml and release-build.yml -- once this workflow had reported on >= 10 PRs
# and agreed with them; this is now the sole required PR workflow. See design §4/§5/§7/§12.
#
# No `paths:`/`paths-ignore:` anywhere in this file, by design and by policy check: a required
# workflow that can be skipped by a path filter leaves a PR pending forever. Every job below is
# itself gated by the router's `route` output instead, and the verdict enforces that every job's
# result matches a static expectation table -- a leaf `if:` that drifts from the table fails the
# verdict in both directions.
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
concurrency:
group: qualification-${{ github.event.pull_request.number || github.ref }}
# S9: cancelling a main push here (cancel-in-progress: true, unconditionally) races the next
# main push's superseding run against every rust-cache's `save-if: github.ref == main` post
# step. A push run cancelled mid-cache-save writes nothing, so under frequent merges to main the
# caches every PR depends on may never be written. PR runs still want unconditional cancellation
# (a superseded PR run's work is worthless the moment a new commit lands), so cancel only on
# pull_request; a main push always runs to completion and its caches always save.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUSTUP_TOOLCHAIN: 1.99.1
jobs:
route:
name: classify qualification paths and step conditions
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
route: ${{ steps.classify.outputs.route }}
math_closure: ${{ steps.classify.outputs.math_closure }}
release_inputs: ${{ steps.classify.outputs.release_inputs }}
self_tests: ${{ steps.classify.outputs.self_tests }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Validate path-routing policy and mutations
run: |
python3 -B scripts/check-ci-path-routing.py
python3 -B scripts/test-ci-path-routing.py
- id: classify
name: Select fail-safe qualification route and step conditions
run: |
# pipefail: without it a router crash leaves an empty GITHUB_OUTPUT and the step exits 0,
# and the failure only surfaces as "unknown route" in the verdict a full cycle later.
set -o pipefail
python3 -B scripts/ci-path-router.py --flags \
--event "${{ github.event_name }}" \
--base "${{ github.event.pull_request.base.sha || github.event.before }}" \
--head "${{ github.event.pull_request.head.sha || github.sha }}" \
| tail -n 4 >> "$GITHUB_OUTPUT"
docs-gates:
name: docs and research evidence gates
needs: route
# Runs on the evidence route and on the full route (a mixed docs+code PR routes full; the
# research and listening-packet validators must not disappear for it).
if: needs.route.outputs.route == 'evidence' || needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install ripgrep (not on the runner image; every policy script greps with rg)
run: |
sudo apt-get update
sudo apt-get install -y ripgrep
# dsp-research/**/*.md joined the evidence family (design §2); its own validators move here
# so a research-note edit does not ride the full route. Hermetic: no cargo, no clock. The
# research gate's mutation tests run in gate-self-tests when a file they read changes (#1043).
- name: DSP research corpus and listening-evidence packet validators
run: |
bash scripts/check-dsp-research.sh
bash scripts/check-builtins-listening.sh
artifact:
name: shipped AudioWorklet artifact
needs: route
if: needs.route.outputs.route == 'sdk' || needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 15
outputs:
sha256: ${{ steps.build.outputs.sha256 }}
toolchain: ${{ steps.build.outputs.toolchain }}
closure_sha256: ${{ steps.build.outputs.closure_sha256 }}
named_sha256: ${{ steps.build.outputs.named_sha256 }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: artifact
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain and Wasm standard library
run: |
rustup toolchain install 1.99.1 --profile minimal
rustup target add --toolchain 1.99.1 wasm32-unknown-unknown
channel="$(bash scripts/build-web-audioworklet.sh --print-toolchain)"
rustup toolchain install "$channel" --profile minimal --component rust-src
# Built exactly once here; every consumer downloads it and checks it against this job's
# `sha256` output, so every artifact gate in the run reads exactly these bytes. #1044: the
# parameter metadata and ABI layout are generated exactly once too, here, and `closure_sha256`
# (one digest over every file's sha256, in byte order of name) is what every consumer checks
# the whole directory against, so none of them runs the generator again. Issue #1061
# (owner decision 5): no comparison with the committed pin here. The pin is the release
# fingerprint; `artifact-identity` reports whether these bytes changed and holds a release
# change to the pin (docs/RELEASE.md). Issue #1109: the same build writes the named twin, the
# shipped module plus its `name` section, outside the delivery closure; `named_sha256` is what
# artifact-gates checks its download against.
- name: Build the exact shipped artifact
id: build
run: |
# The build script refuses a non-empty output directory; never rely on the cache
# action having pruned a previous run's loose files.
rm -rf target/ci/qualification-artifacts target/ci/qualification-named-twin
mkdir -p target/ci/qualification-artifacts target/ci/qualification-named-twin
bash scripts/build-web-audioworklet.sh --named-twin target/ci/qualification-named-twin target/ci/qualification-artifacts
sha256="$(sha256sum target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm | awk '{print $1}')"
echo "sha256=$sha256" >> "$GITHUB_OUTPUT"
named_sha256="$(sha256sum target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm | awk '{print $1}')"
echo "named_sha256=$named_sha256" >> "$GITHUB_OUTPUT"
echo "toolchain=$(bash scripts/build-web-audioworklet.sh --print-toolchain)" >> "$GITHUB_OUTPUT"
closure="$(set -o pipefail; cd target/ci/qualification-artifacts && find . -type f -printf '%P\0' | LC_ALL=C sort -z | xargs -0 sha256sum -- | sha256sum | awk '{print $1}')"
echo "closure_sha256=$closure" >> "$GITHUB_OUTPUT"
echo "Shipped AudioWorklet module at \`$GITHUB_SHA\`: \`$sha256\` (the \`artifact-identity\` summary says whether it changed)" >> "$GITHUB_STEP_SUMMARY"
# Issue #2045: the shipped bytes name the compiler that built them. The module's `producers`
# section must give rustc exactly the version the browser artifact toolchain's rustc prints,
# so a build on another rustc fails here, before anything is uploaded.
- name: Check the shipped module names the browser artifact toolchain's compiler
env:
TOOLCHAIN: ${{ steps.build.outputs.toolchain }}
run: |
python3 -B scripts/check-wasm-producers.py --self-test
python3 -B scripts/check-wasm-producers.py target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm --toolchain "$TOOLCHAIN"
python3 -B scripts/check-wasm-producers.py target/ci/qualification-artifacts/miso-engine-v1-blake3.simd128.wasm --toolchain "$TOOLCHAIN"
- name: Upload shipped artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: audioworklet-${{ github.sha }}
path: target/ci/qualification-artifacts/
if-no-files-found: error
overwrite: true
# Issue #1109: the named twin is not shipped. It is kept for the gates that find functions by
# name (artifact-gates) and to symbolicate a production stack trace (docs/RELEASE.md).
- name: Upload the named twin (not shipped)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: audioworklet-named-${{ github.sha }}
path: target/ci/qualification-named-twin/
if-no-files-found: error
overwrite: true
# Issue #1061 (owner decision 5): every PR says whether the shipped module changed against the
# digest its base's own CI run recorded, proves the build reproducible, and holds only a release
# change to the committed pin (docs/RELEASE.md). Its own job, so the twin build stays off the
# path of `sdk`, `artifact-gates` and the browser legs, which start once `artifact` has uploaded.
# scripts/web-audioworklet-identity.py has the rules.
artifact-identity:
name: shipped AudioWorklet artifact identity (changed or unchanged, reproducible, release pin)
needs: [route, artifact]
if: needs.route.outputs.route == 'sdk' || needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
statuses: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
# No toolchain in the runner's RUSTUP_HOME: the twin installs its own, and the report runs
# only git and gh (#2045).
# The twin: this commit again, from another checkout path, with another CARGO_HOME and with
# the browser artifact toolchain freshly installed into another RUSTUP_HOME: the three roots
# rustc could embed (#2045: the third holds the sysroot remap). `artifact` built from
# $GITHUB_WORKSPACE with the runner's. A fresh install, not a copy: a copied toolchain
# directory carries no rustup manifest, and RUSTUP_TOOLCHAIN could resolve elsewhere.
- name: Rebuild the module from another checkout path, CARGO_HOME and RUSTUP_HOME
run: |
git worktree add --detach "$RUNNER_TEMP/twin" HEAD
mkdir "$RUNNER_TEMP/twin-module"
channel="$(bash "$RUNNER_TEMP/twin/scripts/build-web-audioworklet.sh" --print-toolchain)"
RUSTUP_HOME="$RUNNER_TEMP/twin-rustup" rustup toolchain install "$channel" --profile minimal --component rust-src
RUSTUP_HOME="$RUNNER_TEMP/twin-rustup" CARGO_HOME="$RUNNER_TEMP/twin-cargo-home" bash "$RUNNER_TEMP/twin/scripts/build-web-audioworklet.sh" --module-only "$RUNNER_TEMP/twin-module"
# The base is never rebuilt here: this run's workflow is the change's, and a base built in it
# is built in the change's environment (#1061 attempts 1 and 2). The report reads the digest
# the base's own run recorded instead, and says CANNOT TELL when there is none.
- name: Report ARTIFACT CHANGED or UNCHANGED against the base's recorded digest, and hold a release change to its pin
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
BEFORE: ${{ github.event.before }}
BUILT: ${{ needs.artifact.outputs.sha256 }}
TOOLCHAIN: ${{ needs.artifact.outputs.toolchain }}
run: |
set -o pipefail
python3 -B scripts/web-audioworklet-identity.py --self-test
python3 -B scripts/web-audioworklet-identity.py report --event "$EVENT" --before "$BEFORE" --repository "$GITHUB_REPOSITORY" --built "$BUILT" --toolchain "$TOOLCHAIN" --twin "$RUNNER_TEMP/twin-module/miso-engine-v1-audio-worklet.simd128.wasm" | tee -a "$GITHUB_STEP_SUMMARY"
# Issue #1061: main's own record of the module this commit's run built, as the commit status
# `audioworklet-sha256` (`<sha256> rustc <toolchain>`, linking the run; #1334: the toolchain is
# the browser artifact's dated nightly channel). Later changes compare
# with it (`artifact-identity`), so no change ever rebuilds its base inside its own, possibly
# edited, workflow. Its own job, run only on a push to main, checking out nothing and running no
# repository code: it is the only job holding a write token, and no pull request reaches it.
artifact-record:
name: record the shipped AudioWorklet module's digest on main
needs: [route, artifact]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && (needs.route.outputs.route == 'sdk' || needs.route.outputs.route == 'full')
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
statuses: write
steps:
- name: Record this commit's module for later changes to compare against
env:
GH_TOKEN: ${{ github.token }}
SHA256: ${{ needs.artifact.outputs.sha256 }}
TOOLCHAIN: ${{ needs.artifact.outputs.toolchain }}
run: |
[[ "$SHA256" =~ ^[0-9a-f]{64}$ && "$TOOLCHAIN" =~ ^nightly-[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "malformed record: '$SHA256' rustc '$TOOLCHAIN'" >&2; exit 1; }
gh api -X POST "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" -f state=success -f context=audioworklet-sha256 -f "description=$SHA256 rustc $TOOLCHAIN" -f "target_url=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
sdk:
name: SDK package and generated-surface qualification
needs: [route, artifact]
if: needs.route.outputs.route == 'sdk' || needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: sdk
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: sdk/package-lock.json
# check-sdk-generated.sh no longer runs the parameter-metadata generator (#1044): it compares
# sdk/assets with the documents the artifact job generated, which the closure digest below
# proves this job reads. The toolchain stays for check-sdk-headless.sh, whose evals cross-check
# the SDK against native debug oracles (`cargo run -p parameter-metadata --bin
# parameter_metadata_lattice_oracle`, `cargo run -p host-web --example sdk_render_oracle`).
- name: Install pinned Rust toolchain and Wasm standard library
run: |
rustup toolchain install 1.99.1 --profile minimal
rustup target add --toolchain 1.99.1 wasm32-unknown-unknown
- name: Install SDK dependencies
working-directory: sdk
run: npm ci --no-audit --no-fund --prefer-offline
- name: Download shipped artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: audioworklet-${{ github.sha }}
path: target/ci/qualification-artifacts
- name: Verify the downloaded artifact against the artifact job's digest
env:
BUILT: ${{ needs.artifact.outputs.sha256 }}
CLOSURE: ${{ needs.artifact.outputs.closure_sha256 }}
run: |
actual="$(sha256sum target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm | awk '{print $1}')"
[[ "$BUILT" =~ ^[0-9a-f]{64}$ && "$actual" == "$BUILT" ]] || { echo "downloaded artifact sha256 mismatch: the artifact job built $BUILT, got $actual" >&2; exit 1; }
closure="$(set -o pipefail; cd target/ci/qualification-artifacts && find . -type f -printf '%P\0' | LC_ALL=C sort -z | xargs -0 sha256sum -- | sha256sum | awk '{print $1}')"
[[ "$CLOSURE" =~ ^[0-9a-f]{64}$ && "$closure" == "$CLOSURE" ]] || { echo "downloaded artifact closure mismatch: the artifact job built $CLOSURE, got $closure" >&2; exit 1; }
# Issue #1334: sdk-package.sh check runs test-sdk-artifact-builder-output-contract.sh, which runs
# the real builder with only cargo mocked; the builder asks the browser artifact toolchain for
# its sysroot. Installed after the verification, which no repository script may precede.
- name: Install the browser artifact toolchain
run: |
channel="$(bash scripts/build-web-audioworklet.sh --print-toolchain)"
rustup toolchain install "$channel" --profile minimal --component rust-src
# check-sdk-deletions.py's --self-test (design §6.6, S7) runs in gate-self-tests
# when the gate or the SDK tree it reads changes (#1043); the gate itself runs here.
# test-sdk-artifact-builder-output-contract.sh (also design §6.6) is not added separately:
# sdk-package.sh check already runs it as its own first step (see scripts/sdk-package.sh:29-30)
# before the `sdk-package.sh check` call below, so adding it here again would be a duplicate
# invocation, not a new gate.
# Issue #1435: the five commands run with a fresh, private TMPDIR, so anything they or a
# process they start leave there fails the step (the runner's shared temporary directory hid
# the enginectl CLI test's leak). A command's own failure still fails the step first; the trap
# removes the directory.
- name: Qualify the SDK package against the shared artifact
run: |
# pipefail: GitHub's default shell is `bash -e {0}`, so without it a `find` failure in the
# emptiness check would leave `leftover` empty and the check would pass.
set -o pipefail
scratch="$(mktemp -d "$RUNNER_TEMP/sdk-qualify-tmp.XXXXXX")"
trap 'rm -rf -- "$scratch"' EXIT
export TMPDIR="$scratch"
# The TMPDIR is private and deleted on exit, so a compile cache in it has no reuse value; tsc
# (typescript's tsc.js) and npm 10's cli.js would write node-compile-cache into it.
export NODE_DISABLE_COMPILE_CACHE=1
bash scripts/check-sdk-generated.sh target/ci/qualification-artifacts
python3 -B scripts/check-sdk-deletions.py
bash scripts/check-sdk-types.sh
bash scripts/check-sdk-headless.sh target/ci/qualification-artifacts
bash scripts/sdk-package.sh check target/ci/qualification-artifacts
leftover="$(find "$scratch" -mindepth 1 -maxdepth 1 -printf '%f\n' | LC_ALL=C sort)"
if [[ -n "$leftover" ]]; then
printf 'the SDK qualify step left these entries in its TMPDIR:\n%s\n' "$leftover" >&2
exit 1
fi
# check-sdk-generated.sh, given the artifact directory, skips the SOURCE_ONLY assets
# (sdk/codegen/assets.mjs: session keys and response frame), whose currency tests otherwise run
# only on the full route; an sdk-only change routes here, so they run here too (#1623).
- name: Check the source-only SDK assets are current
run: cargo test --locked -p parameter-metadata --test response_frame --test session_keys
artifact-gates:
name: AudioWorklet artifact gates
needs: [route, artifact]
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: artifact-gates
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain and wabt
run: |
rustup toolchain install 1.99.1 --profile minimal
rustup target add --toolchain 1.99.1 wasm32-unknown-unknown
sudo apt-get update
sudo apt-get install -y ripgrep wabt
- name: Download shipped artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: audioworklet-${{ github.sha }}
path: target/ci/qualification-artifacts
- name: Verify the downloaded artifact against the artifact job's digest
env:
BUILT: ${{ needs.artifact.outputs.sha256 }}
CLOSURE: ${{ needs.artifact.outputs.closure_sha256 }}
run: |
actual="$(sha256sum target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm | awk '{print $1}')"
[[ "$BUILT" =~ ^[0-9a-f]{64}$ && "$actual" == "$BUILT" ]] || { echo "downloaded artifact sha256 mismatch: the artifact job built $BUILT, got $actual" >&2; exit 1; }
closure="$(set -o pipefail; cd target/ci/qualification-artifacts && find . -type f -printf '%P\0' | LC_ALL=C sort -z | xargs -0 sha256sum -- | sha256sum | awk '{print $1}')"
[[ "$CLOSURE" =~ ^[0-9a-f]{64}$ && "$closure" == "$CLOSURE" ]] || { echo "downloaded artifact closure mismatch: the artifact job built $CLOSURE, got $closure" >&2; exit 1; }
# Issue #1334: the render-lock probe below builds on the shipped module's cargo line, so on the
# browser artifact toolchain. Installed after the verification, which no repository script
# may precede.
- name: Install the browser artifact toolchain
run: |
channel="$(bash scripts/build-web-audioworklet.sh --print-toolchain)"
rustup toolchain install "$channel" --profile minimal --component rust-src
# Issue #1109: the shipped module carries no `name` section, so the three gates below that find
# functions by name -- the call graph (inside check-web-audioworklet.sh), scalar-oracle-absent
# and the V8 spill gate -- read the named twin. Its download is checked against the artifact
# job's digest, and then against the shipped module: the same sections, byte for byte, but
# its `name` section, so what the gates prove of its code holds for the code that ships.
- name: Download the named twin
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: audioworklet-named-${{ github.sha }}
path: target/ci/qualification-named-twin
- name: Verify the named twin against the artifact job's digest and the shipped module
env:
NAMED: ${{ needs.artifact.outputs.named_sha256 }}
run: |
actual="$(sha256sum target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm | awk '{print $1}')"
[[ "$NAMED" =~ ^[0-9a-f]{64}$ && "$actual" == "$NAMED" ]] || { echo "downloaded named twin sha256 mismatch: the artifact job built $NAMED, got $actual" >&2; exit 1; }
python3 -B scripts/strip-wasm-names.py --self-test
python3 -B scripts/strip-wasm-names.py check target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm
# #106 E1/E2/E5/E7 and the F4 native<->simd128 session parity, over the artifact that ships.
# check-web-audioworklet.sh asserts the frozen export set, no imports, no shared memory, no
# atomics or relaxed SIMD, the render export's allocation-free/trap-free closure, and the
# vector-kernel ratchet. check-browser-expected-resources.py --artifacts runs the same
# direct-oracle.mjs digest parity and unconditionally self-tests before returning (design
# §6.3: subsumes the retired web-audioworklet-browser-correctness.py --check and its own
# --self-test step). #1044: `--without-metadata-regeneration` leaves out only the gate's
# `parameter-metadata --check`, which here compared the generator with itself: the artifact
# job generated these documents at this commit, and the closure digest was verified above.
- name: Browser AudioWorklet artifact gates and native parity
run: |
bash scripts/check-web-audioworklet.sh --without-metadata-regeneration target/ci/qualification-artifacts target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm
python3 -B scripts/check-browser-expected-resources.py --artifacts target/ci/qualification-artifacts
# #1059: the whole-plan scalar renderer is a test-only oracle; the shipped module must not
# contain it (owner ruling 2026-09-28, decision 3). Read from the named twin (#1109).
- name: Shipped AudioWorklet module contains no whole-plan scalar path
run: python3 -B scripts/check-scalar-oracle-absent.py --wasm target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm
# Issue #1429: the suite runs with a fresh, private TMPDIR, so anything it or a process it
# starts leaves there fails the step (the runner's shared temporary directory hid #1421's
# leak). The script's own failure still fails the step first; the trap removes the directory.
- name: Hermetic browser host and worklet tests
run: |
# pipefail: GitHub's default shell is `bash -e {0}`, so without it a `find` failure in the
# emptiness check would leave `leftover` empty and the check would pass.
set -o pipefail
scratch="$(mktemp -d "$RUNNER_TEMP/test-web-audioworklet-tmp.XXXXXX")"
trap 'rm -rf -- "$scratch"' EXIT
export TMPDIR="$scratch"
bash scripts/test-web-audioworklet.sh
leftover="$(find "$scratch" -mindepth 1 -maxdepth 1 -printf '%f\n' | LC_ALL=C sort)"
if [[ -n "$leftover" ]]; then
printf 'scripts/test-web-audioworklet.sh left these entries in its TMPDIR:\n%s\n' "$leftover" >&2
exit 1
fi
# Issue #1529: the positive control of the render-allocation gates, on a module built from the
# shipped sources and cargo line plus the default-off `render-lock-probe` feature: an
# allocation inside a render-locked window moves the count by exactly 2. It is a separately
# named module in a temporary directory, never the shipped one, never uploaded.
- name: Render-lock probe counts an allocation in the browser module
run: |
probe="$(mktemp -d "$RUNNER_TEMP/render-lock-probe.XXXXXX")"
trap 'rm -rf -- "$probe"' EXIT
bash scripts/build-web-audioworklet.sh --render-lock-probe "$probe"
node scripts/test-render-lock-probe.mjs "$probe/miso-engine-v1-render-lock-probe.wasm"
# Issues #1000/#1009: V8's register allocation of the EQ's stationary cascade loops, read from
# the downloaded, digest-verified named twin of the artifact (#1109; run-wasm-gates.sh
# --without-v8-spill leaves it to this job). Register allocation belongs to one V8, so the Node version is exact and the
# gate refuses any other. Last, so the steps above keep the runner's own Node.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.23.2
- name: V8 spill gate over the shipped module's EQ cascade loops
run: |
python3 -B scripts/check-web-audioworklet-v8-spill.py --self-test
python3 -B scripts/check-web-audioworklet-v8-spill.py target/ci/qualification-named-twin/miso-engine-v1-audio-worklet.simd128.named.wasm
browser:
name: ${{ matrix.browser }} — browser qualification gates
needs: [route, artifact]
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
browser: [chromium, webkit]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm
cache-dependency-path: hosts/host-web/qualification/package-lock.json
- name: Download shipped artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: audioworklet-${{ github.sha }}
path: target/ci/qualification-artifacts
- name: Install SDK dependencies for the response Worker bundle
working-directory: sdk
run: npm ci --no-audit --no-fund --prefer-offline
- name: Verify the downloaded artifact against the artifact job's digest
env:
BUILT: ${{ needs.artifact.outputs.sha256 }}
CLOSURE: ${{ needs.artifact.outputs.closure_sha256 }}
run: |
actual="$(sha256sum target/ci/qualification-artifacts/miso-engine-v1-audio-worklet.simd128.wasm | awk '{print $1}')"
[[ "$BUILT" =~ ^[0-9a-f]{64}$ && "$actual" == "$BUILT" ]] || { echo "downloaded artifact sha256 mismatch: the artifact job built $BUILT, got $actual" >&2; exit 1; }
closure="$(set -o pipefail; cd target/ci/qualification-artifacts && find . -type f -printf '%P\0' | LC_ALL=C sort -z | xargs -0 sha256sum -- | sha256sum | awk '{print $1}')"
[[ "$CLOSURE" =~ ^[0-9a-f]{64}$ && "$closure" == "$CLOSURE" ]] || { echo "downloaded artifact closure mismatch: the artifact job built $CLOSURE, got $closure" >&2; exit 1; }
- name: Cache Playwright browser binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ hashFiles('hosts/host-web/qualification/package-lock.json') }}-${{ matrix.browser }}-${{ runner.os }}
- name: Install pinned browser
working-directory: hosts/host-web/qualification
run: |
npm ci
npx playwright install "${{ matrix.browser }}"
# Prefer the runner's official archives; Azure downloads hit the job budget (#1161).
printf 'https://archive.ubuntu.com/ubuntu/\tpriority:1\nhttps://security.ubuntu.com/ubuntu/\tpriority:2\n' | sudo tee /etc/apt/apt-mirrors.txt >/dev/null
npx playwright install-deps "${{ matrix.browser }}"
sudo apt-get install -y --no-install-recommends pulseaudio pulseaudio-utils
- name: ${{ matrix.browser }} — attestation, AudioWorklet, native digest, and stall gates
working-directory: hosts/host-web/qualification
run: |
# A real AudioContext needs an output device even on a headless runner. Keep this
# browser job's sink/socket private; qualification still renders the actual Worklet.
qualification_audio_dir="$(mktemp -d "$RUNNER_TEMP/qualification-audio.XXXXXX")"
export PULSE_SERVER="unix:$qualification_audio_dir/native"
DBUS_SESSION_BUS_ADDRESS="unix:path=$qualification_audio_dir/no-session-bus" \
pulseaudio --daemonize=no --exit-idle-time=-1 --use-pid-file=no --disable-shm=yes -n \
--load="module-native-protocol-unix socket=$qualification_audio_dir/native auth-anonymous=1" \
--load="module-null-sink sink_name=qualification rate=48000 channels=2" \
--log-target="file:$qualification_audio_dir/pulse.log" &
qualification_audio_pid=$!
trap 'kill "$qualification_audio_pid" 2>/dev/null || true; wait "$qualification_audio_pid" 2>/dev/null || true; cat "$qualification_audio_dir/pulse.log"' EXIT
for attempt in {1..50}; do
[[ -S "$qualification_audio_dir/native" ]] && break
sleep 0.1
done
[[ -S "$qualification_audio_dir/native" ]]
pactl -s "$PULSE_SERVER" info
npm run qualify -- --artifacts "$GITHUB_WORKSPACE/target/ci/qualification-artifacts" --sdk-root "$GITHUB_WORKSPACE/sdk" --browser "${{ matrix.browser }}" --check-matrix --self-test-mutations
lint:
name: fmt, clippy, doc, and hermetic policy gates
needs: route
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# #1044: the hermetic host, worklet and qualification-boot suite (#288,
# scripts/test-web-audioworklet.mjs) runs in artifact-gates, as the first line of
# `bash scripts/test-web-audioworklet.sh`, on the same full route; it no longer runs here too.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: lint
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain
run: |
rustup toolchain install 1.99.1 --profile minimal --component clippy --component rustfmt
sudo apt-get update
sudo apt-get install -y ripgrep
- name: Hydrate locked Cargo dependencies
run: cargo fetch --locked
# fmt first: the fastest possible first failure.
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
- name: Documentation
run: RUSTDOCFLAGS='-D warnings' cargo doc --locked --workspace --no-deps
- name: Workspace policy
run: bash scripts/check-workspace-policy.sh
- name: Workspace policy mutation tests
run: bash scripts/test-workspace-policy.sh
- name: Session dependency and compiler boundary policy
run: |
bash scripts/check-session-policy.sh
bash scripts/test-session-policy.sh
# #1043: this gate's mutation tests, and those of the benchmark-validator and conformance
# gates below, run in gate-self-tests when a file they read changes; each gate runs here.
- name: Environment and marker vocabulary policy
run: bash scripts/check-env-vocabulary.sh
- name: Bench harness duplication policy and mutation tests
run: |
bash scripts/check-bench-policy.sh
bash scripts/test-bench-policy.sh
- name: test-support CI coverage policy and mutation tests
run: |
python3 -B scripts/check-test-support-ci.py
python3 -B scripts/test-test-support-ci.py
# #1059: the reader and matcher of the shipped-artifact scalar-oracle gate, hermetically.
# artifact-gates and audit-native run the gate itself on the built module and library.
- name: Scalar-oracle-absent gate self-test
run: python3 -B scripts/check-scalar-oracle-absent.py --self-test
# #1027: every file under scripts/ outside scripts/operator/ is reached from a workflow.
- name: Script reachability policy and mutation tests
run: |
python3 -B scripts/check-script-reachability.py
python3 -B scripts/test-script-reachability.py
- name: Console benchmark fixture integrity
run: bash scripts/check-console-benchmark-fixture.sh
# check-console-fixtures.sh and check-builtins-fixtures.sh (S1/S2): neither script has a
# hermetic no-binary mode -- with no binary argument, check-console-fixtures.sh's
# derive-intended-console-fixture.py falls back to `cargo run -q -p session-validator`
# (debug) and check-builtins-fixtures.sh unconditionally falls back to
# `cargo run --bin audit` whenever tools/audit exists on disk (it always does), so a bare
# call here is a hidden debug cargo build, not a manifest-only check. Both invocations moved
# entirely to audit-native, which already runs them a second time with the prebuilt release
# binaries; only the hermetic mutation-test suite stays here.
- name: Standing builtins fixture manifest mutation tests
run: bash scripts/test-builtins-fixtures.sh
- name: Benchmark preconditions
run: bash scripts/check-bench-preconditions.sh
- name: Shared host facade policy and mutation tests
run: |
bash scripts/check-host-core-policy.sh
bash scripts/test-host-core-policy.sh
- name: Protocol control typed-boundary policy and mutation tests
run: |
bash scripts/check-protocol-control-policy.sh
bash scripts/test-protocol-control-policy.sh
- name: Realtime audit and artifact evidence leak gates
run: |
bash scripts/check-realtime-audit-leak.sh
bash scripts/test-realtime-audit-leak.sh
bash scripts/check-artifact-evidence-leak.sh
bash scripts/test-artifact-evidence-leak.sh
- name: Realtime trace validator mutation tests
run: bash scripts/test-realtime-trace-validator.sh
- name: Lane numeric-boundary policy and mutation tests
run: |
bash scripts/check-lane-policy.sh
bash scripts/test-lane-policy.sh
- name: Unfused multiply-add seal and self-test
run: |
bash scripts/check-unfused-seal.sh
bash scripts/check-unfused-seal.sh --self-test
- name: Rack, builtins and graph dependency-boundary policy and mutation tests
run: |
bash scripts/check-rack-policy.sh
bash scripts/test-rack-policy.sh
bash scripts/check-builtins-policy.sh
bash scripts/test-builtins-policy.sh
bash scripts/check-graph-policy.sh
bash scripts/test-graph-policy.sh
- name: Effect runtime dependency-boundary and fixture policy and mutation tests
run: |
bash scripts/check-effect-runtime-policy.sh
bash scripts/test-effect-runtime-policy.sh
bash scripts/check-effect-runtime-fixtures.sh
bash scripts/test-effect-runtime-fixtures.sh
# check-effect-contract.sh has no hermetic no-binary mode either (S1): with no bench binary
# argument it falls back to `cargo run --locked --release -q -p bench`, a full release build
# inside the lint shard. audit-native runs the whole script (the factory-count grep and the
# bench conformance record) once, with the prebuilt release bench binary. The step above is
# the one run of the effect-runtime policy and fixture scripts; the contract script no longer
# repeats them (#1044).
- name: Conformance boundaries
run: bash scripts/check-conformance-boundaries.sh
# The rg half of the former parametric-eq target script (design §6.2/implementation issue
# 5): a hermetic render-contract seal with no cargo build.
- name: Parametric EQ render-contract seal
run: bash scripts/check-parametric-eq-render-contract.sh
- name: Release shape policy self-test
run: python3 -B scripts/check-release-shape.py --self-test
- name: npm publish mode reachability and trust gates
run: python3 -B scripts/test-npm-publish-modes.py
- name: Stem store V1 gate (mutation ledger moved to nightly)
run: node scripts/check-stem-store-v1.mjs
# The stem-identity corpus's only consumer in this repository (#1033): the release CLI copies
# these frozen bytes, and the Rust oracle that used to read them is gone (#1035).
- name: Stem identity corpus drift check
run: python3 -B fixtures/stem-identity/v1/generate.py --check
# x86-probes job folded in (design §6.2): three distinct sub-v3-refusal claims, dominated by
# toolchain install, which lint already paid for.
- name: Sub-v3 builds are refused by the lane guard (scalar)
run: |
unset CARGO_ENCODED_RUSTFLAGS; ! CARGO_TARGET_DIR=target/ci/x86-scalar RUSTFLAGS='-C target-cpu=x86-64 -C target-feature=-avx2,-fma' cargo check --locked -p engine -p target-smoke -p math 2>/tmp/ci-scalar-probe.log
grep -q 'requires x86-64-v3' /tmp/ci-scalar-probe.log
! rustc --print cfg -C target-cpu=x86-64 -C target-feature=-avx2,-fma | grep -qx 'target_feature="avx2"'
! rustc --print cfg -C target-cpu=x86-64 -C target-feature=-avx2,-fma | grep -qx 'target_feature="fma"'
- name: Sub-v3 builds are refused by the lane guard (AVX2 without FMA)
run: |
unset CARGO_ENCODED_RUSTFLAGS; ! CARGO_TARGET_DIR=target/ci/x86-avx2 RUSTFLAGS='-C target-cpu=x86-64 -C target-feature=+avx2,-fma' cargo check --locked -p engine -p target-smoke -p math 2>/tmp/ci-avx2-probe.log
grep -q 'requires x86-64-v3' /tmp/ci-avx2-probe.log
rustc --print cfg -C target-cpu=x86-64 -C target-feature=+avx2,-fma | grep -qx 'target_feature="avx2"'
! rustc --print cfg -C target-cpu=x86-64 -C target-feature=+avx2,-fma | grep -qx 'target_feature="fma"'
- name: AVX2 with FMA compile probe and cfg assertion
run: |
unset CARGO_ENCODED_RUSTFLAGS; CARGO_TARGET_DIR=target/ci/x86-avx2fma RUSTFLAGS='-C target-cpu=x86-64 -C target-feature=+avx2,+fma' cargo check --locked -p engine -p target-smoke -p math -p builtins -p builtins-compiler -p effect-runtime -p gate-expander -p multiband-compressor -p parametric-eq -p soft-clip
rustc --print cfg -C target-cpu=x86-64 -C target-feature=+avx2,+fma | grep -qx 'target_feature="avx2"'
rustc --print cfg -C target-cpu=x86-64 -C target-feature=+avx2,+fma | grep -qx 'target_feature="fma"'
test-debug-a:
name: workspace debug tests (minus DSP crates and evidence tools)
needs: route
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
# 1.5 times the slowest recent green run (14:41), #1728.
timeout-minutes: 22
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: test-debug-a
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain
run: rustup toolchain install 1.99.1 --profile minimal
- name: Compile builtins-compiler tests with default features
run: cargo test --locked -p builtins-compiler --no-run
# Every DSP crate moves to test-debug-b; audit/bench/console-workload/wasm-* move to
# audit-native and wasm-guests in release. Explicit --features (design §10 finding 1): a
# local reproduction must name the same features to compile the same program. Dev-dependency
# unification would turn on builtins-compiler and graph test-support anyway; nothing turns on
# host-web, host-core, effect-compiler or protocol test-support, whose gated tests never ran
# before #1021. scripts/check-test-support-ci.py (lint job) fails when a workspace
# test-support feature is left out of every whole-package cargo test step.
- name: Workspace debug tests (excluding DSP crates, audit, and wasm evidence tools)
run: |
cargo test --locked --workspace --all-targets \
--exclude lane --exclude math --exclude effect-runtime --exclude delay --exclude reverb \
--exclude compressor --exclude multiband-compressor --exclude gate-expander --exclude de-esser --exclude dynamic-eq \
--exclude true-peak-limiter --exclude transient-shaper --exclude soft-clip \
--exclude parametric-eq --exclude builtins --exclude dsp-reference \
--exclude conformance --exclude audit --exclude bench --exclude console-workload \
--exclude wasm-gates --exclude wasm-gate-guest \
--exclude wasm-gate-corpus \
--features builtins-compiler/test-support,graph/test-support,host-web/test-support,host-web/render-lock-probe,host-core/test-support,effect-compiler/test-support,protocol/test-support,control-plane/test-support,engine/test-support,engine/realtime-audit
# --all-targets runs no doctest, and most doctests here are compile_fail type-level
# guarantees, each beside a plain twin (#1422). Same packages and features as the step above.
- name: Workspace doctests (same packages and features as the debug tests)
run: |
cargo test --locked --workspace --doc \
--exclude lane --exclude math --exclude effect-runtime --exclude delay --exclude reverb \
--exclude compressor --exclude multiband-compressor --exclude gate-expander --exclude de-esser --exclude dynamic-eq \
--exclude true-peak-limiter --exclude transient-shaper --exclude soft-clip \
--exclude parametric-eq --exclude builtins --exclude dsp-reference \
--exclude conformance --exclude audit --exclude bench --exclude console-workload \
--exclude wasm-gates --exclude wasm-gate-guest \
--exclude wasm-gate-corpus \
--features builtins-compiler/test-support,graph/test-support,host-web/test-support,host-web/render-lock-probe,host-core/test-support,effect-compiler/test-support,protocol/test-support,control-plane/test-support,engine/test-support,engine/realtime-audit
test-debug-b:
name: DSP crates debug tests
needs: route
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
# #1972 cut the slowest tests: the test binaries run 33 % shorter on 35 % less CPU, about
# 10 minutes in the step at the slowest runner seen (it reached 15 before).
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: test-debug-b
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain
run: rustup toolchain install 1.99.1 --profile minimal
# Explicit --features, as in test-debug-a: a local reproduction names the same features.
# parametric-eq and builtins test-support turn on gated pass-count and peak-merge assertions
# inside tests that run either way; nothing in this package set forwards them (#1021).
# lane/test-support compiles the test-only `Backend::Scalar` oracle (#1059); the builtins
# and compressor dev-dependencies unify it in anyway, and naming it keeps that explicit.
- name: DSP crates debug tests (lane feature unification pinned explicitly)
run: |
cargo test --locked --all-targets \
-p lane -p math -p effect-runtime -p delay -p reverb -p compressor -p multiband-compressor \
-p gate-expander -p true-peak-limiter -p transient-shaper -p soft-clip -p de-esser -p dynamic-eq \
-p parametric-eq -p builtins -p dsp-reference -p conformance \
--features math/lane,parametric-eq/test-support,builtins/test-support,lane/test-support
# --all-targets runs no doctest (#1422). Same packages and features as the step above.
- name: DSP crates doctests (same packages and features as the debug tests)
run: |
cargo test --locked --doc \
-p lane -p math -p effect-runtime -p delay -p reverb -p compressor -p multiband-compressor \
-p gate-expander -p true-peak-limiter -p transient-shaper -p soft-clip -p de-esser -p dynamic-eq \
-p parametric-eq -p builtins -p dsp-reference -p conformance \
--features math/lane,parametric-eq/test-support,builtins/test-support,lane/test-support
- name: Conformance research-fixture completeness
run: cargo run --locked -p conformance --example conformance_fixtures -- --check
test-release:
name: release-mode lane, math, and wasm-gates digest gates
needs: route
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
# 1.5 times the slowest recent run that finished its steps (15:05, cancelled in the cache save), #1728.
timeout-minutes: 23
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: test-release
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain
run: rustup toolchain install 1.99.1 --profile minimal
# wasm-gates' G5 test `g5_native_digests_match_pins` is the one native owner of every
# cross-target digest corpus (#1048); wasm-guests leaves the script's native leg to it.
- name: Lane and math gates (G1-G4, G6, P1, M1-M3) and wasm-gates G5/G6 in release
run: cargo test --locked --release -p lane -p math -p wasm-gates --features math/lane
# #1407 gate 4: the release scale of the reachable-word sweep (every launch rate, quanta
# 1-63, 22 histories x 512 blocks). test-debug-b runs only its debug subset (48 kHz, quanta
# {1, 2, 7, 63}).
- name: Live input filter reachable-word sweep (filter_liveness) in release
run: cargo test --locked --release -p builtins --features builtins/test-support --test filter_liveness
# #1329 Amendment 4 (R6): the input section's tail and exact-rest gates at release scale
# (every launch rate; gate 2's real-kernel run at the domain extreme, which debug ignores).
# test-debug-b runs only the debug subset (48 kHz, a tenth of gate 1(b)'s scan).
- name: Input section tail and exact-rest gates (tail_contract) in release
run: cargo test --locked --release -p builtins --features builtins/test-support --test tail_contract
# #1375: the compressor's and the limiter's tail and exact-rest gates at release scale (every
# launch rate and both peak cases; debug runs 48 kHz and one peak case only). #1374: the
# delay's, whose real-kernel soundness gate (about 200M frames) runs in release only. #2114:
# the reverb's, whose real-kernel gate (about 110M frames) runs in release only.
- name: Compressor, limiter, delay and reverb tail and exact-rest gates (tail_contract) in release
run: cargo test --locked --release -p compressor -p true-peak-limiter -p gate-expander -p transient-shaper -p soft-clip -p delay -p reverb --test tail_contract
# #2114 test 1: the reverb's RT60 per octave band against the design prediction (about 30M
# frames and their band filters), ignored in debug.
- name: Reverb RT60 per band (rt60) in release
run: cargo test --locked --release -p reverb --test rt60
# #1379: the graph tail composition's real-kernel gates (C1, C2, C6 at every launch rate,
# C11 (b)), ignored in debug.
- name: Graph tail composition gates (host-core tail_composition) in release
run: cargo test --locked --release -p host-core --test tail_composition -- --include-ignored
# #1409 gate 1 at release scale (root ruling, 2026-10-06): effect-runtime's ramp-endpoint
# gate draws 100,000 random ramps in release (debug draws fewer), and the seven effect crates'
# ramp_endpoint targets run beside it in the shipping profile.
- name: Effect ramp endpoint gates (ramp_endpoint) in release
run: >-
cargo test --locked --release -p compressor -p delay -p gate-expander
-p multiband-compressor -p soft-clip -p transient-shaper -p true-peak-limiter
-p effect-runtime --test ramp_endpoint
# #1366 (root ruling, 2026-10-06): the exhaustive crossover designer sweep over every f32 in
# the domain; the per-PR debug test checks every 1024th f32 only.
- name: Multiband crossover designer exhaustive sweep (designer_total) in release
run: cargo test --locked --release -p multiband-compressor --test designer_total -- --ignored
# #1044: the "Lane and math gates ... in release" step runs M3 (`m3_determinism`) with FMA on,
# because `.cargo/config.toml` builds every x86-64 target with +avx2,+fma and `lane` refuses to
# compile without both. The retired "Math M3 digests on an FMA-enabled build" step replaced
# those flags with +fma alone, a build that ships nowhere. This prints the cfg cargo gives the
# lane-and-math step's `math` (no dependency is built: `math` has none without `lane`), and
# fails without FMA.
- name: M3's leg is built with FMA (cargo's cfg for math)
run: cargo rustc --locked --release -p math --lib -- --print cfg | grep -x 'target_feature="fma"'
# Own CARGO_TARGET_DIR per distinct RUSTFLAGS (design §8): a per-crate override must not
# invalidate the shared release target dir's fingerprints for every step after it.
# Issue #2067: cargo reads CARGO_ENCODED_RUSTFLAGS before RUSTFLAGS, so each loom leg unsets it
# first; an exported value would otherwise silently replace the leg's `--cfg loom` flags.
- name: Realtime race model (engine SPSC loom leg)
run: unset CARGO_ENCODED_RUSTFLAGS; CARGO_TARGET_DIR=target/ci/loom RUSTFLAGS='--cfg loom --check-cfg=cfg(loom)' cargo test --locked --release -p engine --lib spsc_loom
# #1702: the seqlock behind the applied-revision watermark and the seek report, on the same
# target dir and flags as the SPSC leg, so it reuses that leg's build.
- name: Realtime race model (engine seqlock loom leg)
run: unset CARGO_ENCODED_RUSTFLAGS; CARGO_TARGET_DIR=target/ci/loom RUSTFLAGS='--cfg loom --check-cfg=cfg(loom)' cargo test --locked --release -p engine --lib seqlock_loom
# Issue #1609. host-core depends on `lane`, which refuses to compile without the pinned
# x86-64-v3 features, and a RUSTFLAGS value replaces `.cargo/config.toml`'s rustflags, so this
# leg names them. The `-C target-feature=+avx2,+fma` below is a copy of `.cargo/config.toml`'s
# x86_64 rustflags: change the two together. Its own target dir: the flags differ from the
# engine leg's.
- name: Realtime race model (spectrum handoff loom leg)
run: unset CARGO_ENCODED_RUSTFLAGS; CARGO_TARGET_DIR=target/ci/loom-host-core RUSTFLAGS='--cfg loom --check-cfg=cfg(loom) -C target-feature=+avx2,+fma' cargo test --locked --release -p host-core --lib spectrum_handoff_loom
- name: Math M1 exhaustive sweep
if: needs.route.outputs.math_closure == 'true'
run: cargo test --locked --release -p math --features lane --test m1_exhaustive -- --ignored
- name: Fast dB tier gate F1, exhaustive over both operating domains
if: needs.route.outputs.math_closure == 'true'
run: cargo test --locked --release -p math --features lane --test f1_fast_db_bounds -- --ignored
audit-native:
name: release audit, trace, and fixture gates
needs: route
if: needs.route.outputs.route == 'full'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: audit-native
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install pinned Rust toolchain and strace
run: |
rustup toolchain install 1.99.1 --profile minimal
sudo apt-get update
sudo apt-get install -y ripgrep strace
# One release build backs every audit/trace/probe/fixture/capi/determinism step below,
# instead of each re-paying its own release compile (design §5: this is what moves 470s of
# tools/audit unit-test execution and ~330s of per-crate release re-runs out of the debug
# workspace run without losing their claims).
- name: Build release audit, bench, capi, session-validator, and realtime-policy binaries
run: cargo build --locked --release -p audit -p bench -p capi -p session-validator -p realtime-policy
- name: Realtime source policy (tools/realtime-policy)
run: ./target/release/realtime_policy
- name: Audit and console-workload unit tests in release
run: cargo test --locked --release -p audit -p bench -p console-workload
- name: Issue-544 C ABI runtime caller audit
run: |
set -o pipefail
set +e
./target/release/audit capi 2>target/issue544-capi-audit.stderr | tee target/issue544-capi-audit.json
pipeline_status=("${PIPESTATUS[@]}")
set -e
if (( pipeline_status[0] != 0 )); then exit "${pipeline_status[0]}"; fi
if (( pipeline_status[1] != 0 )); then exit "${pipeline_status[1]}"; fi
- name: Validate Issue-544 runtime audit records
run: |
python3 -B - target/issue544-capi-audit.json <<'PY'
import json
import re
import sys
def record(path):
text = open(path, "r", encoding="utf-8", newline="").read()
lines = text.splitlines()
if len(lines) != 1 or not lines[0].strip():
raise AssertionError(f"{path}: expected exactly one nonempty JSON record")
def pairs(items):
result = {}
for key, value in items:
if key in result:
raise AssertionError(f"{path}: duplicate key {key}")
result[key] = value
return result
value = json.loads(lines[0], object_pairs_hook=pairs)
if type(value) is not dict:
raise AssertionError(f"{path}: record is not an object")
return value
def exact_keys(obj, expected, path):
if set(obj) != set(expected):
raise AssertionError(f"{path}: keys differ: {sorted(obj)}")
def integer(value, field, path):
if type(value) is not int:
raise AssertionError(f"{path}: {field} is not an integer")
return value
(capi_path,) = sys.argv[1:]
capi = record(capi_path)
capi_expected = {
"schema_version", "kind", "calls", "sample_rate_hz", "quantum_frames",
"stable_output_address", "pcm_digest", "render_errors", "allocations",
"deallocations", "locks", "feature_detection", "logs", "file_io",
"network_io", "syscalls", "panic_unwinds", "total_violations",
}
exact_keys(capi, capi_expected, capi_path)
if type(capi["schema_version"]) is not int or capi["schema_version"] != 1:
raise AssertionError(f"{capi_path}: schema_version")
if capi["kind"] != "issue022_capi_render_audit":
raise AssertionError(f"{capi_path}: kind")
for field, expected in {
"calls": 100000, "sample_rate_hz": 48000, "quantum_frames": 128,
"render_errors": 0, "allocations": 0, "deallocations": 0, "locks": 0,
"feature_detection": 0, "logs": 0, "file_io": 0, "network_io": 0,
"syscalls": 0, "panic_unwinds": 0, "total_violations": 0,
}.items():
if integer(capi[field], field, capi_path) != expected:
raise AssertionError(f"{capi_path}: {field} != {expected}")
if type(capi["stable_output_address"]) is not bool or not capi["stable_output_address"]:
raise AssertionError(f"{capi_path}: stable_output_address")
if type(capi["pcm_digest"]) is not str or re.fullmatch(r"[0-9a-f]{16}", capi["pcm_digest"]) is None:
raise AssertionError(f"{capi_path}: pcm_digest")
PY
- name: Delay realtime allocation and syscall audit
run: ./target/release/audit delay --blocks 100000
- name: Compressor realtime allocation and syscall audit
run: ./target/release/audit compressor --blocks 100000
- name: Parametric EQ realtime allocation and syscall audit
run: ./target/release/audit parametric-eq --blocks 100000
- name: Gate/expander realtime audit (100,000 blocks, W8 bank and connected scalar)
run: |
./target/release/audit gate-expander | tee target/gate-expander-audit.json
grep -q '"total_violations":0' target/gate-expander-audit.json
- name: De-esser realtime audit (100,000 blocks, W8 bank and connected scalar)
run: |
./target/release/audit de-esser | tee target/de-esser-audit.json
grep -q '"total_violations":0' target/de-esser-audit.json
grep -q '"bank_available":true' target/de-esser-audit.json
- name: Dynamic EQ realtime audit (100,000 blocks, W8 bank and connected scalar)
run: |
./target/release/audit dynamic-eq | tee target/dynamic-eq-audit.json
grep -q '"total_violations":0' target/dynamic-eq-audit.json
grep -q '"bank_available":true' target/dynamic-eq-audit.json
- name: Builtins realtime audit (1,000,000 blocks, direct chain and graph)
run: |
bash scripts/trace-builtins-audit.sh target/release/audit
bash scripts/trace-builtins-graph-audit.sh target/release/audit
- name: Graph realtime audit
run: bash scripts/trace-graph-audit.sh target/release/audit
- name: Protocol caller-buffer allocation audit
run: bash scripts/run-protocol-allocation-audit.sh target/release/audit
- name: Realtime audit hooks and syscall trace
run: |
bash scripts/test-realtime-audit-probes.sh realtime target/release/audit
timeout 120s bash scripts/trace-realtime-audit.sh target/release/audit 1000000
- name: Builtins and builtins-graph audit probe mutation tests
run: |
bash scripts/test-realtime-audit-probes.sh builtins target/release/audit
bash scripts/test-realtime-audit-probes.sh builtins-graph target/release/audit
- name: Native effect one-million-call allocation and syscall audit
run: timeout 180s bash scripts/trace-effect-contract-audit.sh target/release/bench 1000000
# G1: compiles a C++17 header consumer and a C11 fixture at -Wall -Wextra -Werror
# -pedantic, links the real libcapi.so, diffs its exported symbols against the frozen ABI V1
# list, and executes the fixture. Builds capi in release itself (the shipped-crate link
# proof, design §6.7) through scripts/build-capi-release.sh, the fat-LTO cdylib and
# staticlib (#1499), which cargo builds apart from the release build above.
- name: C ABI linkage, frozen symbol set, native consumer smoke test, and self-test
run: |
bash scripts/check-capi-abi.sh
bash scripts/check-capi-abi.sh --self-test
# #1059: check-capi-abi.sh just built `-p capi` alone through build-capi-release.sh, which is
# the shipped feature set (the shard's four-package build unifies audit's graph/test-support
# in, so it is not). This step reads target/release/libcapi.so because cargo copies the
# script's host cdylib there. That library must contain no whole-plan scalar path.
- name: Shipped C ABI library contains no whole-plan scalar path
run: python3 -B scripts/check-scalar-oracle-absent.py --native target/release/libcapi.so
# G2: HashMap iteration order is per-process, so this runs `graph_fixture` fresh 100 times.
- name: Graph fresh-process determinism (100 fresh processes)
run: bash scripts/check-graph-determinism.sh
# Full audit half of the fixture checks (design §6.2 split): lint already ran the
# manifest-only half; this half exercises the release audit/session-validator binaries.
- name: Builtins fixture corpus audit
run: bash scripts/check-builtins-fixtures.sh . target/release/audit
- name: Standing console fixtures against the release session-validator
run: bash scripts/check-console-fixtures.sh target/release/session_validator
# check-effect-contract.sh's factory-count completeness grep and release conformance bench.
# The effect-runtime policy and fixture scripts it used to re-run run once, in lint (#1044).
- name: Native effect runtime conformance bench
run: bash scripts/check-effect-contract.sh target/release/bench