-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathjustfile
More file actions
1467 lines (1360 loc) · 68.1 KB
/
Copy pathjustfile
File metadata and controls
1467 lines (1360 loc) · 68.1 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
set shell := ["bash", "-euo", "pipefail", "-c"]
project_tmp := justfile_directory() + "/.tox/tmp"
coverage_target_root := justfile_directory() + "/.tox/coverage-target"
exe_suffix := if os_family() == "windows" { ".exe" } else { "" }
workspace_target := env_var_or_default("CARGO_TARGET_DIR", justfile_directory() + "/target")
workspace_binary := workspace_target + "/debug/peryx" + exe_suffix
native_coverage_target := workspace_target + "/llvm-cov-target"
native_coverage_binary := native_coverage_target + "/debug/peryx" + exe_suffix
tools_root := justfile_directory() + "/.tox/tools"
export PERYX_TEST_TMPDIR := project_tmp
# Run the default test suite.
default: test
# Create the project-owned temporary directory.
_project-temp:
mkdir -p "{{ project_tmp }}"
# Verify that the Docker daemon is available.
_docker-ready:
docker info >/dev/null
# Print the archive-relative path of the shipped server inside a nextest archive.
_archive-peryx-binary archive:
#!/usr/bin/env bash
set -euo pipefail
tar --extract --to-stdout --file "{{ archive }}" target/nextest/binaries-metadata.json \
| jq -er '
[."rust-build-meta"."non-test-binaries"[][]
| select(.name == "peryx" and .kind == "bin-exe")
| .path]
| unique
| if length == 1 then .[0] else error("archive must contain one Peryx server binary") end
'
# Check CodSpeed benchmark selections.
_codspeed-target-contract:
#!/usr/bin/env bash
set -euo pipefail
metadata="$(cargo metadata --no-deps --format-version 1)"
jq -e '
def benches($package):
[.packages[] | select(.name == $package) | .targets[] | select(.kind == ["bench"])];
(benches("peryx-ecosystem-oci") | length) == 4 and
(benches("peryx-ecosystem-pypi") | map(select(."required-features" | length == 0)) | length) == 7 and
(benches("peryx-ecosystem-pypi") | map(select(.name == "serve" or .name == "transform")) | length) == 2
' <<<"$metadata"
just --dry-run codspeed-build peryx-ecosystem-oci all 2>&1 \
| grep -F 'case "all" in'
just --dry-run codspeed-build peryx-ecosystem-pypi parsing 2>&1 \
| grep -F 'case "parsing" in'
just --dry-run codspeed-build peryx-ecosystem-pypi serving 2>&1 \
| grep -F 'case "serving" in'
just --dry-run codspeed-run peryx-ecosystem-oci 2>&1 \
| grep -F 'cargo codspeed run -m "simulation" --package "peryx-ecosystem-oci"'
just --dry-run codspeed-run peryx-ecosystem-pypi 2>&1 \
| grep -F 'cargo codspeed run -m "simulation" --package "peryx-ecosystem-pypi"'
grep -A1 -xF '[profile.bench.package.peryx-ecosystem-pypi]' Cargo.toml \
| grep -qxF 'codegen-units = 1'
# Check coverage target isolation.
# Check that the line gate reports a body nothing ran, forgives one another monomorphization ran,
# and forgives one the browser target ran.
_coverage-lines-contract: _project-temp
#!/usr/bin/env bash
set -euo pipefail
work="{{ project_tmp }}/coverage-lines-contract"
mkdir -p "$work"
# `a.rs` holds two monomorphizations of one generic, each running the line the other does not, so
# between them they run both and neither line is a gap. Scoring the group by its best single
# member instead, which is what LLVM's own total does, reports one of them missed. `b.rs` holds a
# body nothing ran here, and `c.rs` one that only the browser target runs.
printf '%s' \
'{"data":[{"files":[{"filename":"a.rs"},{"filename":"b.rs"},{"filename":"c.rs"}],"functions":[' \
'{"filenames":["a.rs"],"regions":[[10,1,10,9,5,0,0,0],[11,1,11,9,0,0,0,0]]},' \
'{"filenames":["a.rs"],"regions":[[10,1,10,9,0,0,0,0],[11,1,11,9,7,0,0,0]]},' \
'{"filenames":["b.rs"],"regions":[[20,1,20,9,0,0,0,0]]},' \
'{"filenames":["c.rs"],"regions":[[30,1,30,9,0,0,0,0]]}]}]}' >"$work/export.json"
# The browser tracefile runs the body starting on line 30 and nothing in `b.rs`.
printf '%s\n' 'SF:c.rs' 'FN:30,_c30' 'FNDA:4,_c30' 'end_of_record' \
'SF:b.rs' 'FN:20,_b20' 'FNDA:0,_b20' 'end_of_record' >"$work/browser.lcov"
python3 coverage_lines.py gaps "$work/export.json" "$work/gaps.json"
# The check runs under the interpreter the runner image carries, the way the recipes here already
# reach for `jq`, so it needs no tool this job does not declare.
#
# The check exits non-zero when it reports anything, so its output is captured rather than piped:
# under `pipefail` a pipeline carrying it is non-zero however the reader fares.
report=$(python3 coverage_lines.py check "$work/gaps.json" "$work/browser.lcov" \
&& echo "reported nothing" || true)
# `set -e` is specified to ignore a command preceded by `!`, so a negation asserts nothing here
# and each one is written as the failure it stands for.
grep -qF 'b.rs: 20' <<<"$report"
for forbidden in 'a.rs' 'c.rs' 'reported nothing'; do
if grep -qF "$forbidden" <<<"$report"; then
echo "the line check reported ${forbidden@Q}: $report" >&2
exit 1
fi
done
_coverage-target-contract:
CARGO_TARGET_DIR="{{ project_tmp }}/coverage-target-contract" just --dry-run coverage-frontend 2>&1 \
| grep -F 'export CARGO_TARGET_DIR="{{ project_tmp }}/coverage-target-contract/frontend"'
env -u CARGO_TARGET_DIR just --dry-run coverage-frontend 2>&1 \
| grep -F 'export CARGO_TARGET_DIR="{{ coverage_target_root }}/frontend"'
just --dry-run coverage-native 2>&1 \
| grep -F 'PERYX_BIN="{{ native_coverage_binary }}"'
# Two halves gate the run and neither covers the other. `--fail-uncovered-lines` names a line no
# function reached; the line check names a function body nothing ran, which the first cannot see
# where a covered caller spans it.
just --dry-run coverage-native 2>&1 \
| grep -F -- '--fail-uncovered-lines 0 --show-missing-lines'
just --dry-run coverage-native 2>&1 \
| grep -F 'python3 coverage_lines.py'
# LLVM's own total is not the second half. It scores an instantiation group by its best single
# monomorphization, so it counts a line another monomorphization ran as missed.
! just --dry-run coverage-native 2>&1 | grep -Fq -- '--fail-under-lines'
# Both gates report to the terminal. Folding either into the lcov write makes a failure print
# nothing at all, which leaves the reader a bare exit code.
! just --dry-run coverage-native 2>&1 \
| grep -F -- '--fail-uncovered-lines 0' | grep -Fq -- '--lcov'
# Check that the default test suite receives the built Peryx binary.
_test-target-contract:
CARGO_TARGET_DIR="{{ project_tmp }}/test-target-contract" just --dry-run test 2>&1 \
| grep -F 'PERYX_BIN="{{ project_tmp }}/test-target-contract/debug/peryx{{ exe_suffix }}"'
# Check that the archive lookup reads the shipped server out of the archive metadata.
_archive-binary-contract:
just --dry-run _archive-peryx-binary archive.tar.zst 2>&1 \
| grep -F 'tar --extract --to-stdout --file "archive.tar.zst" target/nextest/binaries-metadata.json'
just --dry-run _archive-peryx-binary archive.tar.zst 2>&1 \
| grep -F 'select(.name == "peryx" and .kind == "bin-exe")'
# Check that archived sanitizer tests receive the relocated Peryx binary.
_sanitizer-target-contract:
just --dry-run sanitizer-run archive.tar.zst slice:1/8 2>&1 \
| grep -F '_archive-peryx-binary "archive.tar.zst"'
just --dry-run sanitizer-run archive.tar.zst slice:1/8 2>&1 \
| grep -F 'PERYX_BIN="$scratch/target/$binary"'
just --dry-run sanitizer-run archive.tar.zst slice:1/8 2>&1 \
| grep -F -- '--extract-to "$scratch"'
# Check that archived mutation baseline tests receive the relocated Peryx binary.
_mutation-baseline-target-contract:
just --dry-run mutation-baseline-run archive.tar.zst slice:1/8 2>&1 \
| grep -F '_archive-peryx-binary "archive.tar.zst"'
just --dry-run mutation-baseline-run archive.tar.zst slice:1/8 2>&1 \
| grep -F 'PERYX_BIN="$scratch/target/$binary"'
just --dry-run mutation-baseline-run archive.tar.zst slice:1/8 2>&1 \
| grep -F -- '--extract-to "$scratch"'
# Check mutation shard planning.
_mutation-shard-count-contract:
test "$(just mutation-shard-count 255 256)" = 1
test "$(just mutation-shard-count 8193 256)" = 33
test "$(just mutation-shard-count 513 256)" = 3
# Check the zero-feature binary with declared CI tools.
_features-tool-contract:
#!/usr/bin/env bash
set -euo pipefail
# A build wrapper is how a developer accelerates the build rather than a tool the build needs, so
# the narrowed PATH below must not take it away. `heavy.sh` names sccache by bare word, which the
# narrow PATH cannot resolve, and cargo then fails to spawn it instead of reaching the guard this
# contract reads. Resolving each wrapper while the full PATH still applies keeps both true.
for wrapper in RUSTC_WRAPPER RUSTC_WORKSPACE_WRAPPER; do
named="${!wrapper:-}"
if [[ -z $named || $named == /* ]]; then
continue
fi
if resolved=$(command -v "$named"); then
export "$wrapper=$resolved"
else
unset "$wrapper"
fi
done
export PATH="$(dirname "$(command -v cargo)"):/usr/bin:/bin"
if command -v rg >/dev/null; then
echo 'ripgrep is present in the feature contract' >&2
exit 1
fi
"{{ just_executable() }}" _zero-feature-binary
# Check that every browser suite runs and that any suite failure fails the recipe.
_frontend-test-contract:
#!/usr/bin/env bash
set -euo pipefail
script=$("{{ just_executable() }}" --dry-run frontend-test 2>&1)
for directory in \
crates/peryx-web/tests/frontend \
crates/peryx-ecosystem-pypi/tests/frontend \
crates/peryx-ecosystem-oci/tests/frontend; do
if [[ $(grep -Fc "$directory" <<<"$script") -ne 1 ]]; then
printf 'frontend-test must run %s exactly once\n' "$directory" >&2
exit 1
fi
done
grep -Fq './node_modules/.bin/playwright test --config playwright.config.mjs) || failed+=("$suite")' <<<"$script"
grep -Fq 'if (( ${#failed[@]} > 0 )); then' <<<"$script"
grep -Fq 'exit 1' <<<"$script"
# A suite that cannot run has to fail the gate. Installing it is the first answer and saying so is
# the second; quietly running two suites out of three would report a green frontend that never
# exercised the third.
grep -Fq 'npm --prefix "$suite" ci' <<<"$script"
grep -Fq 'still has no Playwright runner after npm ci' <<<"$script"
# Whatever `playwright` resolves to on PATH is a different tool that rejects `test`, so reaching it
# turns a missing install into an unrelated error.
if grep -Eq '(^|[^/])\bplaywright test\b' <<<"$script"; then
printf 'frontend-test must run each suite own Playwright, not the one on PATH\n' >&2
exit 1
fi
# Joining the suites into one `;` list puts them back under plain `set -e`, which reports
# the first broken suite and never runs the rest.
if grep -Eq 'playwright test\b.*;' <<<"$script"; then
printf 'frontend-test must not join browser suites with `;`\n' >&2
exit 1
fi
# Check Rust formatting.
format-check: _project-temp
cargo fmt --all --check --
# Check every workspace target with all features.
check: _project-temp
cargo check --workspace --all-targets --all-features
# Lint every workspace target with Clippy.
clippy: _project-temp
cargo clippy --workspace --all-targets --all-features -- -D warnings
# Check Rust formatting and lints.
lint-source: format-check clippy
# Check rustdoc, Markdown, and spelling.
lint-docs: _project-temp
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
prek run mdformat --all-files
prek run codespell --all-files
# Check workflows and repository automation.
lint-automation: _project-temp _archive-binary-contract _browser-contract _codspeed-target-contract _coverage-lines-contract _coverage-target-contract _embedded-docs-contract _features-tool-contract _frontend-test-contract _mise-trust-contract _mutation-baseline-target-contract _mutation-profile-contract _mutation-scope-contract _mutation-shard-count-contract _mutation-telemetry-contract _paused-clock-contract _readthedocs-contract _renovate-contract _sanitizer-target-contract _test-target-contract
SKIP=cargo-fmt,cargo-clippy,mdformat,codespell prek run --all-files
# Check that mutation scope stays on production code and that the shard plan follows it.
_mutation-scope-contract:
#!/usr/bin/env bash
set -euo pipefail
for glob in 'crates/*/src/bench/**' 'crates/peryx-test-support/**' 'crates/*/tests/**'; do
if ! grep -Fq "\"$glob\"" .cargo/mutants.toml; then
printf 'mutation scope must exclude %s; see contributing/ci.md for why\n' "$glob" >&2
exit 1
fi
done
# The shard count divides the same list the run mutates. Counting a wider plan than the run covers
# would size the matrix for mutants nothing then examines.
just --dry-run mutation-count 2>&1 | grep -Fq -- '--list --workspace --all-features'
_mutation-profile-contract:
just --dry-run mutation 0/1 true 2 skip 500 round-robin 2>&1 \
| grep -F 'CARGO_INCREMENTAL=0 CARGO_PROFILE_TEST_DEBUG=0 PATH='
# Check that a shard's telemetry can name the resource it ran out of.
_mutation-telemetry-contract:
#!/usr/bin/env bash
set -euo pipefail
recipe="$(just --dry-run mutation-observed 0/1 true 1 skip 500 round-robin 2>&1)"
# A shard that loses its runner uploads nothing and leaves no log, so each property below is what
# a shard that survives can still report about the run-up.
check() {
if ! grep -Fq -- "$2" <<<"$recipe"; then
printf 'mutation telemetry must %s; see contributing/ci.md for why\n' "$1" >&2
exit 1
fi
}
check 'read progress from the outcomes cargo-mutants writes' '.tox/mutants/mutants.out/outcomes.json'
check 'report free space on the filesystem holding the workspace' 'disk.available=%s'
check 'report the memory the machine has left' 'memory.available=%s'
check 'append every sample to the uploaded output directory' 'tee -a "$trace"'
# Check that the hosted build commands survive their shell wrapper.
_readthedocs-contract:
#!/usr/bin/env bash
set -euo pipefail
# Read the Docs runs each command as /bin/sh -c '<command>' without escaping, so one quote of its
# own ends the wrapper and the rest of the line reparses as something else.
! grep -q "'" .readthedocs.yaml
# Check that every site file a crate embeds is named in the CI path filter.
#
# CI lets a pull request that changes only the site or Markdown skip the Rust jobs. A document a crate
# reads with `include_str!` is compiled into a binary or a test, so a change to it has to run them
# after all; the workflow lists those files by hand, and this keeps the list from going stale.
_embedded-docs-contract:
#!/usr/bin/env bash
set -euo pipefail
grep -rhoE 'include_(str|bytes)!\("[^"]*"\)' crates --include='*.rs' \
| grep -oE '"[^"]+"' | tr -d '"' | grep -E '^(\.\./)+site/|\.md$' | sed -E 's#^(\.\./)+##' | sort -u \
| while IFS= read -r embedded; do
if ! grep -Fq -- "- '$embedded'" .github/workflows/ci.yml; then
printf '%s is embedded by a crate but missing from the embedded filter in ci.yml\n' "$embedded" >&2
exit 1
fi
done
# Check that no test holds a paused clock across real socket work without a stated reason.
#
# A paused tokio clock auto-advances to the next pending timer whenever the runtime parks, and a
# socket read parks, so the clock rather than the code can satisfy such a test's assertion. The
# hazard needs a pending timer at the moment of the park, which most pairings do not have, so this
# asks for a reason rather than deciding the question itself.
_paused-clock-contract:
#!/usr/bin/env bash
set -euo pipefail
harness='TestServer::start|TcpListener::bind|MockServer::start|ControlledServer::start|reqwest::(get|Client)|wiremock'
program='
{ source[NR] = $0 }
END {
total = NR
count = 0
for (i = 1; i <= total; i++) {
if (match(source[i], /^[ \t]*(pub\([a-z()]*\) )?(pub )?(async )?fn [A-Za-z0-9_]+/) == 0)
continue
indent = source[i]
sub(/[^ \t].*$/, "", indent)
name = substr(source[i], RSTART, RLENGTH)
sub(/.*fn /, "", name)
last = total
for (j = i + 1; j <= total; j++)
if (source[j] == indent "}") { last = j; break }
count++
named[count] = name
defined[name] = count
line_of[count] = i
body[count] = ""
for (j = i; j <= last; j++)
body[count] = body[count] source[j] "\n"
header = ""
for (j = i - 1; j >= 1; j--) {
above = source[j]
sub(/^[ \t]+/, "", above)
if (above == "" || above == "}")
break
header = source[j] "\n" header
}
heads[count] = header
}
for (k = 1; k <= count; k++) {
resumes[k] = index(body[k], "tokio::time::resume") > 0
paused[k] = index(heads[k], "start_paused") > 0
at = index(heads[k], "paused-clock-safe:")
reason[k] = at == 0 ? "" : trim(substr(heads[k], at + 18))
}
for (k = 1; k <= count; k++) {
if (paused[k] == 0 && reason[k] != "")
tell(k, "states a paused-clock reason but never pauses its clock; delete the comment")
if (paused[k] == 0 || harness == 0)
continue
if (reason[k] != "" && length(reason[k]) < 20)
tell(k, "states a paused-clock reason too short for the next reader to re-check")
else if (reason[k] == "" && running(k) == 0)
tell(k, "holds a paused clock in a file that opens sockets")
}
}
function trim(text) {
sub(/\n.*$/, "", text)
sub(/^[ \t]+/, "", text)
sub(/[ \t]+$/, "", text)
return text
}
function running(start, queue, head, tail, seen, k, words, w, i, name, text) {
head = 1
tail = 1
queue[1] = start
seen[start] = 1
while (head <= tail) {
k = queue[head++]
if (resumes[k])
return 1
text = body[k]
gsub(/[^A-Za-z0-9_]/, " ", text)
split(text, words, " ")
for (i in words) {
name = words[i]
if (name in defined) {
w = defined[name]
if (seen[w] == 0) { seen[w] = 1; queue[++tail] = w }
}
}
}
return 0
}
function tell(k, message) {
printf "%s:%d: %s: %s\n", FILENAME, line_of[k], named[k], message
}
'
findings=""
while IFS= read -r file; do
opens=0
if grep -Eq "$harness" "$file"; then
opens=1
fi
found=$(awk -v harness="$opens" "$program" "$file")
if [ -n "$found" ]; then
findings="$findings$found"$'\n'
fi
done < <(grep -rlE --include='*.rs' 'start_paused|paused-clock-safe' crates/ | sort)
if [ -n "$findings" ]; then
printf '%s' "$findings" >&2
cat >&2 <<'GUIDANCE'
Run the clock while the socket work is outstanding and pause only once that work is proven
done, the shape PRs #2100, #2108 and #2117 established: bind and accept with the clock running,
await the handle or read to EOF to prove the request landed, then pause and step the clock.
Prefer sleep over advance for the paused step, since a paused clock stops at the earliest
pending timer and advance jumps over an action due sooner than the one under test.
A test that never leaves work on the wire is fine as it stands. Say why, on the line above its
attribute, and this check will take your word for it:
// paused-clock-safe: the saturated cap answers 429 before any upstream call
GUIDANCE
exit 1
fi
# Check the custom Renovate release matcher.
_renovate-contract:
#!/usr/bin/env bash
set -euo pipefail
jq --exit-status --rawfile readthedocs .readthedocs.yaml \
--rawfile setup .github/actions/setup/action.yml '
[.customManagers[] | select(.depNameTemplate == "jdx/mise")] as $rules
| [$rules[] | select(.managerFilePatterns[0] | contains("readthedocs"))][0] as $rtd
| [$rules[] | select(.managerFilePatterns[0] | contains("actions/setup"))][0] as $action
| ($rules | length) == 2
and ([$readthedocs | scan($rtd.matchStrings[0])] | length) == 1
and ([$setup | scan($action.matchStrings[0])] | length) == 1
and ([$readthedocs | capture($rtd.matchStrings[0]).currentValue,
$setup | capture($action.matchStrings[0]).currentValue] | unique | length) == 1
and ($readthedocs | capture($rtd.matchStrings[0]).currentValue
| test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))
' renovate.json > /dev/null
# Trust this checkout's mise config so a fresh worktree can read its tool versions.
#
# mise refuses to read a config it has not been told to trust, and a worktree created minutes ago has
# not. Running `just` here already runs this repository's recipes, so trusting its tool manifest hands
# it no reach it did not already have, and without it every browser recipe reports a trust prompt in
# place of the thing it was asked to check.
_mise-trusted:
@mise trust mise.toml >/dev/null
@mise trust mise.browser.toml >/dev/null
# Check browser package, binary, and updater ownership.
_browser-contract: _mise-trusted
#!/usr/bin/env bash
set -euo pipefail
browser_env=$(MISE_ENV=browser mise env --json)
! env -u MISE_ENV mise config ls | grep -Fq mise.browser.toml
MISE_ENV=browser mise config ls | grep -Fq mise.browser.toml
! grep -Eq '^(linux|windows)-arm64' mise.browser.toml mise.browser.lock
test "$(grep -Ec '^\[tools\."http:(playwright|puppeteer)-headless-shell"\."platforms\.(linux-x64|macos-arm64|macos-x64|windows-x64)"\]$' mise.browser.lock)" -eq 8
playwright=$(jq -r .PERYX_PLAYWRIGHT_PACKAGE_VERSION <<<"$browser_env")
puppeteer=$(jq -r .PERYX_PUPPETEER_PACKAGE_VERSION <<<"$browser_env")
for directory in \
crates/peryx-web/tests/frontend \
crates/peryx-ecosystem-pypi/tests/frontend \
crates/peryx-ecosystem-oci/tests/frontend; do
jq -se --arg version "$playwright" '
.[0].devDependencies["@playwright/test"] == $version and
.[1].packages[""].devDependencies["@playwright/test"] == $version and
.[1].packages["node_modules/@playwright/test"].version == $version and
.[1].packages["node_modules/playwright-core"].version == $version
' "$directory/package.json" "$directory/package-lock.json" > /dev/null
done
jq -se --arg version "$puppeteer" '
.[0].devDependencies.puppeteer == "npm:puppeteer-core@\($version)" and
.[1].packages[""].devDependencies.puppeteer == "npm:puppeteer-core@\($version)" and
.[1].packages["node_modules/puppeteer"].name == "puppeteer-core" and
.[1].packages["node_modules/puppeteer"].version == $version
' site/package.json site/package-lock.json > /dev/null
jq -e '
[.packageRules[] | select(.description == "Update browser packages with their binaries")]
| length == 1 and .[0].enabled == false
' renovate.json > /dev/null
# Check that every recipe invoking `mise` runs the trust step first.
#
# mise takes trust on its own for `install`, `run`, `exec` and `watch` and for nothing else, so a
# recipe leading with a read fails outright on an untrusted worktree, and one leading with an install
# works by side effect until the paranoid setting removes it. Three pull requests each wired one
# caller by hand and left the rest, so this asserts the property rather than today's six names: a
# recipe whose body invokes `mise` has to reach `_mise-trusted` somewhere in its dependencies.
#
# The two recipes implementing that mechanism are exempt: `_mise-trusted` grants the trust, and this
# one reads the dependency graph through `just --dump` and never calls mise at all. Both mention the
# word in their own text, which a scan over recipe bodies cannot tell from a command.
_mise-trust-contract:
#!/usr/bin/env bash
set -euo pipefail
offenders=$("{{ just_executable() }}" --dump --dump-format json \
| jq -r --arg trust _mise-trusted --arg contract _mise-trust-contract '
.recipes as $recipes
| ($recipes | map_values([.dependencies[].recipe])) as $deps
| def reached($name): [$name] + ([$deps[$name][]? | reached(.)] | flatten);
$recipes
| to_entries[]
| select(.key != $trust and .key != $contract)
| select(
(.value.body // [])
| map(map(select(type == "string")) | join(""))
| join("\n")
| test("(^|[^A-Za-z0-9_.-])mise[ \t]+[a-z]"; "m")
)
| select(reached(.key) | index($trust) | not)
| .key
')
if [[ -n "$offenders" ]]; then
printf 'these recipes invoke mise without reaching _mise-trusted:\n%s\n' "$offenders" >&2
exit 1
fi
# Check dependency policy.
lint-deps: _project-temp
cargo deny check
# Check committed PyPI snapshots.
snapshots: _project-temp
cargo insta test --package peryx-ecosystem-pypi --lib --all-features \
--unreferenced reject --test-runner nextest --nextest-profile ci
# Check workspace public API compatibility.
#
# The release type is stated rather than inferred. Every crate here reads `0.0.1`, and a `0.0.x` release
# permits any change, so cargo-semver-checks compares the two versions, finds them equal, assumes a major
# bump and skips all 254 checks. Naming `patch` asks the question worth asking: what would this change
# break for someone pinned to the current API. A break is allowed before the first release, so this
# reports one rather than forbidding it, and CI does not run it.
semver base="origin/main" release_type="patch": _project-temp
cargo semver-checks check-release --workspace --default-features --baseline-rev "{{ base }}" \
--release-type "{{ release_type }}"
# Check one deterministic shard of workspace public APIs.
semver-shard shard shards base="origin/main" release_type="patch": _project-temp
cargo metadata --no-deps --format-version 1 \
| jq -r --argjson shard "{{ shard }}" --argjson shards "{{ shards }}" \
'[.packages[] | select(.publish != []) | .name] | to_entries[] | select(.key % $shards == $shard) | .value' \
| xargs -n 1 cargo semver-checks check-release --default-features --baseline-rev "{{ base }}" \
--release-type "{{ release_type }}" --package
# Check snapshots, public APIs, and the release plan.
lint-contracts base="origin/main": snapshots _coverage-target-contract
just semver "{{ base }}"
just release-plan
# Run every lint lane.
lint base="origin/main": _project-temp
just lint-source
just lint-docs
just lint-automation
just lint-deps
just lint-contracts "{{ base }}"
# Install external test tools into the project cache.
test-deps: _project-temp
PATH="{{ tools_root }}/bin:$PATH" UV_TOOL_BIN_DIR="{{ tools_root }}/bin" \
UV_TOOL_DIR="{{ tools_root }}" uv tool install twine
# Run workspace tests, doctests, and benchmark harnesses.
test: test-deps
PERYX_BIN="{{ workspace_binary }}" PATH="{{ tools_root }}/bin:$PATH" cargo nextest run \
--workspace --exclude peryx-storage --all-features --profile ci \
-E 'not(test(e2e_live))'
cargo nextest run --package peryx-storage --profile ci
cargo test --workspace --all-features --doc
just benchmark
# Run workspace benchmark harnesses as tests.
benchmark: _project-temp
cargo test --workspace --all-features --bench '*' --no-fail-fast
# Run tests that cover platform-specific boundaries.
platform-test: _project-temp
cargo check --workspace --all-targets --all-features
cargo nextest run --package peryx --test cli_entrypoint --all-features --profile ci
cargo nextest run --package peryx-upstream --all-features --profile ci
cargo nextest run --package peryx-test-support --all-features --profile ci
cargo nextest run --package peryx-storage --all-features --test integration \
--profile ci -E 'test(/blob_backend/)'
# Run hermetic PyPI client boundary tests.
e2e: _project-temp
PERYX_BIN="$(just _system-test-build composition-pypi)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-pypi-system-tests \
--features e2e --test e2e -E 'not(test(e2e_live))'
# Run live PyPI client boundary tests.
e2e-live: test-deps
PERYX_BIN="$(just _system-test-build composition-pypi)" PERYX_SINGLE_COMPOSITION=1 \
PATH="{{ tools_root }}/bin:$PATH" cargo nextest run -p peryx-pypi-system-tests \
--features e2e-live --test e2e -E 'test(e2e_live)'
# Run PyPI system tests without external-service cases.
pypi-system: _project-temp
PERYX_BIN="$(just _system-test-build composition-pypi)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-pypi-system-tests --tests \
-E 'not(binary(e2e)) & not(binary(availability)) & not(binary(s3_upload))'
# Run OCI system tests without availability cases.
oci-system: _project-temp
PERYX_BIN="$(just _system-test-build composition-oci)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-oci-system-tests --tests -E 'not(binary(availability))'
# Run the PyPI S3 upload tests.
s3: _project-temp
PERYX_BIN="$(just _system-test-build composition-pypi)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-pypi-system-tests --test s3_upload
# Run storage tests backed by S3 containers.
storage-s3: _project-temp _docker-ready
cargo nextest run -p peryx-storage --features container-tests --test integration
# Run distributed availability tests.
availability: _project-temp
cargo nextest run -p peryx --features availability-e2e --test availability --test cluster --test observability
PERYX_BIN="$(just _system-test-build composition-pypi)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-pypi-system-tests --test availability
PERYX_BIN="$(just _system-test-build composition-oci)" PERYX_SINGLE_COMPOSITION=1 \
cargo nextest run -p peryx-oci-system-tests --test availability
# Run an availability simulation selection.
simulation filter="all()": _project-temp
cargo nextest run -p peryx --features sim-campaign --test sim_campaign -E '{{ filter }}'
# Check every feature independently.
features: _project-temp
cargo check --package peryx --no-default-features --lib
just _zero-feature-binary
cargo hack --workspace --exclude peryx --each-feature check --all-targets
cargo hack --package peryx --each-feature --features composition-pypi check --all-targets
cargo check --package peryx --no-default-features --features composition-oci --all-targets
# Check that the binary rejects an empty composition.
_zero-feature-binary:
#!/usr/bin/env bash
set -euo pipefail
guard='the peryx binary requires at least one `composition-*` feature'
if output=$(cargo check --package peryx --no-default-features --bin peryx 2>&1); then
printf 'the zero-feature peryx binary compiled; it must refuse an empty composition\n' >&2
exit 1
fi
if grep -Fq "$guard" <<<"$output"; then
exit 0
fi
# Neither outcome the contract knows about. Saying so beats reporting a violation the build never
# got far enough to observe, which reads as "your change broke this" when nothing here ran.
printf 'unavailable: the zero-feature check could not run, so it proved nothing about this tree\n' >&2
printf 'cargo stopped for a reason other than the composition guard:\n%s\n' "$output" >&2
exit 1
# Build the shipped server with one composition feature.
_system-test-build feature: _project-temp
cargo build --package peryx --bin peryx --no-default-features --features "{{ feature }}" \
--message-format json-render-diagnostics \
| jq -er 'if .reason == "compiler-message" then (.message.rendered | stderr | empty) \
elif .reason == "compiler-artifact" and .target.kind == ["bin"] and .target.name == "peryx" then .executable \
else empty end'
# Check direct dependency lower bounds.
direct-minimum: _project-temp
rm -rf .tox/direct-minimum
rsync -a --exclude .git --exclude .tox --exclude target ./ .tox/direct-minimum/
cargo +nightly update --manifest-path .tox/direct-minimum/Cargo.toml -Z direct-minimal-versions
cargo +nightly check --manifest-path .tox/direct-minimum/Cargo.toml --workspace --all-targets
rm -rf .tox/direct-minimum
# Interpret pure core crates with Miri.
miri: _project-temp
TMPDIR="${RUNNER_TEMP:-/tmp}" cargo +nightly miri test --package peryx-core --lib --tests
TMPDIR="${RUNNER_TEMP:-/tmp}" cargo +nightly miri test --package peryx-pql --lib --tests
TMPDIR="${RUNNER_TEMP:-/tmp}" cargo +nightly miri test --package peryx-policy --lib --tests
# Check distributed runtime interleavings with Loom.
loom: _project-temp
RUSTFLAGS="--cfg peryx_loom" cargo test --package peryx-ha-distributed --lib runtime_worker::loom_tests
# Run AddressSanitizer against a workspace partition.
sanitizer-address partition="slice:1/1": test-deps
ASAN_OPTIONS=allow_addr2line=1 RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }-Zsanitizer=address" \
PATH="{{ tools_root }}/bin:$PATH" \
cargo +nightly nextest run -Z build-std --workspace --target x86_64-unknown-linux-gnu \
--features peryx/process-fixture --profile ci --build-jobs 1 --test-threads 1 \
--partition "{{ partition }}" -E 'not(test(e2e_live))'
# Build the AddressSanitizer test archive.
sanitizer-archive archive: _project-temp
RUSTFLAGS="${RUSTFLAGS:+$RUSTFLAGS }-Zsanitizer=address" PATH="{{ tools_root }}/bin:$PATH" \
cargo +nightly nextest archive -Z build-std --workspace --target x86_64-unknown-linux-gnu \
--features peryx/process-fixture --profile ci --build-jobs 1 --archive-file "{{ archive }}"
# Run a partition from an AddressSanitizer archive.
sanitizer-run archive partition="slice:1/1": test-deps
#!/usr/bin/env bash
set -euo pipefail
scratch=$(mktemp -d "{{ project_tmp }}/sanitizer.XXXXXX")
trap 'rm -rf "$scratch"' EXIT
binary=$("{{ just_executable() }}" _archive-peryx-binary "{{ archive }}")
ASAN_OPTIONS=allow_addr2line=1 PERYX_BIN="$scratch/target/$binary" \
PATH="{{ tools_root }}/bin:$PATH" cargo +nightly nextest run \
--archive-file "{{ archive }}" --extract-to "$scratch" \
--workspace-remap "{{ justfile_directory() }}" --profile ci --test-threads 1 \
--partition "{{ partition }}" -E 'not(test(e2e_live))'
# Run one cargo-fuzz target.
fuzz package target seconds="60": _project-temp
cd "crates/{{ package }}/fuzz" && cargo +nightly fuzz run \
--target "$(rustc +nightly --print host-tuple)" "{{ target }}" -- -max_total_time="{{ seconds }}"
# Mutate one workspace shard.
mutation shard="0/1" in_place="false" jobs="2" baseline="run" timeout="500" sharding="slice": test-deps
CARGO_INCREMENTAL=0 CARGO_PROFILE_TEST_DEBUG=0 PATH="{{ tools_root }}/bin:$PATH" \
cargo mutants --workspace --all-features --test-tool nextest \
--no-shuffle --shard "{{ shard }}" --sharding "{{ sharding }}" --output .tox/mutants \
{{ if in_place == "true" { "--in-place" } else { "--jobs " + jobs } }} \
--jobserver-tasks "{{ jobs }}" --baseline "{{ baseline }}" \
--timeout "{{ timeout }}" --build-timeout "{{ timeout }}" \
-- --profile mutation -E 'not(test(e2e_live))'
# Run one mutation shard with Linux resource telemetry.
mutation-observed shard="0/1" in_place="false" jobs="2" baseline="run" timeout="500" sharding="slice":
#!/usr/bin/env bash
set -uo pipefail
cgroup_root="/sys/fs/cgroup$(awk -F: '$1 == "0" { print $3 }' /proc/self/cgroup 2>/dev/null)"
if [[ ! -r "$cgroup_root/cgroup.controllers" ]]; then
printf 'mutation resource telemetry requires Linux cgroup v2\n' >&2
exit 2
fi
# cargo-mutants creates mutants.out inside the output directory, and the shard uploads that
# directory, so a trace written beside it reaches the artifact. A job log does not: every log
# from a run whose shards executed already answers BlobNotFound.
mkdir -p .tox/mutants
trace=.tox/mutants/resource.log
sample() {
local metric pressure progress
if [[ -r .tox/mutants/mutants.out/outcomes.json ]]; then
progress="$(jq -c '{
completed: (.outcomes | length),
last: (.outcomes[-1].scenario.Mutant.name //
(if (.outcomes | length) > 0 then (.outcomes[-1].scenario | tostring) else null end))
}' .tox/mutants/mutants.out/outcomes.json 2>/dev/null || printf '{"completed":null,"last":null}')"
else
progress='{"completed":0,"last":null}'
fi
printf 'mutation-resource timestamp=%s progress=%s' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$progress"
# The cgroup counts this job's own process tree. What starves a hosted runner is what the
# machine has left, and its 14 GB disk is the smallest of the three resources it publishes.
if [[ -r /proc/meminfo ]]; then
printf ' memory.available=%s' "$(awk '$1 == "MemAvailable:" { print $2 * 1024 }' /proc/meminfo)"
fi
df -P -B1 . | awk 'NR == 2 { printf " disk.total=%s disk.available=%s", $2, $4 }'
for metric in memory.current memory.peak pids.current; do
if [[ -r "$cgroup_root/$metric" ]]; then
printf ' %s=%s' "$metric" "$(<"$cgroup_root/$metric")"
fi
done
for metric in memory.events pids.events; do
if [[ -r "$cgroup_root/$metric" ]]; then
printf ' %s=%s' "$metric" "$(awk '{printf "%s%s=%s", NR == 1 ? "" : ",", $1, $2}' "$cgroup_root/$metric")"
fi
done
for pressure in cpu memory io; do
if [[ -r "/proc/pressure/$pressure" ]]; then
printf ' %s.pressure=%s' "$pressure" "$(paste -sd ';' "/proc/pressure/$pressure")"
fi
done
printf '\n'
}
record() { sample | tee -a "$trace"; }
record
while sleep 60; do record; done &
monitor_pid=$!
trap 'kill "$monitor_pid" 2>/dev/null || :; wait "$monitor_pid" 2>/dev/null || :' EXIT
just mutation "{{ shard }}" "{{ in_place }}" "{{ jobs }}" "{{ baseline }}" "{{ timeout }}" "{{ sharding }}"
mutation_status=$?
record
printf 'mutation-exit status=%d\n' "$mutation_status" | tee -a "$trace"
exit "$mutation_status"
# Run the mutation baseline suite.
mutation-baseline: test-deps
INSTA_UPDATE=no INSTA_FORCE_PASS=0 PATH="{{ tools_root }}/bin:$PATH" cargo nextest run --verbose \
--workspace --all-features --profile ci -E 'not(test(e2e_live))'
# Build the mutation baseline test archive.
mutation-baseline-archive archive: _project-temp
cargo nextest archive --workspace --all-features --profile ci --archive-file "{{ archive }}"
# Run a partition from the mutation baseline archive.
mutation-baseline-run archive partition="slice:1/1": test-deps
#!/usr/bin/env bash
set -euo pipefail
scratch=$(mktemp -d "{{ project_tmp }}/mutation-baseline.XXXXXX")
trap 'rm -rf "$scratch"' EXIT
binary=$("{{ just_executable() }}" _archive-peryx-binary "{{ archive }}")
INSTA_UPDATE=no INSTA_FORCE_PASS=0 PERYX_BIN="$scratch/target/$binary" \
PATH="{{ tools_root }}/bin:$PATH" cargo nextest run \
--archive-file "{{ archive }}" --extract-to "$scratch" \
--workspace-remap "{{ justfile_directory() }}" --profile ci \
--partition "{{ partition }}" -E 'not(test(e2e_live))'
# Count workspace mutation candidates.
mutation-count: _project-temp
cargo mutants --list --workspace --all-features | wc -l
# Calculate the shard count for a mutant total and per-shard target.
mutation-shard-count mutants target:
#!/usr/bin/env bash
set -euo pipefail
mutants={{ quote(mutants) }}
target={{ quote(target) }}
if ! [[ "$mutants" =~ ^[1-9][0-9]*$ && "$target" =~ ^[1-9][0-9]*$ ]]; then
printf 'mutants and target must be positive integers\n' >&2
exit 1
fi
printf '%d\n' "$(( (mutants + target - 1) / target ))"
# Install browser-test dependencies for the shared and owner suites.
frontend-deps: _project-temp _mise-trusted
MISE_ENV=browser mise install --locked http:playwright-headless-shell
npm --prefix crates/peryx-web/tests/frontend ci
npm --prefix crates/peryx-ecosystem-pypi/tests/frontend ci
npm --prefix crates/peryx-ecosystem-oci/tests/frontend ci
# Run the shared and owner browser suites, installing and building whatever they are missing.
frontend-test: _project-temp _mise-trusted
#!/usr/bin/env bash
set -euo pipefail
# Every suite runs even after an earlier one fails, so a single CI round reports all three;
# the recipe then exits non-zero if any of them failed. Collecting the statuses is what
# buys that: left uncollected, `set -e` ends the run at the first broken suite, and the two
# larger suites are the ones most likely to break.
suites=(
crates/peryx-web/tests/frontend
crates/peryx-ecosystem-pypi/tests/frontend
crates/peryx-ecosystem-oci/tests/frontend
)
if ! shell_root=$(MISE_ENV=browser mise where http:playwright-headless-shell 2>/dev/null); then
MISE_ENV=browser mise install --locked http:playwright-headless-shell
shell_root=$(MISE_ENV=browser mise where http:playwright-headless-shell)
fi
browser="$shell_root/chrome-headless-shell"
if [[ -f "$browser.exe" ]]; then
browser="$browser.exe"
fi
export PERYX_PLAYWRIGHT_BROWSER_PATH="${PERYX_PLAYWRIGHT_BROWSER_PATH:-$browser}"
# `export VAR="$(cmd)"` reports the status of `export`, so the lookup gets its own assignment.
version=$(MISE_ENV=browser mise current http:playwright-headless-shell)
export PERYX_PLAYWRIGHT_BROWSER_VERSION="$version"
# A suite installs when it has no runner or its lock file has moved on, so a fresh worktree runs
# the gate instead of reporting a tool it never had. An installed suite skips the work, which is
# what keeps the `frontend-deps` step CI runs first from paying for it twice.
for suite in "${suites[@]}"; do
if [[ ! -x "$suite/node_modules/.bin/playwright" ]] \
|| [[ "$suite/package-lock.json" -nt "$suite/node_modules/.package-lock.json" ]]; then
npm --prefix "$suite" ci
fi
if [[ ! -x "$suite/node_modules/.bin/playwright" ]]; then
printf 'browser suite %s still has no Playwright runner after npm ci\n' "$suite" >&2
exit 1
fi
done
# A caller that has already built names its binary, as the coverage run does with its instrumented
# copy. Building only when it has not is what lets a fresh worktree run the gate without making CI
# build a second server it would not use.
if [[ -z "${PERYX_FRONTEND_BINARY:-}" ]]; then
cargo leptos build
fi
failed=()
for suite in "${suites[@]}"; do
# The suite's own runner rather than whatever `playwright` resolves to: the one on PATH is a
# different tool that rejects `test`, so a lane reaching it debugs that instead of the install.
(cd "$suite" && ./node_modules/.bin/playwright test --config playwright.config.mjs) || failed+=("$suite")
done
if (( ${#failed[@]} > 0 )); then
printf 'browser suite failed: %s\n' "${failed[@]}" >&2
exit 1
fi
# Print tool versions used by local and container validation.
versions: _project-temp
rustc --version
cargo --version
cargo nextest --version
cargo llvm-cov --version
just --version
node --version
npm --version
# Refresh locked mise tool versions and checksums.
mise-lock: _mise-trusted
mise lock --bump
MISE_ENV=browser mise lock --bump --platform linux-x64,macos-arm64,macos-x64,windows-x64
# Update browser packages, verified archives, and rendered diagrams.
browser-update:
#!/usr/bin/env bash
set -euo pipefail
PUPPETEER_SKIP_DOWNLOAD=1 npm --prefix site install --save-dev --save-exact \
puppeteer@npm:puppeteer-core@latest
for directory in \
crates/peryx-web/tests/frontend \
crates/peryx-ecosystem-pypi/tests/frontend \
crates/peryx-ecosystem-oci/tests/frontend; do
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix "$directory" install --save-dev --save-exact \
@playwright/test@latest
done
if just _browser-contract >/dev/null 2>&1; then exit; fi
just browser-lock
# Rebuild the browser lock from package release metadata.
browser-lock: _mise-trusted
#!/usr/bin/env bash
set -euo pipefail
PUPPETEER_SKIP_DOWNLOAD=1 npm --prefix site ci
for directory in \
crates/peryx-web/tests/frontend \
crates/peryx-ecosystem-pypi/tests/frontend \
crates/peryx-ecosystem-oci/tests/frontend; do
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm --prefix "$directory" ci
done
node --input-type=module <<'NODE'
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
const frontends = [
"crates/peryx-web/tests/frontend",
"crates/peryx-ecosystem-pypi/tests/frontend",
"crates/peryx-ecosystem-oci/tests/frontend",
];
const readJson = async (file) => JSON.parse(await readFile(file, "utf8"));
const playwrightPackages = await Promise.all(
frontends.map(async (directory) => ({
manifest: (await readJson(join(directory, "package.json"))).devDependencies[
"@playwright/test"
],
lock: (await readJson(join(directory, "package-lock.json"))).packages[
"node_modules/@playwright/test"
].version,
})),
);
if (new Set(playwrightPackages.flatMap(Object.values)).size !== 1)
throw new Error("Playwright package revisions differ");
const playwrightPackage = playwrightPackages[0].manifest;
const playwrightRequire = createRequire(
resolve(frontends[0], "package.json"),
);
const playwrightBrowser = (await readJson(
join(
dirname(playwrightRequire.resolve("playwright-core/package.json")),
"browsers.json",
),
)).browsers.find(({ name }) => name === "chromium-headless-shell").browserVersion;
const site = await readJson("site/package.json");
const puppeteerPackage = site.devDependencies.puppeteer.replace(
"npm:puppeteer-core@",
"",
);
const siteRequire = createRequire(resolve("site/package.json"));
const { PUPPETEER_REVISIONS } = await import(
pathToFileURL(siteRequire.resolve("puppeteer/internal/revisions.js"))
);
const puppeteerBrowser = PUPPETEER_REVISIONS.chrome;
const platforms = new Map([
["linux-x64", "linux64"],
["macos-arm64", "mac-arm64"],
["macos-x64", "mac-x64"],
["windows-x64", "win64"],
]);
async function browser(version) {
const metadataResponse = await fetch(
`https://googlechromelabs.github.io/chrome-for-testing/${version}.json`,
);
if (!metadataResponse.ok) throw new Error(`missing Chrome for Testing ${version}`);
const metadata = await metadataResponse.json();
return Promise.all(
[...platforms].map(async ([misePlatform, chromePlatform]) => {
const url = metadata.downloads["chrome-headless-shell"].find(
({ platform }) => platform === chromePlatform,
)?.url;
const prefix = `https://storage.googleapis.com/chrome-for-testing-public/${version}/`;
if (!url?.startsWith(prefix)) throw new Error(`unexpected ${chromePlatform} URL`);
const response = await fetch(url);
if (!response.ok) throw new Error(`failed to download ${url}`);
const hash = createHash("sha256");
for await (const chunk of response.body) hash.update(chunk);
return [misePlatform, url, hash.digest("hex")];
}),