-
Notifications
You must be signed in to change notification settings - Fork 3
1035 lines (1014 loc) · 48.2 KB
/
Copy pathci.yml
File metadata and controls
1035 lines (1014 loc) · 48.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
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: CI
on:
push:
branches: [main]
pull_request:
# Manual re-run, matching every other workflow in this directory. The
# `coverage` job is the reason: Codecov compares a PR against the most
# recent *ancestor that has a report*, so a push to main that produces
# no run leaves every later PR silently baselined against an older
# commit — the diff table then attributes those intervening commits'
# coverage to whichever PR is open. That happened at d818e306, which
# carries zero check-runs, and left PR #1223 compared against a base
# eight commits behind. Without a dispatch trigger the only remedy is
# to land another commit on main.
workflow_dispatch:
# Default to read-only; jobs that need more escalate explicitly.
permissions:
contents: read
# Cancel in-progress runs for the same ref (e.g. PR pushes) but not on
# main or tag builds — those should always complete.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/') }}
# All `uses:` below are pinned to commit SHAs to eliminate supply-chain
# risk from action-release spoofing. SHAs sourced from the host-identity
# reference workflow for parity; bump via Dependabot.
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
RUSTUP_MAX_RETRIES: 10
RUSTFLAGS: "-D warnings"
RUSTDOCFLAGS: "-D warnings"
jobs:
# Path-filter detection. Outputs `py: true` when the change set
# touches the bindings crate, the workflow file itself, mise.toml
# (toolchain pins), or the root Cargo.toml / Cargo.lock (PyO3 /
# workspace dep bumps must re-run the python-test matrix even on a
# Rust-only PR title). On Rust-only PRs the python jobs below are
# `skipped`, which satisfies the `ci` aggregator's `needs:` without
# being `failure` or `cancelled`.
changes:
name: detect path changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
py: ${{ steps.filter.outputs.py }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4
id: filter
with:
filters: |
py:
- 'big-code-analysis-py/**'
- '.github/workflows/ci.yml'
- '.pre-commit-config.yaml'
- 'Makefile'
- 'mise.toml'
- 'Cargo.toml'
- 'Cargo.lock'
fmt:
name: rustfmt
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
name: clippy
runs-on: ubuntu-latest
timeout-minutes: 30
# security-events:write lets the upload-sarif step publish the clippy
# findings to GitHub code scanning (Security tab + PR annotations).
# actions:read is required by upload-sarif on private/internal repos
# (harmless on public ones) and matches codeql.yml's permission set.
# The workflow-level default is contents:read.
permissions:
contents: read
security-events: write
actions: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
# clippy-sarif converts cargo's JSON diagnostics into SARIF; sarif-fmt
# renders the same findings human-readably into the job log. Pinned to
# 0.8.0 for a reproducible install. taiki-e/install-action has no
# manifest entry for these crates, so they are compiled from source.
# Swatinem/rust-cache does not persist ~/.cargo/bin, so cache the built
# binaries explicitly (keyed on the version pin) to avoid recompiling
# them — sarif-fmt pulls in syntect — on every run.
- name: Cache clippy-sarif and sarif-fmt
id: cache-sarif-tools
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/clippy-sarif
~/.cargo/bin/sarif-fmt
key: sarif-tools-${{ runner.os }}-0.8.0
- name: Install clippy-sarif and sarif-fmt
if: steps.cache-sarif-tools.outputs.cache-hit != 'true'
run: |
cargo install clippy-sarif --version 0.8.0 --locked
cargo install sarif-fmt --version 0.8.0 --locked
# Capture clippy's JSON diagnostics to a file (not a pipe) so the SARIF
# is generated from a complete document — a hard compile-error that
# aborts clippy mid-stream cannot truncate a piped report.
# `|| clippy_status=$?` records clippy's real exit code without `set -e`
# aborting first; the final `exit` re-raises it so this step still
# fails the build on any warning, exactly as the old `-- -D warnings`
# invocation did. The SARIF file is written before that exit, so the
# upload step can still publish it via `if: always()`.
- name: Run clippy (SARIF)
run: |
set -euo pipefail
clippy_status=0
cargo clippy --workspace --all-targets --all-features \
--message-format=json -- -D warnings > clippy.json || clippy_status=$?
clippy-sarif < clippy.json > rust-clippy-results.sarif
sarif-fmt < rust-clippy-results.sarif
exit "$clippy_status"
# Fork PRs get a read-only GITHUB_TOKEN without security-events scope,
# so the upload would error; skip it there (and when no SARIF was
# produced, e.g. clippy never ran). The clippy gate above still fails
# the build on warnings regardless of this step.
- name: Upload SARIF to code scanning
if: always() && hashFiles('rust-clippy-results.sarif') != '' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
sarif_file: rust-clippy-results.sarif
category: clippy
docs:
name: rustdoc
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
# Delegate to the Makefile so the strict-doc gate lives in one
# place. `make doc-check` appends `-D warnings` to the workflow-
# level RUSTDOCFLAGS rather than replacing it.
- run: make doc-check
# Gate-effectiveness test: inject a known broken intra-doc link
# on a `pub` item and assert `make doc-check` FAILS. Mirrors the
# `enums-check` sabotage step further down — catches regressions
# where `-D warnings` is silently dropped from the recipe, the
# `_pc-doc-check` / `_ci-doc-check` wiring breaks, or rustdoc's
# broken-intra-doc-links lint gets demoted. The trap restores
# the file even if the assertion fails. See lesson 21 in
# docs/development/lessons_learned.md on dedicated-gate parity.
- name: doc-check gate-effectiveness test
run: |
set -eu
test_file=src/lib.rs
cp "$test_file" "$test_file.bak"
trap 'mv "$test_file.bak" "$test_file"' EXIT
# shellcheck disable=SC2016 # backticks inside the literal rustdoc payload are intentional
printf '\n/// Sabotage broken link: [`NonExistentDocCheckGateTarget`]\npub fn _doc_check_gate_sabotage() {}\n' >> "$test_file"
if make doc-check > /tmp/doc-sabotage.log 2>&1; then
echo "FAIL: doc-check did not reject the broken intra-doc link"
tail -40 /tmp/doc-sabotage.log
exit 1
fi
# Confirm the failure was caused by *this* sabotage (not some
# unrelated pre-existing rustdoc warning, a network glitch, or
# a cargo-side error). Without this check the gate could
# appear effective while silently skipping the injected
# payload.
if ! grep -q 'NonExistentDocCheckGateTarget' /tmp/doc-sabotage.log; then
echo "FAIL: doc-check exited non-zero but not because of the injected payload"
tail -40 /tmp/doc-sabotage.log
exit 1
fi
echo "PASS: doc-check correctly rejected the broken intra-doc link"
test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 45
# checks:write lets action-junit-report create the test-report check run;
# pull-requests:write lets it annotate failing tests on the PR. The
# workflow-level default is contents:read.
permissions:
contents: read
checks: write
pull-requests: write
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# Integration snapshots live in the big-code-analysis-output
# submodule under tests/repositories/; tests require it.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: recursive
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
with:
tool: cargo-nextest
# nextest runs the lib / bins / integration tests and writes the JUnit
# report to target/nextest/ci/junit.xml (see .config/nextest.toml). It
# does not run doctests — those are covered by the next step.
- name: cargo nextest run
run: cargo nextest run --profile ci --all-features --workspace --locked
# Doctests, matching the Makefile's `test` / `test-doc` split. nextest
# cannot run these, so dropping this step would silently stop
# exercising the workspace's documentation examples.
- name: cargo test --doc
run: cargo test --doc --all-features --workspace --locked
# Publish the JUnit XML as an artifact regardless of outcome — this
# works even on fork PRs, where the check-run API used below is not
# available.
- name: Upload JUnit report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: junit-${{ matrix.os }}
path: target/nextest/ci/junit.xml
if-no-files-found: warn
# Render the JUnit report as a check run + PR annotations + job summary.
# nextest already gates the job, so fail_on_failure stays off to avoid
# masking the real exit code under `if: always()`. Skipped on fork PRs,
# whose read-only token cannot create check runs.
- name: Publish test report
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: mikepenz/action-junit-report@a9170d5795813c01ab4901ffb045b52bab4ab09d # v6.5.0
with:
report_paths: target/nextest/ci/junit.xml
check_name: test report (${{ matrix.os }})
detailed_summary: true
include_passed: false
fail_on_failure: false
# The exact ancestor-chain assertion (#1122). `Ancestors::checked` keeps
# only an O(1) approximation on by default, because the exact
# `chain.last() == node.parent()` form costs Node::parent's O(depth) per
# node and makes every debug-build walk quadratic. This lane restores it
# so a walker whose truncate/push bookkeeping desynchronises still fails
# a build rather than silently feeding predicates a wrong ancestor.
#
# ubuntu-only and lib-scoped: the assertion lives in the library's walks,
# so the per-OS matrix and the CLI / web / integration tiers would re-pay
# the quadratic cost without covering anything the lib tests do not.
chain-audit:
name: chain audit
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: cargo test --lib with --cfg chain_audit
env:
# Replaces the workflow-level RUSTFLAGS rather than extending
# it — a step `env` key shadows the workflow one — so `-D
# warnings` is repeated here deliberately. Keep the two in
# step if the workflow-level value ever gains a flag.
RUSTFLAGS: "-D warnings --cfg chain_audit"
run: cargo test -p big-code-analysis --lib --all-features --locked
coverage:
name: coverage
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
# Coverage instruments and runs the full test suite, so it needs the
# big-code-analysis-output submodule exactly like the test job above.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: recursive
# llvm-tools-preview supplies llvm-profdata/llvm-cov for source-based
# coverage instrumentation.
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
components: llvm-tools-preview
# Coverage builds carry -C instrument-coverage, so their artifacts are
# incompatible with the regular build cache; a distinct key keeps the
# two from thrashing each other.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: coverage
- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
with:
tool: cargo-nextest,cargo-llvm-cov
# nextest is the same runner the `test` job uses, so instrumenting it
# keeps the coverage numbers aligned with what CI actually exercises.
# --no-report defers reporting so the same profile data can be emitted
# in two shapes below (Codecov upload + human-readable job summary).
#
# Doctests are intentionally excluded: `cargo llvm-cov --doc` requires
# nightly, and pairing nightly with the workflow-wide `-D warnings`
# would red-X CI on unrelated new nightly lints. The `test` job still
# runs doctests for correctness — only their coverage is omitted.
- name: Run tests with coverage instrumentation
run: cargo llvm-cov --no-report nextest --all-features --workspace --locked
# Codecov's native format preserves region coverage (lcov would drop to
# line-only).
- name: Generate Codecov report
run: cargo llvm-cov report --codecov --output-path codecov.json
# Per-file coverage table in the run's Summary tab, so reviewers can
# read coverage without leaving GitHub or waiting on the Codecov PR
# comment.
- name: Write coverage to job summary
run: |
{
echo '### Coverage'
echo ''
echo '```'
cargo llvm-cov report
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# token authenticates same-repo runs; fork PRs fall back to Codecov's
# tokenless upload for public repos (the secret resolves empty there).
# fail_ci_if_error stays off so a Codecov outage or a credential-less
# fork PR cannot red-X the merge gate — coverage is informational.
- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: codecov.json
flags: rust
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
msrv:
name: msrv (1.94)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # 1.94
with:
toolchain: "1.94"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
# Build only — MSRV is a compile-time promise; dev-deps may
# require a newer toolchain.
- run: cargo build --all-features --workspace --locked
feature-matrix:
name: features (${{ matrix.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: default (lib)
flags: -p big-code-analysis
- name: no-default-features (lib)
flags: --no-default-features -p big-code-analysis
# Per-language feature subset (issue #252). Locks in the
# "minimal grammar set" guarantee: dropping default features
# and re-enabling only the languages the caller cares about
# must still produce a working library. `rust` + `typescript`
# is the canonical subset because it exercises both a
# single-feature grammar (`tree-sitter-rust`) and a shared-
# crate grammar (`tree-sitter-typescript`, also used by the
# `Tsx` variant).
- name: minimal-langs (lib)
flags: --no-default-features --features rust,typescript -p big-code-analysis
- name: default (cli)
flags: -p big-code-analysis-cli
- name: no-default-features (cli)
flags: --no-default-features -p big-code-analysis-cli
- name: default (web)
flags: -p big-code-analysis-web
- name: no-default-features (web)
flags: --no-default-features -p big-code-analysis-web
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
# `cargo check --all-targets` exercises bins, tests, and examples,
# so a cfg-gated test file that only compiles under default features
# does not slip through the no-default-features matrix leg.
- run: cargo check --all-targets ${{ matrix.flags }} --locked
deny:
name: cargo-deny
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with:
command: check advisories bans licenses sources
license-audit:
name: license-audit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
with:
tool: cargo-about@0.8.4
# Fails if any dep in either binary crate's transitive closure
# resolves to a license not in `about.toml` `accepted`. Dependabot
# PRs that introduce an unapproved license now fail here instead of
# silently slipping into the release.
# Render to a file (rather than /dev/null) and sanity-check the
# output. A regression in about.hbs that yielded an empty render
# would otherwise pass silently — exit 0 from cargo-about does not
# imply non-empty output.
- name: Render attribution bundle for big-code-analysis-cli
run: |
cargo about generate --locked \
--manifest-path big-code-analysis-cli/Cargo.toml \
about.hbs > cli-thirdparty.md
grep -q '^## Crates' cli-thirdparty.md
test "$(wc -l < cli-thirdparty.md)" -gt 50
- name: Render attribution bundle for big-code-analysis-web
run: |
cargo about generate --locked \
--manifest-path big-code-analysis-web/Cargo.toml \
about.hbs > web-thirdparty.md
grep -q '^## Crates' web-thirdparty.md
test "$(wc -l < web-thirdparty.md)" -gt 50
lint:
name: lint (make lint)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
# `rustfmt` is here for the rustfmt-bail gate, which shells out to
# it directly. The toolchain action installs the minimal profile,
# and the gate skips cleanly when rustfmt is missing — so leaving
# it out would disable the check in CI without failing anything.
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
components: clippy, rustfmt
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.x"
# rumdl is the Markdown linter (replaced markdownlint-cli2). The
# prebuilt binary is not in taiki-e/install-action's manifest, so
# we fetch the upstream release tarball directly with a pinned
# SHA256, mirroring the actionlint and checkmake patterns below.
# Bump RUMDL_SHA256 in lockstep with `RUMDL_VERSION` (and with the
# `rumdl = …` pin in `mise.toml`). To regenerate the SHA256 after
# a version bump:
# curl -fsSL https://github.com/rvben/rumdl/releases/download/v${RUMDL_VERSION}/rumdl-v${RUMDL_VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha256
- name: Install rumdl
env:
RUMDL_VERSION: "0.2.2"
RUMDL_SHA256: "d38ad81c51221990d5e0204b4746f8c980a77de235aa875c2f30f02f9a19bb1a"
run: |
set -euo pipefail
url="https://github.com/rvben/rumdl/releases/download/v${RUMDL_VERSION}/rumdl-v${RUMDL_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL -o /tmp/rumdl.tgz "$url"
echo "${RUMDL_SHA256} /tmp/rumdl.tgz" | sha256sum -c -
tar -xzf /tmp/rumdl.tgz -C /tmp rumdl
install -m 0755 /tmp/rumdl /usr/local/bin/rumdl
rumdl --version
- uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0
with:
tool: taplo-cli@0.10.0,shellcheck@0.10.0,shfmt@3.12.0
# actionlint 1.7.12's prebuilt binary is not yet in
# taiki-e/install-action's manifest, and cargo-binstall has no
# fallback for it. Fetch the upstream release tarball directly,
# mirroring the checkmake step below. Bump SHA256 in lockstep
# with `ACTIONLINT_VERSION`.
- name: Install actionlint
env:
ACTIONLINT_VERSION: "1.7.12"
ACTIONLINT_SHA256: "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
run: |
set -euo pipefail
url="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -fsSL -o /tmp/actionlint.tgz "$url"
echo "${ACTIONLINT_SHA256} /tmp/actionlint.tgz" | sha256sum -c -
tar -xzf /tmp/actionlint.tgz -C /tmp actionlint
install -m 0755 /tmp/actionlint /usr/local/bin/actionlint
actionlint -version
# checkmake is not in taiki-e/install-action's manifest; fetch the
# pinned upstream release binary directly. The SHA256 below comes
# from the upstream `checksums.txt` published alongside the
# 0.2.2 release; bump it in lockstep with the version pin.
- name: Install checkmake
env:
CHECKMAKE_VERSION: "0.2.2"
CHECKMAKE_SHA256: "bedd033b06f2563809855ec2a9950c7a81acea6cd82937fd2f124e2c1c5fc3d5"
run: |
set -euo pipefail
url="https://github.com/mrtazz/checkmake/releases/download/${CHECKMAKE_VERSION}/checkmake-${CHECKMAKE_VERSION}.linux.amd64"
curl -fsSL -o /tmp/checkmake "$url"
echo "${CHECKMAKE_SHA256} /tmp/checkmake" | sha256sum -c -
install -m 0755 /tmp/checkmake /usr/local/bin/checkmake
checkmake --version
- name: make lint
run: make lint
# Defensive: `make lint` invokes the snapshot-anchors recipe, but
# `make lint`'s file-finding helpers depend on a glob fix tracked
# under #160; until that lands, run the snapshot-anchor gate
# explicitly so a regression in `make lint` cannot silently disable
# this check.
- name: snapshot anchors (explicit)
run: python3 utils/check-snapshot-anchors.py
# Run the gate's own unittests as their own explicit step, for the
# same reason as the rustfmt-bail twin below: a refactor that
# breaks the script must not be able to disable the gate silently.
- name: snapshot-anchors self-tests (explicit)
run: python3 -m unittest -q utils/check-snapshot-anchors-test.py
# Defensive twin for the rustfmt-bail gate (#1136): a comment
# inside a match pattern makes rustfmt emit the whole match
# verbatim while `cargo fmt --check` exits 0, so the fmt job
# cannot catch it. `make lint` already runs this gate; pinning it
# here keeps it enforced even if a future refactor drops it from
# the aggregate recipe.
- name: rustfmt-bail (explicit)
run: python3 utils/check-rustfmt-bail.py
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: rustfmt-bail self-tests (explicit)
run: python3 -m unittest -q utils/check-rustfmt-bail-test.py
# Defensive twin to the snapshot-anchors gate above: `make lint`
# already invokes check-versions, but pinning it here guarantees
# the lockstep version invariant is enforced even if a future
# refactor drops it from the aggregate recipe.
- name: check-versions (explicit)
run: python3 utils/check-versions.py
# Defensive twin for the man-page packaging gate (#446): a bca
# subcommand man page that drops out of the hand-maintained
# deb/rpm asset lists ships an artifact without its page (the
# #444 bug class). `make lint` already runs this gate; pinning
# it here guarantees enforcement even if a future refactor drops
# it from the aggregate recipe.
- name: check-manpage-assets (explicit)
run: python3 utils/check-manpage-assets.py
# Defensive twin for the man-page drift gate's self-tests
# (#1249). The gate itself runs in the `manpage` job, which
# invokes the script directly and so cannot be dropped by a
# refactor of the `make lint` aggregate — but the tests that
# prove the gate still catches an untracked page can be, and a
# gate that has silently stopped gating is indistinguishable
# from a clean tree. That is the failure this pair exists for.
- name: check-manpage-drift-test (explicit)
run: python3 -m unittest -q utils/check-manpage-drift-test.py
# Defensive twin for the diagnostic-prefix gate (#1199): a
# capitalised `Warning:` / `Error:` / `Note:` baked into a string
# literal bypasses the one-helper-per-crate severity ladder and
# reads as correct in review. Its self-tests run as their own
# step because a source-scanning gate that stops matching
# reports a clean tree.
- name: check-diagnostic-prefix (explicit)
run: python3 utils/check-diagnostic-prefix.py
- name: check-diagnostic-prefix self-tests (explicit)
run: python3 -m unittest -q utils/check-diagnostic-prefix-test.py
# Defensive twin for the safety-doc pin gate (#1057): the
# module doc of big-code-analysis-py/src/node.rs is the
# canonical soundness argument for this workspace's only
# sanctioned `unsafe` block, and it reasons about a named
# tree-sitter release. A bump that leaves the literal behind
# leaves an argument that reads as verified against a crate
# nobody compiles. Its self-tests run as their own step for the
# usual reason: a source-scanning gate that stops matching
# reports a clean tree.
- name: check-safety-doc-pin (explicit)
run: python3 utils/check-safety-doc-pin.py
- name: check-safety-doc-pin self-tests (explicit)
run: python3 -m unittest -q utils/check-safety-doc-pin-test.py
# Defensive twin for the grammar-marker-sync gate (#400): bumping
# the notification-only marker in tree-sitter-{mozjs,mozcpp}/
# Cargo.toml without re-running the matching generate-*.sh
# ships a marker that lies about the bundled grammar version.
# Invokes the script directly (matching the snapshot-anchors
# and check-versions defensive twins above) so a regression
# in the `make lint` aggregate cannot silently disable this
# check.
- name: grammar-marker-sync (explicit)
run: python3 utils/check-grammar-marker-sync.py
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: grammar-marker-sync self-tests (explicit)
run: python3 -m unittest -q utils/check-grammar-marker-sync-test.py
# Defensive twin for the workspace-exclusion gate (#1145, #1151):
# an excluded crate that roots no workspace of its own breaks
# `cargo fmt --all` inside every git worktree, and an unpinned
# tree-sitter dependency lets `cargo update` move a grammar
# silently. `make lint` already runs this gate; invoking it here
# (matching the twins above) keeps it enforced even if a future
# refactor drops it from the aggregate recipe.
- name: check-excluded-manifests (explicit)
run: python3 utils/check-excluded-manifests.py
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: check-excluded-manifests self-tests (explicit)
run: python3 -m unittest -q utils/check-excluded-manifests-test.py
# Defensive twin for the ruff-lockstep gate (#1230): the
# ruff-pre-commit `rev:`, `uv.lock`, and the hash-pinned
# `requirements/dev.txt` export CI installs from must name one
# version. They had already drifted once, silently, because a
# comment was the whole mechanism — so this gate in particular
# must not be able to disappear from the aggregate unnoticed.
- name: check-ruff-lockstep (explicit)
run: python3 utils/check-ruff-lockstep.py
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: check-ruff-lockstep self-tests (explicit)
run: python3 -m unittest -q utils/check-ruff-lockstep-test.py
# Defensive twin for the enums-codegen-drift gate (#405):
# running any grammar regen previously regenerated
# `src/c_langs_macros/*.rs` to a pre-optimization form
# silently. The gate diffs codegen output against
# checked-in files; running it explicitly here guards
# against `make lint` ever dropping it from the aggregate.
- name: enums-codegen-drift (explicit)
run: bash utils/check-enums-codegen-drift.sh
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: enums-codegen-drift self-tests (explicit)
run: python3 -m unittest -q utils/check-enums-codegen-drift-test.py
# Defensive: the prior `make lint` step already runs enums-check
# as part of its aggregate. Repeating it here protects against a
# regression to that aggregate (e.g., enums-check accidentally
# dropped from the `lint` recipe), which is precisely the failure
# mode #164 was filed against — `[workspace].exclude` had hidden
# the crate from every workspace-scoped lint until the gate
# existed. The redundant invocation is cheap (cargo cache is warm)
# and pins the gate at the workflow level.
- name: enums-crate check (explicit)
run: make enums-check
# Gate-effectiveness test: inject a known unused-variable warning
# into enums/src/main.rs and assert `make enums-check` FAILS.
# Catches regressions where RUSTFLAGS is silently dropped, the
# recipe stops failing on warnings, or `--all-targets` is
# removed. The trap restores the file even if the assertion
# fails. This step runs only in CI — locally, a developer who
# changes the gate should re-verify with the same maneuver.
- name: enums-check gate-effectiveness test
run: |
set -eu
test_file=enums/src/main.rs
cp "$test_file" "$test_file.bak"
trap 'mv "$test_file.bak" "$test_file"' EXIT
printf '\nfn _enums_check_gate_sabotage() { let unused_x = 1u32; }\n' >> "$test_file"
if make enums-check > /tmp/sabotage.log 2>&1; then
echo "FAIL: enums-check did not reject the injected warning"
tail -40 /tmp/sabotage.log
exit 1
fi
echo "PASS: enums-check correctly rejected the injected warning"
# Python bindings — lint + type-check. Single Linux job; the heavier
# cross-platform matrix lives in `python-test` below. Both jobs are
# gated by `needs.changes.outputs.py == 'true'` so Rust-only PRs skip
# them entirely (verified by the `ci` aggregator job's tolerance for
# `skipped` results).
python-lint:
name: python-lint
needs: changes
if: needs.changes.outputs.py == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
# The bindings tests use the local fixtures under
# big-code-analysis-py/tests/fixtures/, not the
# big-code-analysis-output submodule (that's the Rust integration
# snapshot store). Skip recursive submodules to avoid pulling
# ~hundreds of MB of unused fixtures and to avoid blocking on a
# missing/force-pushed submodule SHA.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# Share the cache key with the ubuntu-latest/py3.12 leg of
# the python-test matrix below — both jobs build the same
# bindings crate against the same toolchain on the same OS,
# so a populated target/ from one job warms the other.
shared-key: "python-bindings-ubuntu-latest-3.12"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: "pip"
# Create the venv inside big-code-analysis-py/ to match
# pyrightconfig.json's venvPath="." + venv=".venv" pair (relative
# to pyrightconfig.json's location). A repo-root venv would leave
# pyright unable to resolve the maturin-built _native module.
# requirements/dev.txt is the hash-pinned export of uv.lock's
# `dev` extra (regenerated by `make py-relock`), so this job
# installs the exact resolved set `make py-bootstrap` uses
# locally — no floor-vs-lockfile drift — and `--require-hashes`
# pins the supply chain (OpenSSF Scorecard Pinned-Dependencies).
# The ruff-pre-commit `rev:` in .pre-commit-config.yaml is held in
# lockstep with the ruff version resolved there by the
# `check-ruff-lockstep` gate in the `lint` job (#1230), so this is
# no longer something a reviewer has to remember.
- name: Create venv + install Python toolchain
working-directory: big-code-analysis-py
run: |
python -m venv .venv
source .venv/bin/activate
pip install --require-hashes -r requirements/dev.txt
# maturin develop must precede mypy / pyright so the _native
# extension is resolvable from the venv. Both type checkers
# treat the .pyi stub as authoritative, but they still need to
# import the package without ModuleNotFoundError. Use the
# default (debug) profile to match the local `make py-typecheck`
# / `make py-test` invocations — local-CI parity matters more
# than build speed at the lint job (debug is faster anyway).
- name: maturin develop
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
maturin develop
- name: ruff format --check
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
ruff format --check .
- name: ruff check
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
ruff check .
- name: mypy --strict
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
mypy --strict python tests examples
- name: pyright
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
pyright
# Python bindings — wheel build + pytest, Linux/macOS/Windows
# × Python 3.12/3.13. The cargo `test` job already builds the
# bindings crate's Rust side; this job is *additive*, verifying that
# the maturin-produced wheel actually loads under CPython on each
# platform (which cargo cannot do).
python-test:
name: python-test (${{ matrix.os }}, py${{ matrix.python }})
needs: changes
if: needs.changes.outputs.py == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.12", "3.13"]
steps:
# Submodules unnecessary — bindings tests use the local fixtures
# at big-code-analysis-py/tests/fixtures/. The
# big-code-analysis-output submodule is for Rust integration
# snapshots and would otherwise gate this matrix on its
# availability.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# Per-matrix-leg cache key so Linux / macOS / Windows do not
# trample each other's target/ directories. The
# ubuntu-latest+3.12 leg shares its key with python-lint so
# they reuse each other's build artifacts.
shared-key: "python-bindings-${{ matrix.os }}-${{ matrix.python }}"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python }}
cache: "pip"
# Cross-platform venv inside big-code-analysis-py/ (matches
# pyrightconfig.json + the python-lint job + the local Makefile
# convention). bash on Linux/macOS, PowerShell on Windows.
# requirements/dev.txt is the hash-pinned export of uv.lock's
# `dev` extra (regenerated by `make py-relock`): exact resolved
# versions with `--require-hashes` supply-chain pinning (OpenSSF
# Scorecard Pinned-Dependencies). It carries a few lint-only
# tools this job does not run (ruff / mypy / pyright) — the cost
# of installing them is preferred over maintaining a second,
# driftable pin set.
- name: Create venv + install deps (Unix)
if: runner.os != 'Windows'
working-directory: big-code-analysis-py
run: |
python -m venv .venv
.venv/bin/pip install --require-hashes -r requirements/dev.txt
- name: Create venv + install deps (Windows)
if: runner.os == 'Windows'
working-directory: big-code-analysis-py
shell: pwsh
run: |
python -m venv .venv
.venv\Scripts\python.exe -m pip install --require-hashes -r requirements/dev.txt
# maturin develop uses the default (debug) profile to match the
# local `make py-test` recipe — same .so layout end-to-end, so
# local-CI parity issues surface in CI rather than only on a
# contributor's machine.
- name: maturin develop (Unix)
if: runner.os != 'Windows'
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
maturin develop
- name: maturin develop (Windows)
if: runner.os == 'Windows'
working-directory: big-code-analysis-py
shell: pwsh
run: |
.venv\Scripts\Activate.ps1
maturin develop
- name: pytest (Unix)
if: runner.os != 'Windows'
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
python -m pytest --cov=big_code_analysis --cov-report=xml --cov-report=term-missing
- name: pytest (Windows)
if: runner.os == 'Windows'
working-directory: big-code-analysis-py
shell: pwsh
run: |
.venv\Scripts\Activate.ps1
python -m pytest
# Python-side coverage measures the pure-Python surface
# (python/big_code_analysis/_flatten.py + __init__.py); the Rust
# extension is measured by the `coverage` job. Collected on a single
# matrix leg because pure-Python coverage is identical across OS /
# interpreter — uploading from all six legs would only race parallel
# uploads to Codecov. flags:python keeps it distinct from the Rust
# upload; fail_ci_if_error stays off for the same reason as the Rust
# job (informational, fork-PR-safe).
- name: Upload coverage to Codecov
if: runner.os == 'Linux' && matrix.python == '3.12'
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: big-code-analysis-py/coverage.xml
flags: python
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
# Execute examples/jupyter_quickstart.ipynb end-to-end so a notebook
# cell that drifts away from the bindings API (renamed kwarg, removed
# function) breaks CI before it can rot in the documentation. The
# job stays single-OS / single-Python because the notebook exercises
# the public Python surface and pandas / matplotlib — both of which
# the `python-test` matrix already covers across OS / Python combos.
# A second matrix axis would only re-prove pandas works on Windows.
#
# Gated by `needs.changes.outputs.py == 'true'` same as the other
# python-* jobs; Rust-only PRs see `skipped` (not `failure`) so the
# aggregator's tolerance for skipped dependents continues to hold.
python-examples-nbconvert:
name: python-examples-nbconvert
needs: changes
if: needs.changes.outputs.py == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# Share with the python-lint / ubuntu-latest+3.12 python-test
# legs so the maturin develop step warms from a populated
# target/ rather than rebuilding from scratch.
shared-key: "python-bindings-ubuntu-latest-3.12"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: "pip"
- name: Create venv + install notebook + maturin deps
working-directory: big-code-analysis-py
run: |
python -m venv .venv
source .venv/bin/activate
# requirements/examples.txt is the hash-pinned export of
# uv.lock's [examples] extra (regenerated by `make
# py-relock`) — installing it directly (no `pip install -e
# .`) avoids a maturin PEP 517 build on top of the explicit
# `maturin develop` below, and `--require-hashes` pins the
# supply chain (OpenSSF Scorecard Pinned-Dependencies).
pip install --require-hashes -r requirements/examples.txt
- name: maturin develop
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
maturin develop
- name: Execute jupyter_quickstart.ipynb
working-directory: big-code-analysis-py
run: |
source .venv/bin/activate
# `--inplace` writes the executed outputs back to the file,
# but we deliberately do not commit them — CI just needs the
# exit code. Using `--to notebook` keeps the format stable;
# `--ExecutePreprocessor.timeout=120` caps a hung kernel.
# `MPLBACKEND=Agg` keeps matplotlib headless on the runner.
MPLBACKEND=Agg jupyter nbconvert \
--to notebook \
--execute \
--inplace \
--ExecutePreprocessor.timeout=120 \
examples/jupyter_quickstart.ipynb
manpage:
name: man pages up to date
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
submodules: false
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # stable tip
with:
toolchain: stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Regenerate man pages
run: cargo xtask
# Fails on every direction the regenerated tree can differ from
# the committed one — modified, deleted, *and* added pages. The
# added direction needs `git ls-files --others`: the page xtask
# writes for a brand-new subcommand is untracked, and `git diff`
# alone passed green over it until #1249. The check lives in the