-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathflake.nix
More file actions
3853 lines (3362 loc) · 172 KB
/
Copy pathflake.nix
File metadata and controls
3853 lines (3362 loc) · 172 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
{
description = "beady-eye - a bead graph annotated with live herdr pane activity";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
# Beads ships its own flake. Do not add inputs.nixpkgs.follows here — beads
# needs Go 1.26 and this flake's nixpkgs carries an older toolchain.
#
# Every check builds bd, so this pin reaches every shell. A bd on a schema
# other than the tracker's cannot read it: an older one refuses a newer
# schema, and 1.3.0 fails every issue read on a v53 server for want of its
# `leases` table. So the pin and the tracker's schema move together — treat
# it as a schema decision, not a version bump.
beads.url = "github:gastownhall/beads/v1.3.0";
# The dependency build is its own derivation here, and crane is what makes
# one. It declares no inputs of its own, so it costs a lock entry and
# nothing else.
crane.url = "github:ipetkov/crane";
};
outputs = { self, nixpkgs, flake-utils, beads, crane }:
let
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
# Every check is built from a source tree, and nix hashes the whole of
# the one it is handed. So a file in there that no check reads still
# gives every check a derivation nothing has built before, and the run
# rebuilds all of them for a verdict the tree already has: a pull request
# changing only README.md paid for the entire suite.
#
# An allowlist, for the reason Cargo.toml's `include` is one — a file
# that arrives later is out until somebody says so. Out is also the safe
# direction: a check that needed the file fails saying it is missing,
# where an exclude list that forgot it goes on quietly rebuilding.
sourceOf = paths: nixpkgs.lib.fileset.toSource {
root = ./.;
fileset = nixpkgs.lib.fileset.unions paths;
};
# What the compiler, the tests and the tree scans read, and nothing else.
# Every `include_str!` in the crate points inside tests/fixtures/, and
# the two tests that start from CARGO_MANIFEST_DIR walk into src/ and
# tests/shims/.
source = sourceOf [ ./Cargo.toml ./Cargo.lock ./src ./tests ];
# The crate names the version once. The release tag is cut from it and
# this output is compared against it before anything is published, so a
# crate on crates.io always has a flake output built from the same source
# at the same version.
common = {
pname = cargoToml.package.name;
version = cargoToml.package.version;
src = source;
};
# crane strips the crate's own code out of the dependency build but hands
# the manifests through, so the crate's real version would reach that
# build in the files and key the graph on the one thing a release changes.
versionForDeps = "0.0.0";
# Cargo states a package's name and version on consecutive lines, in the
# manifest and in the lock entry alike, and that pair is the only place
# either file names this crate's own version. So a manifest that stops
# spelling it that way throws: a pin that silently matches nothing reads
# exactly like one that worked.
pinnedManifest = file:
let
text = builtins.readFile file;
declaration = version: builtins.concatStringsSep "\n" [
''name = "${cargoToml.package.name}"''
''version = "${version}"''
];
pinned = builtins.replaceStrings
[ (declaration cargoToml.package.version) ]
[ (declaration versionForDeps) ]
text;
in
if pinned == text
then throw "${builtins.baseNameOf file} no longer states this crate's name and version on consecutive lines, so the dependency graph cannot be pinned off the version"
else pinned;
# The dependency graph, compiled on its own and keyed on Cargo.lock rather
# than on the source, so one compilation serves every later check — which
# is the whole point, because compiling it is most of what this project
# waits for. In CI the Actions cache is scoped per branch, so `main` seeds
# a graph every pull request restores, while a pull request seeds only
# itself.
#
# Naming the source tree in this derivation would put every later edit to
# it back into the dependency build, so it gets the manifests alone —
# which is what crane reduces a tree to anyway, plus dummies of the
# targets Cargo.toml declares.
#
# Two of them, because a check reuses artifacts only at the profile it was
# built at, and the checks are not all at one profile: the package builds
# and tests at release, while clippy, the dead-code pass and `cargo
# package`'s verify build all run at dev. Building both is what leaves
# every check's command exactly as it was.
artifactsFor = pkgs:
let
craneLib = crane.mkLib pkgs;
depsCommon = common // {
version = versionForDeps;
src = pkgs.runCommand "beady-eye-deps-source" { } ''
mkdir -p $out
cp ${pkgs.writeText "Cargo.toml" (pinnedManifest ./Cargo.toml)} $out/Cargo.toml
cp ${pkgs.writeText "Cargo.lock" (pinnedManifest ./Cargo.lock)} $out/Cargo.lock
'';
};
in
{
release = craneLib.buildDepsOnly depsCommon;
dev = craneLib.buildDepsOnly (depsCommon // {
pname = "${common.pname}-dev";
CARGO_PROFILE = "dev";
});
};
# The overlay and the per-system outputs are the same package, so a
# consumer taking either gets what CI built.
beadyEyeFor = pkgs: (crane.mkLib pkgs).buildPackage (common // {
cargoArtifacts = (artifactsFor pkgs).release;
# tests/no_config.rs runs the binary as a fresh machine would, and
# bdi asks bd where the tracker is. The worktree-listing test builds
# a repository and adds a worktree to it, so git has to be here too.
# Only the check phase needs either; nothing at runtime is built
# against them.
nativeCheckInputs = [
beads.packages.${pkgs.stdenv.hostPlatform.system}.bd
pkgs.git
];
# The package is named for the crate, the binary for the command.
meta.mainProgram = "bdi";
});
# Not eachDefaultSystem: nixpkgs unstable has dropped x86_64-darwin, so
# the flake cannot evaluate there. An Intel Mac takes the Release binary,
# which cargo builds without nix.
systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
in
flake-utils.lib.eachSystem systems (system:
let
pkgs = import nixpkgs { inherit system; };
beady-eye = beadyEyeFor pkgs;
artifacts = artifactsFor pkgs;
# What `cargo package` is handed. It builds the tarball Cargo.toml's
# `include` list selects, and refuses a `readme` it cannot find — but
# all it does with README.md and LICENSE is copy them, and no part of
# the check reads a word of either. So it gets a stand-in for both, and
# rewriting the README stops being a reason to build anything.
#
# `pathExists` rather than the paths themselves: naming a path is what
# would put the file's contents back into the derivation. Deleting one
# is still refused here rather than at publishing time.
publishedSource =
let
copied = [ "README.md" "LICENSE" ];
absent = builtins.filter (f: !builtins.pathExists (./. + "/${f}")) copied;
stoodInFor = pkgs.runCommand "published-source" { } ''
cp -r ${sourceOf [ ./Cargo.toml ./Cargo.lock ./src ]} "$out"
chmod -R u+w "$out"
for file in ${builtins.concatStringsSep " " copied}; do
echo "Stood in for; see publishedSource in flake.nix." > "$out/$file"
done
'';
in
nixpkgs.lib.throwIf (absent != [ ])
("Cargo.toml's include list names ${builtins.concatStringsSep " and " absent}, "
+ "which cargo package needs and this tree has not got.")
stoodInFor;
# Both properties above are silent when they break. Put a documentation
# file back into either source and every check still passes, on the
# same command, with the same output — only having built what it was
# handed already built. Nothing in a green run says which of the two
# happened, so they have to be stated somewhere they can fail.
documentationIsNotSource =
pkgs.runCommand "documentation-is-not-source" { } ''
carried="$(find ${source} \( -name '*.md' -o -name docs \) )"
if [ -n "$carried" ]; then
echo "The source the checks are built from carries documentation:"
printf '%s\n' "$carried"
echo
echo "Every check hashes the whole of that tree, so a change to any"
echo "of these rebuilds all of them for a verdict the tree already"
echo "has. Take it out of \`source\` in flake.nix — the file stays in"
echo "the repository, it just stops being something a check reads."
exit 1
fi
if ! grep -q 'Stood in for' ${publishedSource}/README.md; then
echo "The package check has been handed the repository's own README.md."
echo
echo "Its contents then decide that check's derivation, so every"
echo "rewrite of it builds the crate again to learn what the last"
echo "one already proved. See publishedSource in flake.nix."
exit 1
fi
touch $out
'';
# Starts bdi and puts it back whenever the source changes, so a copy
# left running in a terminal keeps up with what the other seats land.
#
# `--wrap-process=none` and the `exec` are load-bearing together.
# watchexec's default runs the command in a process group of its own,
# which is a background group on the terminal, and entering raw mode
# from a background group raises SIGTTOU: bdi stops before it draws
# anything. Sharing watchexec's own group lifts that — but then the
# signal that restarts goes to the process watchexec spawned, so that
# process has to be bdi itself rather than a `cargo run` holding it as
# a child it would not pass the signal on to.
#
# It watches the whole of src/, so a test-only edit restarts the view
# too. The tests sit in `#[cfg(test)]` modules inside the files they
# cover, and a filesystem event says which file changed, never which
# part of it, so nothing can separate them. A restart nobody asked for
# costs a redraw; a rebuild that never happens leaves the view showing
# yesterday's build, which is what this exists to prevent. tests/ is
# left out: an edit there cannot change what the running binary does.
rerunBdiOnChange = pkgs.writeShellScriptBin "rerun-bdi-on-change" ''
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)" || exit 1
exec ${pkgs.watchexec}/bin/watchexec \
--watch src --watch Cargo.toml --restart --wrap-process=none \
-- 'cargo build --release --quiet && exec target/release/bdi'
'';
# `nix flake check` reads the git index, so an untracked file is not in
# the source it checks and a green result has not compiled it. Refusing
# the tree the check cannot see all of is what puts that out of reach.
checkBeforePush = pkgs.writeShellScriptBin "check-before-push" ''
set -u
git=${pkgs.git}/bin/git
cd "$($git rev-parse --show-toplevel)" || exit 1
dirty="$($git status --porcelain)"
if [ -n "$dirty" ]; then
echo "check-before-push: this tree is dirty, so a check of it would not"
echo "be a check of what you are about to push."
echo
printf '%s\n' "$dirty"
untracked="$($git ls-files --others --exclude-standard)"
if [ -n "$untracked" ]; then
echo
echo "These are untracked, so the check would not see them at all:"
printf '%s\n' "$untracked"
fi
echo
echo "Commit, then run this again."
exit 1
fi
exec nix flake check -L "$@"
'';
# GitHub records `cancelled` for a job its own `timeout-minutes` killed,
# and there is nothing else to read it off: the run, the job and the log
# all say exactly what they say for a run somebody cancelled by hand.
# The check-run annotation is the only place the difference is written
# down, so this is what tells the two apart. It prints the limit and
# exits non-zero when the annotations name none.
limitTheJobExceeded = ''
limit_the_job_exceeded() {
${pkgs.gnugrep}/bin/grep -m1 'exceeded the maximum execution time'
}
'';
# The sentence above is GitHub's, not ours, so nothing in this tree
# fails when it changes. This is what says the pattern still matches the
# one that was measured, against both annotation sets from the push that
# measured it.
limitTheJobExceededTest = pkgs.runCommand "limit-the-job-exceeded-test" { } ''
set -u
${limitTheJobExceeded}
timed_out='The job has exceeded the maximum execution time of 1m0s
The operation was canceled.'
by_hand='The run was canceled by @GraemeF.
The operation was canceled.'
found="$(printf '%s\n' "$timed_out" | limit_the_job_exceeded)" || found=""
case "$found" in
*'maximum execution time of 1m0s'*) ;;
*)
echo "FAIL: the timeout annotation was not recognised, or its limit"
echo "was not what came back. Got: '$found'"
exit 1
;;
esac
if printf '%s\n' "$by_hand" | limit_the_job_exceeded; then
echo "FAIL: a run cancelled by hand was read as one that timed out."
exit 1
fi
touch $out
'';
# ci.yml's `on.pull_request` comment names the cost: a push and a body
# edit landing in the same second put two runs of the same workflow
# into one concurrency group, and the group cancels the older of the
# two. `createdAt` only carries second resolution, so the two runs
# tie on it, and `max_by` breaks a tie by array position rather than
# by which run gh happened to create first — gh's own ordering is not
# documented, so which one that leaves in front is not reliable
# either. A run's `databaseId` is assigned in creation order and
# never ties, so it is what breaks the tie: the later-created run of
# a tied pair is the one still standing, cancelled or not. Recency
# still decides everything else, so an old success several runs back
# is never preferred over a genuinely later cancellation — only ties
# on `createdAt` itself reach the second key.
pickRunPerWorkflow = ''
pick_run_per_workflow() {
$jq -c 'group_by(.workflowName) | map(max_by([.createdAt, .databaseId]))'
}
'';
# `max_by(.createdAt)` alone breaks a tie by array position, and
# nothing here controls what order gh hands the tied pair back in —
# so a fixture has to cover both orderings to show the databaseId
# tiebreak is what decides it, not a fixture that happens to agree
# with gh's usual order. This is bdi-7ao.142.12's own shape, invented
# here rather than captured, per this repo's rule that only invented
# ground goes in the tree.
pickRunPerWorkflowTest = pkgs.runCommand "pick-run-per-workflow-test" { } ''
set -u
jq=${pkgs.jq}/bin/jq
${pickRunPerWorkflow}
fail() { echo "FAIL: $1"; echo "$got"; exit 1; }
run() {
printf '{"workflowName":"%s","conclusion":"%s","createdAt":"%s","databaseId":%s}' \
"$1" "$2" "$3" "$4"
}
# Tied createdAt, cancelled run listed first — the array order a
# naive tiebreak would already get right, so this alone would not
# catch a databaseId regression.
got="$(printf '[%s,%s]' \
"$(run CI cancelled 2026-09-10T10:00:00Z 1)" \
"$(run CI in_progress 2026-09-10T10:00:00Z 2)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq '.[0].databaseId')" = 2 ] ||
fail "did not prefer the live sibling when it was listed second:"
# The same tie, cancelled run listed last instead — the ordering
# that trips a bare array-position tiebreak, since the cancelled
# run now sits where a naive pick would take it.
got="$(printf '[%s,%s]' \
"$(run CI in_progress 2026-09-10T10:00:00Z 2)" \
"$(run CI cancelled 2026-09-10T10:00:00Z 1)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq '.[0].databaseId')" = 2 ] ||
fail "did not prefer the live sibling when it was listed first:"
# The same shape, but the sibling has already concluded rather
# than still running.
got="$(printf '[%s,%s]' \
"$(run CI success 2026-09-10T10:00:00Z 2)" \
"$(run CI cancelled 2026-09-10T10:00:00Z 1)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq '.[0].databaseId')" = 2 ] ||
fail "did not prefer the finished sibling over the tied cancelled run:"
# An old success is not a live sibling: a genuinely later
# cancellation — no tie, no sibling, just a fresh run somebody
# killed — still reads as cancelled rather than as the stale pass.
got="$(printf '[%s,%s]' \
"$(run CI success 2026-01-01T10:00:00Z 1)" \
"$(run CI cancelled 2026-09-10T10:00:00Z 2)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq -r '.[0].conclusion')" = cancelled ] ||
fail "preferred a stale old success over a genuinely later cancellation:"
# Every run in the group was cancelled, so there is no sibling to
# prefer, and this still answers with one of them rather than
# losing the group entirely.
got="$(printf '[%s]' "$(run CI cancelled 2026-09-10T10:00:00Z 1)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq '.[0].databaseId')" = 1 ] ||
fail "lost the only run when every run in the group was cancelled:"
# A second workflow's own single run on the same sha stays its own
# pick, unaffected by the other workflow's tied cancel/live group.
got="$(printf '[%s,%s,%s]' \
"$(run CI cancelled 2026-09-10T10:00:00Z 1)" \
"$(run CI success 2026-09-10T10:00:00Z 2)" \
"$(run Release success 2026-09-10T10:00:00Z 3)" \
| pick_run_per_workflow)"
[ "$(printf '%s' "$got" | $jq 'length')" = 2 ] ||
fail "did not keep the two workflows' picks separate:"
touch $out
'';
# `gh run list` answers with an empty list for four different reasons
# and only one of them means "wait", so this has to tell them apart —
# `read-ci-verdict --help` says how.
#
# The fetch is load-bearing. Ancestry measured against a stale
# `origin/main` calls a superseded commit the tip and sends you back to
# wait for a run that will never exist, which is the wrong answer this
# command exists to prevent.
readCiVerdict = pkgs.writeShellScriptBin "read-ci-verdict" ''
set -u
git=${pkgs.git}/bin/git
gh=${pkgs.gh}/bin/gh
jq=${pkgs.jq}/bin/jq
grep=${pkgs.gnugrep}/bin/grep
${limitTheJobExceeded}
${pickRunPerWorkflow}
case "''${1:-}" in
-h|--help)
cat <<'USAGE'
read-ci-verdict [<commit-ish>] (default: HEAD)
Says whether CI passed for one commit, and exits 0 only for a run
whose own conclusion is "success".
GitHub makes one run per push, on the tip, so an empty run list is not
a verdict and does not always mean wait:
no run coming CI runs on pushes to main and on pull requests
against it. A commit that is on neither has no
run and will never get one.
conflicted A pull request is run on a merge of its head
and its base, so one GitHub cannot merge gets
no run until somebody resolves it.
not the tip The verdict belongs to a descendant. This reads
that run instead and says whose it is, and a
green one that contains your commit exits 0:
a run tests a tree rather than a commit, no
run will ever test yours alone, and this is
the strongest true claim available. It may be
green because of what landed after you.
not started yet Wait. This one resolves on its own.
A cancelled run is neither green nor red: the commit has no verdict.
On a pull request it is the branch moving: a push cancels the run on
the head it replaced, and the verdict lives on the new head, which
this names.
A run cancelled with its head still in place is one of two things,
and GitHub records them identically — same run conclusion, same job
conclusion, and a log ending "The operation was canceled." either
way. Only the check-run annotation says which, so this reads that:
ran out of time ci.yml caps the job at timeout-minutes, and a
job that reaches the cap is killed and recorded
cancelled. Starting it again reproduces
whatever hung and spends the cap over again.
cancelled by hand Nothing else did it, so it wants starting
again.
USAGE
exit 0
;;
esac
cd "$($git rev-parse --show-toplevel)" || exit 1
ref="''${1:-HEAD}"
sha="$($git rev-parse --verify --quiet "$ref^{commit}")" || {
echo "read-ci-verdict: no such commit in this repository: $ref" >&2
exit 1
}
runs_for() {
$gh run list --commit "$1" --limit 50 \
--json databaseId,headSha,headBranch,status,conclusion,workflowName,url,createdAt
}
runs="$(runs_for "$sha")" || exit 1
subject="$sha"
if [ "$(printf '%s' "$runs" | $jq 'length')" -eq 0 ]; then
$git fetch --quiet origin ||
echo "read-ci-verdict: could not reach origin, so what follows may be stale" >&2
# Silence is only readable against a known trigger, and this reads
# it against one. Say so rather than answer for a workflow this no
# longer describes.
workflow="$($git show "$sha:.github/workflows/ci.yml" 2>/dev/null)"
if ! printf '%s\n' "$workflow" | $grep -q 'branches: \[main\]' ||
printf '%s\n' "$workflow" | $grep -qE 'paths-ignore|^ *paths:'; then
echo "NO VERDICT — $sha has no run, and this cannot say why."
echo "ci.yml no longer triggers on a bare push to main, so silence can"
echo "now mean a filtered path too. Teach this command the new trigger."
exit 1
fi
if ! $git merge-base --is-ancestor "$sha" origin/main 2>/dev/null; then
pr="$($gh pr list --state open --limit 100 \
--json headRefOid,url,mergeable |
$jq -r --arg sha "$sha" \
'first(.[] | select(.headRefOid == $sha)) |
"\(.mergeable) \(.url)"')"
if [ -n "$pr" ]; then
url="''${pr#* }"
# GitHub builds a pull request's run on a merge of the head and
# the base, so a conflict leaves it with nothing to run and the
# wait never ends on its own. UNKNOWN is a mergeability it has
# not computed yet, which does.
if [ "''${pr%% *}" = CONFLICTING ]; then
echo "NO RUN UNTIL YOU RESOLVE IT — $sha heads a pull request that"
echo "conflicts with its base, and GitHub runs nothing it cannot merge."
echo " $url"
echo "Merge origin/main, resolve, and push. The run follows the push."
exit 1
fi
echo "NOT STARTED YET — $sha heads an open pull request and has no run."
echo " $url"
echo "This one resolves on its own. Ask again."
exit 1
fi
echo "NO RUN, AND NONE IS COMING — $sha is neither on origin/main nor"
echo "the head of an open pull request, and CI runs on those two things"
echo "only. Open a pull request and a run appears."
exit 1
fi
tip="$($git rev-parse origin/main)"
if [ "$tip" = "$sha" ]; then
echo "NOT STARTED YET — $sha is the tip of origin/main and has no run."
echo "This one resolves on its own. Ask again."
exit 1
fi
echo "No run for $sha, and there never will be:"
echo "it is on origin/main but not the tip, and GitHub makes one run per"
echo "push. Its verdict lives on the descendant that contains it:"
echo " $tip"
echo
subject="$tip"
runs="$(runs_for "$tip")" || exit 1
if [ "$(printf '%s' "$runs" | $jq 'length')" -eq 0 ]; then
echo "NO VERDICT — $tip has no run either. Ask again once it does."
exit 1
fi
fi
stray="$(printf '%s' "$runs" |
$jq --arg sha "$subject" '[.[] | select(.headSha != $sha)] | length')"
if [ "$stray" != "0" ]; then
echo "read-ci-verdict: gh returned a run for another commit; refusing to guess." >&2
exit 1
fi
latest="$(printf '%s' "$runs" | pick_run_per_workflow)"
printf '%s' "$latest" |
$jq -r '.[] | " \(.workflowName): \(.status)/\(if .conclusion == null or .conclusion == "" then "-" else .conclusion end) \(.url)"'
echo
verdict="$(printf '%s' "$latest" | $jq -r '
if any(.[]; .status != "completed") then "running"
elif any(.[]; .conclusion == "failure" or .conclusion == "timed_out"
or .conclusion == "startup_failure"
or .conclusion == "action_required") then "failed"
elif any(.[]; .conclusion == "cancelled") then "cancelled"
elif all(.[]; .conclusion == "skipped") then "skipped"
elif all(.[]; .conclusion == "success") then "passed"
else "unclear" end')"
case "$verdict" in
passed)
echo "PASSED — $subject"
;;
failed)
echo "FAILED — $subject"
exit 1
;;
cancelled)
branch="$(printf '%s' "$latest" |
$jq -r 'first(.[] | select(.conclusion == "cancelled")) | .headBranch')"
moved="$($gh api "repos/{owner}/{repo}/commits/$subject/pulls" |
$jq -r --arg sha "$subject" --arg branch "$branch" \
'first(.[] | select(.head.ref == $branch and .head.sha != $sha)) |
"\(.head.sha) \(.html_url)"')"
if [ -n "$moved" ]; then
echo "NO VERDICT — the run for $subject was cancelled because $branch"
echo "moved on. Its verdict lives on the head that replaced it:"
echo " ''${moved%% *}"
echo " ''${moved#* }"
exit 1
fi
# Nobody cancelled a run its own timeout killed, so "start it
# again" is the one instruction that cannot help: the rerun hangs
# the same way and spends the timeout over again.
#
# Every cancelled job of every cancelled run is asked, because one
# timed-out job among several cancelled ones is still a commit
# whose rerun will hang, and asking only the first reports the
# timeout or misses it depending on which order gh answered in.
#
# A gh that fails answers with nothing, which is what a run
# cancelled by hand also answers with, so a question that could
# not be asked would come out as "start it again". Each call says
# so instead.
github_would_not_say() {
echo "NO VERDICT — a run for $subject was cancelled, and GitHub would"
echo "not say whether its own timeout did it. That reads exactly like a"
echo "cancellation by hand, so this is not telling you to start it again."
echo "Ask again."
exit 1
}
limit=""
for run in $(printf '%s' "$latest" |
$jq -r '.[] | select(.conclusion == "cancelled") | .databaseId'); do
checks="$($gh api "repos/{owner}/{repo}/actions/runs/$run/jobs" \
--jq '.jobs[] | select(.conclusion == "cancelled") | .check_run_url')" ||
github_would_not_say
for check in $checks; do
messages="$($gh api "$check/annotations" --jq '.[].message')" ||
github_would_not_say
if [ -z "$limit" ]; then
limit="$(printf '%s\n' "$messages" | limit_the_job_exceeded)" || limit=""
fi
done
done
if [ -n "$limit" ]; then
echo "NO VERDICT — a run for $subject ran itself out of time:"
echo " $limit"
echo "Nothing cancelled it and starting it again reproduces whatever"
echo "hung. The log names it: libtest prints \"has been running for"
echo "over 60 seconds\" for a test that never returned, and the job"
echo "log says only that the operation was canceled."
exit 1
fi
echo "NO VERDICT — a run for $subject was cancelled with $branch still"
echo "heading there, so nothing superseded it. Start it again."
exit 1
;;
skipped)
echo "NO VERDICT — every run for $subject was skipped, so nothing was checked."
exit 1
;;
running)
echo "STILL RUNNING — $subject has no conclusion yet."
exit 1
;;
*)
echo "NO VERDICT — $subject's runs concluded in a way this command does"
echo "not recognise. Read them above."
exit 1
;;
esac
'';
# `cargo-mutants --in-diff` exits 0 on a diff it cannot score, and that
# is the same 0 a run which caught every mutant exits. Two runs reach
# it, both measured at 27.1.0: a diff holding no mutable production
# line prints "No mutants to filter" and writes no mutants.out at all,
# and a diff whose every mutant is unviable prints "No mutants were
# viable" and writes one whose every scored row is zero. Neither tested
# the change. This is the count that tells them from a run that did,
# and it refuses them, so the reader is not the guard.
#
# It reads the run's own output directory rather than the repository's,
# because a vacuous run leaves an earlier mutants.out exactly where it
# stood — same inode, same counts, and no mutants.out.old beside it —
# so the tree's copy answers for whichever run last wrote one.
refuseARunThatScoredNothing = ''
refuse_a_run_that_scored_nothing() {
jq=${pkgs.jq}/bin/jq
outcomes="$1/mutants.out/outcomes.json"
if [ ! -f "$outcomes" ]; then
echo "Nothing was scored: the run wrote no $outcomes."
echo "cargo-mutants writes none when the diff holds no mutable"
echo "production line, so this says nothing about the change."
exit 1
fi
scored="$($jq '.caught + .missed + .timeout' "$outcomes")"
if [ "$scored" = 0 ]; then
echo "Nothing was scored: $($jq -r '"\(.total_mutants) generated, \(.unviable) unviable"' "$outcomes")."
echo "An unviable mutant does not compile, so it tests nothing,"
echo "and this says nothing about the change."
exit 1
fi
echo "Scored $scored mutants."
}
'';
# Both runs above pass anybody reading the exit code, so this guard is
# worth having only if it fires. These are those two runs, reduced to
# the tally the count is read off. The third row is the control: a
# guard that refused everything would pass the first two, and nothing
# in them could tell it apart from one that works.
refuseARunThatScoredNothingTest =
pkgs.runCommand "refuse-a-run-that-scored-nothing-test" { } ''
set -u
${refuseARunThatScoredNothing}
fail() { echo "FAIL: $1"; echo "$output"; exit 1; }
tally() {
mkdir -p "$1/mutants.out"
printf '%s\n' "$2" > "$1/mutants.out/outcomes.json"
}
# A diff with no mutable production line. cargo-mutants writes
# nothing, so the directory it was pointed at stays empty.
mkdir -p "$TMPDIR/nothing"
output="$( refuse_a_run_that_scored_nothing "$TMPDIR/nothing" 2>&1 )" &&
fail "it accepted a run that wrote no tally at all:"
case "$output" in
*"wrote no"*) ;;
*) fail "the refusal did not say the run wrote no tally:" ;;
esac
# A diff whose every mutant is unviable. Here there is a tally, and
# every row of it that means a mutant was tested is zero.
tally "$TMPDIR/unviable" \
'{"total_mutants":1,"caught":0,"missed":0,"timeout":0,"unviable":1}'
output="$( refuse_a_run_that_scored_nothing "$TMPDIR/unviable" 2>&1 )" &&
fail "it accepted a run whose every mutant was unviable:"
case "$output" in
*"1 generated, 1 unviable"*) ;;
*) fail "the refusal did not say what the run generated:" ;;
esac
# A run that tested the change.
tally "$TMPDIR/scored" \
'{"total_mutants":5,"caught":4,"missed":1,"timeout":0,"unviable":0}'
output="$( refuse_a_run_that_scored_nothing "$TMPDIR/scored" 2>&1 )" ||
fail "it refused a run that scored five mutants:"
case "$output" in
*"Scored 5 mutants"*) ;;
*) fail "it accepted the run without saying what it scored:" ;;
esac
touch $out
'';
# cargo-mutants copies the working tree and tests that, so the filter
# has to describe the working tree too. Taken from HEAD it would name
# committed lines while the uncommitted edits beside them went
# unmutated, and the run would report a tally for a revision nobody
# was testing.
#
# The merge base rather than the ref itself, which is what `...` means
# and is the whole of why that form is written everywhere here: a diff
# against the ref carries in reverse whatever landed on it while you
# worked, and cargo-mutants scores those lines on your tree as your
# tally.
scopeToTheChange = ''
scope_to_the_change() {
git=${pkgs.git}/bin/git
# An untracked file that is not ignored is copied into the tree
# cargo-mutants tests, and `git diff` cannot see one at all — so a
# new module's lines would be in the source under test and outside
# the filter, and nothing would mutate any of them. That is a full
# tally saying nothing about the file the change is about.
loose="$($git ls-files --others --exclude-standard -- '*.rs')"
if [ -n "$loose" ]; then
echo "These Rust files are untracked, so they are in the tree"
echo "cargo-mutants tests and out of the diff that scopes it:"
printf '%s\n' "$loose"
echo
echo "Stage them (git add -N is enough) and run this again."
exit 1
fi
base="$($git merge-base "$1" HEAD)" || exit 1
echo "Scoping to the change since $($git rev-parse --short "$base"), the merge base with $1."
$git diff "$base" > "$2" || exit 1
}
'';
# Both properties above are silent when they break: a filter naming the
# wrong revision still produces a diff, still generates mutants, and
# still prints a tally. These are the two lines that must be in it and
# the one that must not.
scopeToTheChangeTest = pkgs.runCommand "scope-to-the-change-test"
{ nativeBuildInputs = [ pkgs.git ]; } ''
set -u
${scopeToTheChange}
export HOME="$TMPDIR"
export GIT_CONFIG_GLOBAL="$TMPDIR/gitconfig"
export GIT_AUTHOR_NAME=fixture GIT_AUTHOR_EMAIL=fixture@example.invalid
export GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=fixture@example.invalid
git config --global init.defaultBranch main
repo="$TMPDIR/repo"
git init --quiet "$repo"
printf 'base\n' > "$repo/a.txt"
git -C "$repo" add a.txt
git -C "$repo" commit --quiet -m base
git -C "$repo" checkout --quiet -b work
printf 'a line I committed\n' >> "$repo/a.txt"
git -C "$repo" commit --quiet -am mine
# The base moves under the branch, as it does whenever somebody
# else merges while you work.
git -C "$repo" checkout --quiet main
printf 'a line somebody else landed\n' > "$repo/b.txt"
git -C "$repo" add b.txt
git -C "$repo" commit --quiet -m theirs
git -C "$repo" checkout --quiet work
printf 'a line I have not committed\n' >> "$repo/a.txt"
( cd "$repo" && scope_to_the_change main "$TMPDIR/change.diff" )
scoped="$(cat "$TMPDIR/change.diff")"
fail() { echo "FAIL: $1"; printf '%s\n' "$scoped"; exit 1; }
case "$scoped" in
*"a line I committed"*) ;;
*) fail "the change I committed was left out of the diff:" ;;
esac
# cargo-mutants tests the working tree, so a filter that stops at
# HEAD leaves this line in the source under test and out of the
# filter, and nothing mutates it.
case "$scoped" in
*"a line I have not committed"*) ;;
*) fail "the change I had not committed was left out of the diff:" ;;
esac
# Taken against the ref rather than the merge base, this arrives in
# reverse and is scored on your tree as your tally.
case "$scoped" in
*"a line somebody else landed"*)
fail "what landed on the base while I worked is in my diff:" ;;
esac
# A new module arrives untracked, and cargo-mutants tests it while
# git diff cannot see it.
mkdir -p "$repo/src"
printf 'pub fn fresh() -> bool { true }\n' > "$repo/src/loose.rs"
scoped="$( cd "$repo" && scope_to_the_change main "$TMPDIR/change.diff" 2>&1 )" &&
fail "it scoped a run whose new module was untracked:"
case "$scoped" in
*src/loose.rs*) ;;
*) fail "the refusal did not name the untracked module:" ;;
esac
touch $out
'';
# The wrapper minted its run directory silently and named it only on
# the way out, so a seat that backgrounded the run went looking for it
# — and on a box running two seats, "the newest
# /tmp/mutation-test-this-change.XXXXXX" is the other seat's run. That
# happened on 2026-09-03: a seat read a 198-mutant tally belonging to
# another, and escaped only because the two diffs were flagrantly
# disjoint. Nothing in the reading disagreed with anything else in it,
# because the summary line and the file list are both correct about
# whichever run they come from.
#
# So the name carries what tells runs apart, and minting says it
# aloud. cargo-mutants already names its build tree after the working
# tree it copied, which under a linked worktree is the worktree's name
# — the name was in hand at the moment the run directory was minted and
# went unused. The head sha is the other half, and it is the half a seat
# cannot recover by reading the run: your own successive runs share a
# tree, and after a merge that adds no new file two of them hold
# byte-identical change.diffs, so the diff separates you from a peer
# and cannot separate you from yourself.
#
# Minting and saying are one function because they were two moments,
# and the gap between them is the whole of the defect. The name is
# what survives a seat that backgrounds the run behind `| tail`: the
# line is the convenience, the directory's own name is the guarantee.
nameTheRunsDirectory = ''
name_the_runs_directory() {
git=${pkgs.git}/bin/git
# Named under /tmp rather than TMPDIR: a seat whose dev shell is
# too old for this command reaches a current one with `nix develop
# <ref> --command`, and that shell's TMPDIR is torn down when the
# command returns, taking the artefacts this points at with it.
tree="$(printf '%s' "$(basename "$($git rev-parse --show-toplevel)")" |
tr -c 'A-Za-z0-9._-' '-')"
run="$(mktemp -d "/tmp/mutation-test-this-change-$tree-$($git rev-parse --short HEAD).XXXXXX")"
echo "This run's output directory is $run."
}
'';
# Two runs told apart by their timestamps is what put another seat's
# tally in front of a reader, so these are the two populations that
# reading confused — a peer's run and your own earlier one — with the
# control that says the comparison can come out equal.
nameTheRunsDirectoryTest = pkgs.runCommand "name-the-runs-directory-test"
{ nativeBuildInputs = [ pkgs.git ]; } ''
set -u
${nameTheRunsDirectory}
export HOME="$TMPDIR"
export GIT_CONFIG_GLOBAL="$TMPDIR/gitconfig"
export GIT_AUTHOR_NAME=fixture GIT_AUTHOR_EMAIL=fixture@example.invalid
export GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=fixture@example.invalid
git config --global init.defaultBranch main
fail() { echo "FAIL: $1"; shift; printf '%s\n' "$@"; exit 1; }
seat() {
git init --quiet "$TMPDIR/$1"
printf 'base\n' > "$TMPDIR/$1/a.txt"
git -C "$TMPDIR/$1" add a.txt
git -C "$TMPDIR/$1" commit --quiet -m base
}
# mktemp's suffix differs whatever else does, so every comparison
# below is on the name without it.
stem() { printf '%s' "''${1%.*}"; }
seat bdi-aid
seat bdi-yjsj
cd "$TMPDIR/bdi-aid"
name_the_runs_directory > "$TMPDIR/said"
mine="$run"
said="$(cat "$TMPDIR/said")"
[ -d "$mine" ] || fail "it named a directory it had not minted:" "$mine"
# The defect itself: the directory existed and the seat was not told.
case "$said" in
*"$mine"*) ;;
*) fail "minting the directory did not say where it is:" "$said" ;;
esac
# A peer's run, minted in the same second as yours.
cd "$TMPDIR/bdi-yjsj"
name_the_runs_directory > /dev/null
theirs="$run"
[ "$(stem "$(basename "$mine")")" != "$(stem "$(basename "$theirs")")" ] ||
fail "two seats' runs are named alike:" "$mine" "$theirs"
# Your own earlier run, on the tree you had before you merged.
cd "$TMPDIR/bdi-aid"
printf 'what I merged\n' >> a.txt
git commit --quiet -am merged
name_the_runs_directory > /dev/null
[ "$(stem "$(basename "$mine")")" != "$(stem "$(basename "$run")")" ] ||
fail "one seat's runs on two trees are named alike:" "$mine" "$run"
# The control. Nothing above separates a name that carries the seat
# and the head from one that is simply random per call, because
# mktemp makes every name unique whatever the stem holds. Two runs
# of one seat on one tree must therefore land on the same stem.
earlier="$run"
name_the_runs_directory > /dev/null
[ "$(stem "$(basename "$earlier")")" = "$(stem "$(basename "$run")")" ] ||
fail "the name describes the moment rather than the run:" "$earlier" "$run"
touch $out
'';
# A mutant that does not terminate ends a run in one of two ways, and
# only one of them leaves a verdict behind, so the timeout in
# .cargo/mutants.toml and the cap here are both needed.
#
# The timeout is what produces the honest word: cargo-mutants kills the
# test that exceeds it, records TIMEOUT, and goes on to the next
# mutant. The cap is what keeps the machine while that clock runs.
#
# The cap is the one whose limit is easy to miss. Scoring words_of
# under this bound, the kernel killed the crate's own test binary at
# 8G and cargo-mutants survived to finish the run — and recorded it
# `caught`,
# because `cargo test` exits 101 and nothing tells that from a test
# which failed honestly. A cap alone turns the pathology into a clean
# sheet. A short timeout is what reads it as TIMEOUT instead, and on a
# suite whose baseline is 84 seconds no honest timeout can be short
# enough to beat the cap to a mutant allocating at 80 MB a second.
#
# 8G is an honest clean build of every test binary plus the whole suite
# measured at 3.2G, with room over it. MemorySwapMax=0 so the bound is
# on memory rather than on a machine that is alive and paging.
# OOMPolicy=continue so the kill takes the process that asked rather