Repository navigation
5483 lines (5067 loc) · 292 KB
/
Copy pathci.yml
File metadata and controls
5483 lines (5067 loc) · 292 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
# Copyright (c) Meta Platforms, Inc. and affiliates.
# PR CI: test, build, analyze, a11y audit, and comment
# Everything runs in parallel where possible
#
# Merge queue: test + build also run on merge_group events so the
# merge queue can gate on them before landing a PR.
name: CI
# Main's nightly full interaction run and the weekly full WebKit run name
# their commit, so the main heavy lanes tell them from a release-check. A
# cut-evidence release-check names its commit too, so people reading Actions
# tell it from a monitoring release-check. Every other run keeps its default
# name.
run-name: "${{ github.event_name == 'workflow_dispatch' && inputs.operation == 'interaction-full' && format('Interaction proofs of {0}', github.sha) || github.event_name == 'workflow_dispatch' && inputs.operation == 'webkit-weekly' && format('WebKit weekly of {0}', github.sha) || github.event_name == 'workflow_dispatch' && inputs.operation == 'release-check' && inputs.evidence && format('Release evidence of {0}', github.sha) || '' }}"
on:
pull_request:
# A draft runs nothing until it is ready; an outside contributor's pull
# request runs once a maintainer labels it ci:run and pushes or reopens
# (spec:AST-030 FR17).
types: [opened, synchronize, reopened, ready_for_review]
branches: ['main', 'release/**']
merge_group:
# Explicit release checks and baseline capture share the canonical owners.
# Reviewed captures are committed through a normal pull request.
workflow_dispatch:
inputs:
operation:
description: 'Run the canonical check on exact main or a marked release branch, capture a main baseline candidate, run every Chromium interaction proof on main, or run the full WebKit interaction suite on main (webkit-weekly)'
type: choice
options: [capture, release-check, interaction-full, webkit-weekly]
default: capture
required: true
release-branch:
description: 'Exact branch to check: main before a cut, or marked release/vX.Y.Z after the bump'
type: string
required: false
release-version:
description: 'Exact X.Y.Z version recorded by the active marker; required only for a marked release branch'
type: string
required: false
expected-head:
description: 'Exact full main or release branch SHA; required for release-check'
type: string
required: false
plan-digest:
description: 'Exact SHA-256 digest recorded by the active marker; required only for a marked release branch'
type: string
required: false
recheck:
description: 'Release re-check, sent by the release re-check workflow: takes the main check lane at once'
type: boolean
default: false
evidence:
description: 'Cut evidence, sent by the pre-cut release-check and the release re-check: the run is named Release evidence of <sha>'
type: boolean
default: false
permissions: {}
# PR CI's budget is 15 minutes. Every job in this workflow that does work on a
# pull request carries `timeout-minutes: 15` (release and maintenance
# dispatches keep longer limits; macOS-only `test-native-swift` keeps 30). Split
# a job that nears the limit (shard, scope, cache); do not raise it.
#
# There is no scope job. Each job that owns tests decides its own scope in its
# first steps (.github/actions/test-scope: the PR's file list against the
# base's trusted classifier) and skips its work, reporting success, when
# nothing in its scope changed; anything undecidable runs everything.
#
# Visual, accessibility and RTL owners (pr-a11y*, visual-scout, pr-visual,
# pr-rtl*) do not run on pull request updates (a shared primitive's dependent
# audits are the one exception, below): they run for release-check,
# which the Main heavy lanes workflow (main-heavy-lanes.yml) dispatches for
# main's tip every three hours and before each weekday cut, and the release
# gate dispatches before a cut. A pull request
# opts into one suite with its label, `ci:run-visual`, `ci:run-a11y` or
# `ci:run-rtl`, applied by someone with write access (add the label, then push
# or close and reopen); run-labels decides. A main failure is fixed forward
# (spec:AST-030 FR12, FR17, DEC-7).
#
# Runners: every job uses GitHub's standard hosted runners, except the release
# path's long jobs, which take a paid larger-runner pool so pull request load
# cannot starve them: release-bump, release-merge-back on a merge-back branch
# (chore/merge-v*), and the build and audit jobs of a release-check a person or
# the release tooling dispatches (one dispatched by github-actions[bot] stays
# on standard runners, a cut-evidence run included). The release-check's short gating and join jobs
# (release-authority, pr-a11y, a11y-contracts, pr-rtl, release-check) stay on ubuntu-slim. Arm jobs stay arm and x64 stay x64 on
# both pools, so visual baselines and native builds see the same architecture.
#
# One group per pull request (a fork's branch name can repeat across forks),
# so a new push cancels the superseded run. Merge-queue entries and dispatches
# get their own groups and are never cancelled by a pull request. Main's
# nightly full interaction run has a group of its own, so it never replaces a
# waiting release-check. The
# release-checks main-heavy-lanes.yml and the release re-check dispatch (as
# github-actions[bot]) share one group: one runs and the newest waits. The
# weekly full WebKit run (webkit-weekly.yml) has a group of its own, so a
# release-check never replaces it while it waits. Only a
# release re-check cancels the running check; it checks main's tip in full, so
# the cancelled check's commits stay covered (spec:AST-030 FR23).
#
# A release-check runs its browser owners in wide shards that all start at
# once, so a full check takes minutes; pull requests keep their narrower legs
# (spec:AST-030 FR23, DEC-17).
# A pull request's newer push cancels its older run. A re-run takes a group of
# its own, so re-running an older head never cancels the run for the pull
# request's current head.
concurrency:
group: "ci-${{ github.event.pull_request.number && (github.run_attempt == '1' && github.event.pull_request.number || format('{0}-rerun-{1}', github.event.pull_request.number, github.run_id)) || github.event.merge_group.head_sha || (github.event_name == 'workflow_dispatch' && inputs.operation == 'interaction-full' && 'interaction-full') || (github.event_name == 'workflow_dispatch' && inputs.operation == 'webkit-weekly' && github.ref == 'refs/heads/main' && 'webkit-weekly') || (github.event_name == 'workflow_dispatch' && github.triggering_actor == 'github-actions[bot]' && 'main-heavy-lanes') || github.run_id }}"
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' || (inputs.recheck && github.triggering_actor == 'github-actions[bot]') }}
jobs:
# Admit the whole run under CI's ceiling of about ninety concurrent jobs
# (spec:AST-030 FR16, DEC-16), in two jobs (slim on a pull request, standard
# on a dispatch): `room` waits until the
# Storybook build has room and starts it, and `admission` waits for the rest
# of the run unless it already fit. Every job here but build-storybook and
# ci-full needs admission, directly or through the jobs it needs, so a
# waiting run holds only its one waiting job and a build that found room.
# Like every other job both skip for a draft or an unlabelled fork, which run
# no CI (FR17); the required contexts still report through their own status
# rules. Neither fails, so each job's own condition decides exactly as before,
# except for a pull-request run CI is full for, which ci-full stops. `peak` is
# the most jobs a run holds at once: a release-check's from its legs, a pull
# request's from its measured most plus the suites its labels opt into
# (ci-admission.test.mjs). A pull request waits at most twelve minutes in all,
# inside the fifteen-minute job budget.
room:
# Finds room for the run's first job, its Storybook build, and records
# whether the whole run fit on that look. A pull-request run that finds no
# room waits, up to its longest wait.
if: (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run')))
# A dispatch (a main or release check) may wait longer than a slim job's
# fifteen-minute cap, so it waits on a standard runner (spec:AST-030 DEC-16).
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-latest' || 'ubuntu-slim' }}
timeout-minutes: ${{ github.event_name == 'workflow_dispatch' && 130 || 15 }}
permissions:
actions: read
contents: read
outputs:
fits: ${{ steps.room.outputs.fits }}
full: ${{ steps.room.outputs.full }}
started: ${{ steps.room.outputs.started }}
looks: ${{ steps.room.outputs.looks }}
next: ${{ steps.room.outputs.next }}
attempt: ${{ github.run_attempt }}
steps:
- name: Check out the admission action
uses: actions/checkout@v7
with:
sparse-checkout: .github/actions/ci-admission
path: .ci-admission
persist-credentials: false
- name: Wait for room under the CI ceiling
id: room
uses: ./.ci-admission/.github/actions/ci-admission
continue-on-error: true
timeout-minutes: ${{ github.event_name == 'workflow_dispatch' && 125 || 13 }}
with:
phase: room
peak: ${{ github.event_name == 'workflow_dispatch' && 39 || 17 }}
main: ${{ github.event_name == 'workflow_dispatch' }}
max-wait-minutes: ${{ github.event_name == 'workflow_dispatch' && 120 || 12 }}
max-looks: ${{ github.event_name == 'workflow_dispatch' && 30 || 2 }}
label-legs: '{"ci:run-visual": 6, "ci:run-rtl": 9, "ci:run-a11y": 5}'
# A run stopped for no room fails here, so a re-run of its failed jobs
# waits again from this job, not from this look's result.
- name: Stop when CI is full
if: steps.room.outputs.full == 'true'
run: |
echo "::error title=CI is full::CI is full: re-run all jobs or push later. The run found no room under the CI ceiling."
exit 1
admission:
needs: [room]
if: (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run')))
# A dispatch (a main or release check) may wait longer than a slim job's
# fifteen-minute cap, so it waits on a standard runner (spec:AST-030 DEC-16).
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-latest' || 'ubuntu-slim' }}
timeout-minutes: ${{ github.event_name == 'workflow_dispatch' && 130 || 15 }}
permissions:
actions: read
contents: read
outputs:
full: ${{ steps.rest.outputs.full }}
steps:
- name: Stop when CI is full
if: needs.room.outputs.full == 'true'
run: |
echo "::error title=CI is full::CI is full: re-run all jobs or push later. The run found no room under the CI ceiling."
exit 1
- name: Check out the admission action
uses: actions/checkout@v7
with:
sparse-checkout: .github/actions/ci-admission
path: .ci-admission
persist-credentials: false
- name: Wait for room under the CI ceiling
id: rest
uses: ./.ci-admission/.github/actions/ci-admission
continue-on-error: true
timeout-minutes: ${{ github.event_name == 'workflow_dispatch' && 125 || 13 }}
# A re-run of this job alone reuses an earlier attempt's room
# outputs, whose clock has run out, so it waits for the whole run on
# its own clock.
with:
phase: ${{ needs.room.outputs.attempt == github.run_attempt && 'rest' || 'whole' }}
fits: ${{ needs.room.outputs.fits }}
started: ${{ needs.room.outputs.started }}
looks: ${{ needs.room.outputs.looks }}
next: ${{ needs.room.outputs.next }}
peak: ${{ github.event_name == 'workflow_dispatch' && 39 || 17 }}
main: ${{ github.event_name == 'workflow_dispatch' }}
max-wait-minutes: ${{ github.event_name == 'workflow_dispatch' && 120 || 12 }}
max-looks: ${{ github.event_name == 'workflow_dispatch' && 30 || 2 }}
label-legs: '{"ci:run-visual": 6, "ci:run-rtl": 9, "ci:run-a11y": 5}'
# A pull-request run that waited its longest while another started at
# its longest wait is turned away (spec:AST-030 DEC-16): ci-full stops
# the run, and the jobs behind admission skip.
- name: Stop when CI is full at the longest wait
if: steps.rest.outputs.full == 'true'
run: |
echo "::error title=CI is full::CI is full: re-run all jobs or push later. The run found no room under the CI ceiling."
exit 1
# Stops a pull-request run that CI is full for, with the reason, so it builds
# nothing: the run waited its longest while another started at its longest
# wait, or a run that waited longer still waits. It cancels the run when its
# token can. A fork's read-only token cannot; then
# admission fails, the jobs behind it skip or stop, and build, test and
# docsite-test fail with the same reason, never skip or pass.
ci-full:
needs: [room, admission]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (github.event_name == 'pull_request' && !cancelled() && (needs.room.outputs.full == 'true' || needs.admission.outputs.full == 'true')) }}
runs-on: ubuntu-slim
timeout-minutes: 5
permissions:
# actions: write lets the run cancel itself.
actions: write
steps:
- name: Stop, CI is full
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "::error title=CI is full::CI is full: re-run all jobs or push later. The run found no room under the CI ceiling."
gh run cancel "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" || true
exit 1
maintenance-request:
needs: [admission]
if: github.event_name == 'workflow_dispatch' && inputs.operation == 'capture'
runs-on: ubuntu-slim
steps:
- name: Require explicit main-branch maintenance
run: |
set -eu
if [ "$GITHUB_REF" != 'refs/heads/main' ]; then
echo 'Baseline maintenance must be dispatched from main.'
exit 1
fi
# The weekly full WebKit run (webkit-weekly.yml, spec:AST-030 DEC-24) runs
# only from main, like baseline maintenance.
webkit-weekly-request:
needs: [admission]
if: github.event_name == 'workflow_dispatch' && inputs.operation == 'webkit-weekly'
runs-on: ubuntu-slim
steps:
- name: Require main
run: |
set -eu
if [ "$GITHUB_REF" != 'refs/heads/main' ]; then
echo 'The weekly full WebKit run must be dispatched from main.'
exit 1
fi
# The release authority for an explicit release check: the active release
# marker, or the exact main pre-cut head, that the canonical checks bind to.
# Pull requests never run it: a release-branch pull request validates its
# own bump in release-bump, and every test job decides its own scope.
release-authority:
needs: [admission]
if: github.event_name == 'workflow_dispatch' && inputs.operation == 'release-check'
runs-on: ubuntu-slim
permissions:
contents: read
outputs:
release_branch: ${{ steps.release-authority.outputs.branch || steps.main-authority.outputs.branch }}
release_head: ${{ steps.release-authority.outputs.head || steps.main-authority.outputs.head }}
release_plan_digest: ${{ steps.release-authority.outputs.planDigest }}
steps:
# Marks a release re-check, so the re-check workflow can tell its runs
# from main's hourly check of the same commit.
- name: Mark a release re-check
if: inputs.recheck
run: echo 'Release re-check'
# Full history only where the release-branch validator reads it (tags,
# the cut commit, release diffs). A main pre-cut check fetches current
# main on top, so 50 commits are plenty there; the full clone took over
# a minute.
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
fetch-depth: ${{ inputs['release-branch'] != 'main' && '0' || '50' }}
- name: Fetch active release refs
if: inputs['release-branch'] != 'main'
run: git fetch origin '+refs/heads/release/v*:refs/remotes/origin/release/v*'
- name: Validate active release authority
id: release-authority
if: inputs['release-branch'] != 'main'
env:
RELEASE_BRANCH: ${{ github.event_name == 'workflow_dispatch' && inputs['release-branch'] || github.base_ref }}
RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs['release-version'] || '' }}
EXPECTED_HEAD: ${{ github.event_name == 'workflow_dispatch' && inputs['expected-head'] || github.event.pull_request.head.sha }}
PLAN_DIGEST: ${{ github.event_name == 'workflow_dispatch' && inputs['plan-digest'] || '' }}
RECEIPT_HEAD: ${{ github.event_name == 'workflow_dispatch' && github.sha || github.event.pull_request.head.sha }}
REF_NAME: ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.base_ref }}
MODE: ${{ github.event_name == 'workflow_dispatch' && 'check' || 'pr' }}
run: |
set -eu
TRUSTED_POLICY="$RUNNER_TEMP/release-policy"
mkdir -p "$TRUSTED_POLICY/scripts/release" "$TRUSTED_POLICY/scripts/lib"
git show "origin/$RELEASE_BRANCH:scripts/release/active-release.mjs" \
> "$TRUSTED_POLICY/scripts/release/active-release.mjs"
git show "origin/$RELEASE_BRANCH:scripts/release/release-paths.mjs" \
> "$TRUSTED_POLICY/scripts/release/release-paths.mjs"
git show "origin/$RELEASE_BRANCH:scripts/lib/workspace-globs.mjs" \
> "$TRUSTED_POLICY/scripts/lib/workspace-globs.mjs"
VALIDATOR="$TRUSTED_POLICY/scripts/release/active-release.mjs"
ACTIVE_BRANCHES=$(node "$VALIDATOR" list-active --root "$GITHUB_WORKSPACE")
ARGS=(
validate
--root "$GITHUB_WORKSPACE"
--mode "$MODE"
--release-branch "$RELEASE_BRANCH"
--expected-head "$EXPECTED_HEAD"
--checkout-sha "$RECEIPT_HEAD"
--ref-name "$REF_NAME"
--active-branches "$ACTIVE_BRANCHES"
)
if [ "$MODE" = check ]; then
ARGS+=(--release-version "$RELEASE_VERSION" --plan-digest "$PLAN_DIGEST")
REMOTE_HEAD=$(git ls-remote origin "refs/heads/$RELEASE_BRANCH" | awk '{print $1}')
ARGS+=(--remote-head "$REMOTE_HEAD")
fi
node "$VALIDATOR" "${ARGS[@]}"
- name: Validate exact main pre-cut authority
id: main-authority
if: >-
github.event_name == 'workflow_dispatch' &&
inputs.operation == 'release-check' &&
inputs['release-branch'] == 'main'
env:
EXPECTED_HEAD: ${{ inputs['expected-head'] }}
run: |
set -eu
if [ "$GITHUB_REF" != 'refs/heads/main' ]; then
echo 'Main pre-cut checks must be dispatched from main.'
exit 1
fi
if ! [[ "$EXPECTED_HEAD" =~ ^[0-9a-f]{40}$ ]]; then
echo 'Main pre-cut checks require an exact full expected head.'
exit 1
fi
if [ "$GITHUB_SHA" != "$EXPECTED_HEAD" ]; then
echo 'Checked-out main commit does not match expected head.'
exit 1
fi
REMOTE_HEAD=$(git ls-remote origin refs/heads/main | awk '{print $1}')
if [ -z "$REMOTE_HEAD" ]; then
echo 'Current main head is unavailable.'
exit 1
fi
git fetch --no-tags origin "$REMOTE_HEAD"
if ! git merge-base --is-ancestor "$EXPECTED_HEAD" "$REMOTE_HEAD"; then
echo 'Checked main commit is not an ancestor of current main.'
exit 1
fi
printf 'branch=main\nhead=%s\n' "$EXPECTED_HEAD" >> "$GITHUB_OUTPUT"
# Generated-only version PR lane. It validates release bookkeeping in a few
# minutes; the merged branch head receives one full visual/a11y/RTL projection.
release-bump:
needs: [admission]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (github.event_name == 'pull_request' && startsWith(github.base_ref, 'release/v')) }}
runs-on: 2-core-ubuntu-arm
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Fetch release refs, base, main, and trusted release policy
run: |
git fetch origin '+refs/heads/release/v*:refs/remotes/origin/release/v*'
git fetch origin "${{ github.base_ref }}" --depth=200
git fetch origin 'main:refs/remotes/origin/main'
TRUSTED_POLICY="$RUNNER_TEMP/release-policy"
mkdir -p "$TRUSTED_POLICY/scripts/release" "$TRUSTED_POLICY/scripts/lib"
git show "origin/${{ github.base_ref }}:scripts/release/active-release.mjs" \
> "$TRUSTED_POLICY/scripts/release/active-release.mjs"
git show "origin/${{ github.base_ref }}:scripts/release/release-paths.mjs" \
> "$TRUSTED_POLICY/scripts/release/release-paths.mjs"
git show "origin/${{ github.base_ref }}:scripts/lib/workspace-globs.mjs" \
> "$TRUSTED_POLICY/scripts/lib/workspace-globs.mjs"
- name: Setup Node and pnpm
uses: ./.github/actions/setup
- name: Require generated release bookkeeping or a proven test repair
run: |
node "$RUNNER_TEMP/release-policy/scripts/release/active-release.mjs" validate-diff \
--root "$GITHUB_WORKSPACE" \
--base "origin/${{ github.base_ref }}" \
--head "${{ github.event.pull_request.head.sha }}" \
--main-ref origin/main
- name: Validate branch, head, plan, versions, and Changeset consumption
run: |
set -eu
VALIDATOR="$RUNNER_TEMP/release-policy/scripts/release/active-release.mjs"
ACTIVE_BRANCHES=$(node "$VALIDATOR" list-active --root "$GITHUB_WORKSPACE")
node "$VALIDATOR" validate \
--root "$GITHUB_WORKSPACE" \
--mode pr \
--release-branch "${{ github.base_ref }}" \
--expected-head "${{ github.event.pull_request.head.sha }}" \
--checkout-sha "${{ github.event.pull_request.head.sha }}" \
--ref-name "${{ github.base_ref }}" \
--active-branches "$ACTIVE_BRANCHES" \
| tee release-bump-receipt.json
- name: Validate generated changelogs and codemods
run: |
pnpm check:changesets
node scripts/format-changelogs.mjs --check
VERSION=$(node -p "require('./.release/active.json').version")
pnpm exec vitest run \
"packages/cli/assets/codemods/transforms/v${VERSION}/__tests__" \
packages/cli/assets/codemods/__tests__/registry.test.mjs
- name: Upload exact-head release bump receipt
uses: actions/upload-artifact@v7
with:
name: release-bump-receipt-${{ github.event.pull_request.head.sha }}
path: release-bump-receipt.json
retention-days: 30
release-merge-back:
name: release-merge-back
needs: [admission]
# Only a release merge-back branch has anything for this job to validate,
# so every other pull request skips it before the full-history checkout
# below (minutes on a larger runner). The step after the checkout still
# checks the exact branch contract, and required lint validates merge-back
# branches with the same trusted policy.
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (github.event_name == 'pull_request' && github.base_ref == 'main' && startsWith(github.head_ref, 'chore/merge-v') && endsWith(github.head_ref, '-to-main')) }}
# Match the stable publisher so byte parity covers the same packed manifest.
runs-on: ${{ startsWith(github.head_ref, 'chore/merge-v') && '4-core-ubuntu' || 'ubuntu-latest' }}
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout merge-back candidate
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Detect the release-merge-back branch contract
id: release-merge-back-scope
env:
MERGE_BRANCH: ${{ github.head_ref }}
run: |
set -eu
case "$MERGE_BRANCH" in
chore/merge-v*-to-main)
VERSION=${MERGE_BRANCH#chore/merge-v}
VERSION=${VERSION%-to-main}
printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'
echo 'required=true' >> "$GITHUB_OUTPUT"
;;
*)
echo 'required=false' >> "$GITHUB_OUTPUT"
;;
esac
- name: Fetch release authority and trusted validator
if: steps.release-merge-back-scope.outputs.required == 'true'
run: |
set -eu
git fetch origin main 'refs/tags/v*:refs/tags/v*' '+refs/heads/release/v*:refs/remotes/origin/release/v*'
TRUSTED_POLICY="$RUNNER_TEMP/release-policy"
mkdir -p "$TRUSTED_POLICY/scripts/release" "$TRUSTED_POLICY/scripts/lib"
git show origin/main:scripts/release/active-release.mjs \
> "$TRUSTED_POLICY/scripts/release/active-release.mjs"
git show origin/main:scripts/release/release-paths.mjs \
> "$TRUSTED_POLICY/scripts/release/release-paths.mjs"
git show origin/main:scripts/lib/workspace-globs.mjs \
> "$TRUSTED_POLICY/scripts/lib/workspace-globs.mjs"
- name: Validate published bytes and consumed-only Changeset merge-back
if: steps.release-merge-back-scope.outputs.required == 'true'
env:
MERGE_BRANCH: ${{ github.head_ref }}
run: |
set -eu
VERSION=${MERGE_BRANCH#chore/merge-v}
VERSION=${VERSION%-to-main}
printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'
VALIDATOR="$RUNNER_TEMP/release-policy/scripts/release/active-release.mjs"
ACTIVE_BRANCHES=$(node "$VALIDATOR" list-active --root "$GITHUB_WORKSPACE")
# The merge commit's first parent: the main commit it was built on.
# A main that moved since would read its new files as deletions.
node "$VALIDATOR" validate-merge-back \
--root "$GITHUB_WORKSPACE" \
--base HEAD^1 \
--release-ref "v$VERSION" \
--active-branches "$ACTIVE_BRANCHES"
- name: Setup Node and pnpm for published-byte verification
if: steps.release-merge-back-scope.outputs.required == 'true'
uses: ./.github/actions/setup
with:
install: 'false'
- name: Verify immutable tag package bytes match npm
if: steps.release-merge-back-scope.outputs.required == 'true'
env:
MERGE_BRANCH: ${{ github.head_ref }}
run: |
set -eu
VERSION=${MERGE_BRANCH#chore/merge-v}
VERSION=${VERSION%-to-main}
TAG_ROOT="$RUNNER_TEMP/release-tag"
git worktree add --detach "$TAG_ROOT" "v$VERSION"
cd "$TAG_ROOT"
pnpm install --frozen-lockfile
pnpm build
node scripts/release/verify-published.mjs --version "$VERSION"
# Required docsite owner (historical check name, AST-030 FR7): one job
# builds the packages and the docsite production build once, then runs the
# docsite data tests and the browser contracts side by side on that build.
# The browser contracts matter because the playground preview runs
# user-authored code in a sandboxed iframe whose only tie to the page is a
# nonce-attested MessagePort handshake
# (apps/docsite/src/app/playground/previewChannel.ts): jsdom can drive that
# state machine but cannot prove the integration (the frame's real `load`
# event, a replaced document torn down and its successor given the current
# code and theme, nothing sent to a document the code navigated to), and
# only the production build has the opaque-origin sandbox at all. The
# spec-only lane validates spec records here, where it always has.
# Whether this push defers the heavy suites (spec:AST-030 FR20, DEC-14): a
# push to a ready agent-lane pull request (the shared agent login), in its
# first attempt, runs only the fast checks (lint, typecheck). The heavy jobs
# skip, and the required checks fail with the reason until the pull request
# is marked ready or reopened, or its run is re-run in full before merge
# (any later attempt runs everything). A spec-only change is never deferred.
# Every other run skips this job at once, so it delays nothing; every job
# below it carries a status check, so the skip never skips them. It needs
# only room, beside admission, so the Storybook build it gates still starts
# with room (spec:AST-030 FR16), and like room it holds one slim job.
heavy-ci:
needs: [room]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && ((github.event_name == 'pull_request' && github.event.action == 'synchronize' && github.run_attempt == '1' && contains(fromJSON('["cixzhang"]'), github.event.pull_request.user.login) && github.base_ref == 'main' && !startsWith(github.head_ref, 'chore/merge-v')) && needs.room.outputs.full != 'true') }}
runs-on: ubuntu-slim
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
deferred: ${{ steps.decide.outputs.deferred }}
steps:
- name: Check out the scope action
uses: actions/checkout@v7
with:
sparse-checkout: .github/actions/test-scope
path: .test-scope
persist-credentials: false
- name: Decide this push's scope
id: scope
uses: ./.test-scope/.github/actions/test-scope
- name: Defer the heavy suites unless the change is spec-only or Lab-only
id: decide
env:
SPEC_ONLY: ${{ steps.scope.outputs.spec_only }}
LAB_ONLY: ${{ steps.scope.outputs.lab_only }}
run: |
# A Lab-only change runs only fast work, so nothing is deferred (FR26).
if [ "$SPEC_ONLY" = true ] || [ "$LAB_ONLY" = true ]; then
echo 'deferred=false' >> "$GITHUB_OUTPUT"
exit 0
fi
echo 'deferred=true' >> "$GITHUB_OUTPUT"
echo "::notice title=Heavy CI deferred::Heavy CI deferred: runs when marked ready and before merge. Mark the pull request ready, reopen it, or re-run all jobs of this run to run everything."
echo "Heavy CI deferred: runs when marked ready and before merge." >> "$GITHUB_STEP_SUMMARY"
docsite-build:
needs: [room, admission, heavy-ci]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (github.event_name != 'workflow_dispatch' && !startsWith(github.base_ref, 'release/v') && !cancelled() && needs.heavy-ci.outputs.deferred != 'true' && needs.room.outputs.full != 'true' && needs.admission.outputs.full != 'true') }}
# The Next.js production build uses every core it gets.
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
# Placeholder docs-prompt origins: the build and its browser contracts
# exercise the configured prompt. Deployed values live only in the
# deployment environment, never in this repository.
env:
NEXT_PUBLIC_INTERNAL_DOCS_ORIGIN: https://docs.example.test
NEXT_PUBLIC_DOCS_ACCESS_CHECK_ORIGIN: https://probe.example.test
outputs:
lab_only: ${{ steps.scope.outputs.lab_only }}
# The browser contracts apply: the docsite was built, and the change is
# shared infrastructure or more than component tests, stories, spec
# records and changesets.
browser: ${{ steps.browser.outputs.applies }}
steps:
# This job decides its own scope (no separate scope job): the step
# reads the PR's file list and the base's trusted classifier, and
# everything is in scope when it cannot decide. `touches` is false only
# when every changed file outside the docsite app is a test, a story
# (or Storybook file), a spec record, a changeset, or an a11y-spec or
# interaction-harness file. The browser contracts still run when the
# classifier calls the change shared infrastructure (`full_suite`: the
# harnesses, Storybook config, a shared test), as for every owner
# (spec:AST-030 FR13).
- name: Check out the scope action
uses: actions/checkout@v7
with:
sparse-checkout: .github/actions/test-scope
path: .test-scope
persist-credentials: false
- name: Decide this job's scope
id: scope
uses: ./.test-scope/.github/actions/test-scope
with:
paths: '^(?:apps/docsite/|(?!(?:.*/)?__tests__/|.*\.(?:test|spec)\.[cm]?[jt]sx?$|apps/storybook/|.*\.stories\.[jt]sx?$|docs/specs/|\.changeset/|internal/a11y-spec/|internal/interaction-harness/))'
- name: 'Lab-only: Lab tests + Storybook build'
if: steps.scope.outputs.lab_only == 'true'
run: |
echo "::notice title=Lab-only::Lab-only: Lab tests + Storybook build. This job's work is not required for a change only to the Lab package (spec:AST-030 FR26)."
- name: Skip docsite work for Node tooling
if: steps.scope.outputs.tooling_only == 'true'
run: echo "Node-tooling PR — docsite generation is not an owned check"
- uses: actions/checkout@v7
if: steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
# Spec validation imports the canonical CLI discovery path, so the
# lightweight docs path needs workspace dependencies too.
- uses: ./.github/actions/setup
if: steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
- name: Validate spec records
if: (steps.scope.outputs.spec_only == 'true' || steps.scope.outputs.decided != 'true')
run: node scripts/check-knowledge.mjs
- name: Build docsite workspace dependencies
if: steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
run: |
pnpm -F @astryxdesign/core build
pnpm --workspace-concurrency=4 \
-F @astryxdesign/lab \
-F @astryxdesign/charts \
-F @astryxdesign/richtext \
-F @astryxdesign/vega \
-F @astryxdesign/theme-neutral \
-F @astryxdesign/theme-matcha \
-F @astryxdesign/theme-stone \
-F @astryxdesign/theme-gothic \
-F @astryxdesign/theme-chocolate \
-F @astryxdesign/theme-y2k \
-F @astryxdesign/theme-butter \
build
# `build` generates the docsite data first, so the data tests below run
# on the same generated files the production build used.
- name: Build the docsite
if: steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
run: pnpm -F @astryxdesign/docsite build
- name: Decide whether the browser contracts apply
id: browser
run: echo "applies=${{ (steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true') && (steps.scope.outputs.touches == 'true' || steps.scope.outputs.full_suite == 'true') }}" >> "$GITHUB_OUTPUT"
# docsite-browser serves this build, never one of its own. The commit
# stamp lets it check that the build is of the commit it checked out.
- name: Stamp the build with its commit
if: steps.browser.outputs.applies == 'true'
run: |
git rev-parse HEAD > apps/docsite/.next/BUILD_COMMIT
# The sizes behind the upload, largest first, and what it leaves out.
du -sh apps/docsite/.next/* apps/docsite/.next/server/* apps/docsite/src/generated apps/docsite/public/monaco apps/docsite/public/vendor 2>/dev/null | sort -rh | head -15
for kind in '*.map' '*.nft.json'; do
echo "left out, $kind: $(find apps/docsite/.next -path apps/docsite/.next/cache -prune -o -name "$kind" -type f -print0 | du -ch --files0-from=- 2>/dev/null | tail -1)"
done
# Everything `next start` reads that the build generated and the
# repository ignores: the server and static output, the generated data
# and theme, the ShadCN registry and the playground's vendored assets.
# Not the compile
# cache, the build traces, source maps or file traces (*.nft.json, for
# a standalone deployment), which serving never reads.
- name: Upload the docsite build
if: steps.browser.outputs.applies == 'true'
uses: actions/upload-artifact@v7
with:
name: docsite-build
path: |
apps/docsite/.next/
!apps/docsite/.next/cache/
!apps/docsite/.next/trace
!apps/docsite/.next/trace-build
!apps/docsite/.next/diagnostics/
!apps/docsite/.next/types/
!apps/docsite/.next/**/*.map
!apps/docsite/.next/**/*.nft.json
apps/docsite/src/generated/
apps/docsite/src/themes/astryx.js
apps/docsite/src/themes/astryx.css
apps/docsite/public/playground-types.json
apps/docsite/public/monaco/
apps/docsite/public/vendor/
apps/docsite/public/shadcn/
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 1
# HTML, RSC payloads and JavaScript compress well; the default level
# takes most of that at a fraction of the highest level's time.
compression-level: 6
# The built API surface the data tests read for public-hook coverage.
# The CLI and build packages, which the dependency build above skips,
# are listed as not built; the test names them as excluded and fails on
# any other package missing from the surface.
- name: Write the docsite's API surface
if: steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
run: node scripts/api-report/cli.mjs --surface .api-surface-docsite
- name: test-docsite (data tests)
if: steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
env:
# Relative to the repository root.
ASTRYX_API_SURFACE: .api-surface-docsite
run: pnpm -F @astryxdesign/docsite test
# A docsite-only change skips test-node, and the registry generator
# lives in the docsite.
- name: Verify the hidden production registry gate
if: steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true' && steps.scope.outputs.lab_only != 'true'
run: node internal/shadcn-registry/verify-production-gate.mjs
# The docsite browser contracts, against the build docsite-build uploaded:
# the same commit, never rebuilt. A planner-driven matrix (one leg today;
# playwright-legs.mjs splits the spec files by recorded time, so a second
# leg is a one-line change).
docsite-browser:
name: docsite-browser (${{ matrix.leg }}/${{ strategy.job-total }})
needs: [docsite-build]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (!cancelled() && needs.docsite-build.result == 'success' && needs.docsite-build.outputs.browser == 'true') }}
strategy:
fail-fast: false
matrix:
leg: [1]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
env:
NEXT_PUBLIC_INTERNAL_DOCS_ORIGIN: https://docs.example.test
NEXT_PUBLIC_DOCS_ACCESS_CHECK_ORIGIN: https://probe.example.test
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup
- name: Install Playwright
run: npx playwright install chromium
- name: Download the docsite build
uses: actions/download-artifact@v8
with:
name: docsite-build
path: apps/docsite
- name: Require the build of this commit
run: |
built=$(cat apps/docsite/.next/BUILD_COMMIT)
here=$(git rev-parse HEAD)
echo "build: $built; checkout: $here"
test "$built" = "$here"
- name: Plan this leg's browser contracts
id: plan
run: |
node .github/scripts/playwright-legs.mjs plan \
--key docsite --config apps/docsite/playwright.config.ts \
--leg ${{ matrix.leg }} --of ${{ strategy.job-total }} \
--out "$RUNNER_TEMP/docsite-leg.txt"
- name: test-docsite-e2e (browser contracts)
if: steps.plan.outputs.files != '0'
run: pnpm test:docsite-browser --test-list "$RUNNER_TEMP/docsite-leg.txt"
# Only a leg whose contracts ran has evidence; one that stopped before
# them reports that failure alone.
- name: Upload docsite browser evidence
if: always() && steps.plan.outcome == 'success' && steps.plan.outputs.files != '0'
uses: actions/upload-artifact@v7
with:
name: docsite-browser-evidence-${{ matrix.leg }}
path: test-results/docsite/
if-no-files-found: error
retention-days: 7
# The required docsite context (spec:AST-030 FR7): passes only when the
# build passed and every browser leg passed, or the build's scope found no
# browser contract to run. A skipped or cancelled leg otherwise fails it.
docsite-test:
needs: [room, admission, heavy-ci, docsite-build, docsite-browser]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name != 'workflow_dispatch' && !startsWith(github.base_ref, 'release/v') && !cancelled()) }}
runs-on: ubuntu-slim
permissions: {}
steps:
- name: Require a branch in this repository or the ci:run label
if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run')) }}
run: |
echo "::error::Waiting for a maintainer's ci:run label: an outside contributor's pull request runs CI once a maintainer adds it (spec:AST-030 FR17)."
exit 1
- name: Stop when CI is full
if: needs.room.outputs.full == 'true' || needs.admission.outputs.full == 'true'
run: |
echo "::error title=CI is full::CI is full: re-run all jobs or push later. The run found no room under the CI ceiling."
exit 1
# An agent lane's push defers the heavy suites (spec:AST-030 FR20): the
# required check fails with the reason, never passes as skipped.
- name: Heavy CI deferred
if: needs.heavy-ci.outputs.deferred == 'true'
run: |
echo "::error::Heavy CI deferred: runs when marked ready and before merge (spec:AST-030 FR20)."
exit 1
- name: 'Lab-only: Lab tests + Storybook build'
if: needs.docsite-build.outputs.lab_only == 'true'
run: |
echo "::notice title=Lab-only::Lab-only: Lab tests + Storybook build. This job's work is not required for a change only to the Lab package (spec:AST-030 FR26)."
- name: Require the docsite build and its browser contracts
env:
BUILD_RESULT: ${{ needs.docsite-build.result }}
BROWSER: ${{ needs.docsite-build.outputs.browser }}
BROWSER_RESULT: ${{ needs.docsite-browser.result }}
run: |
echo "docsite-build: $BUILD_RESULT; browser contracts apply: ${BROWSER:-unknown}; docsite-browser: $BROWSER_RESULT"
[ "$BUILD_RESULT" = success ] || exit 1
if [ "$BROWSER" = true ]; then
[ "$BROWSER_RESULT" = success ] || exit 1
else
[ "$BROWSER" = false ] && [ "$BROWSER_RESULT" = skipped ] || exit 1
fi
# Tests run as two parallel lanes joined by `test` below.
#
# They used to be one `pnpm test` on a 2-core runner, which ran both Vitest
# projects back to back and was terminated around 20 minutes with no summary
# — no assertion output, so nothing said which suite was at fault. `main` was
# already at ~19m15, so the budget, not any one test, was the defect.
#
# The projects are independent by construction (see vitest.config.ts: `ui` is
# jsdom + StyleX, `node` is everything else), so running them as two jobs
# costs no duplicate execution. They do NOT carry half the work each: on this
# CI, `ui` is ~5x the heavier lane, so it takes the larger runner.
#
# The earlier local figure that put `node` ahead (570s vs 244s) was not a
# comparable measurement — only the node run was bounded to a small worker
# pool, and node is the project whose cost is per-assertion CLI subprocesses.
#
# The repo-wide guardrail steps ride `test-ui` (~18s of it).
#
# `test` remains a job, so the required check of that name still exists and
# still reports for every PR. This mirrors the `build` join over
# build-storybook + typecheck below.
test-ui:
name: test-ui (${{ matrix.shard }}/${{ strategy.job-total }})
needs: [admission, heavy-ci]
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft && (github.event.pull_request.head.repo.full_name == github.repository || contains(github.event.pull_request.labels.*.name, 'ci:run'))) && (!cancelled() && needs.heavy-ci.outputs.deferred != 'true' && (needs.admission.result == 'success' && (github.event_name != 'workflow_dispatch'))) }}
# One runner ran the whole UI project in ~16 minutes and set the PR's wall
# time. Vitest's --shard gives each leg an equal share of the files; the
# two slowest files (~3.5 minutes each) bound a leg, so more legs stop
# paying. `test` joins the legs: the job result is success only when
# every leg passed.
strategy:
fail-fast: false
# A pull request or merge-queue entry runs at most two legs at a time,
# so many open pull requests cannot flood the organization's shared
# runner pool; the legs still all run, in two waves.
max-parallel: 2
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: read
steps:
# This job decides its own scope (no separate scope job): the step
# reads the PR's file list and the base's trusted classifier, and
# everything is in scope when it cannot decide.
- name: Check out the scope action
uses: actions/checkout@v7
with:
sparse-checkout: .github/actions/test-scope
path: .test-scope
persist-credentials: false
- name: Decide this job's scope
id: scope
uses: ./.test-scope/.github/actions/test-scope
- name: 'Lab-only: Lab tests + Storybook build'
if: steps.scope.outputs.lab_only == 'true'
run: |
echo "::notice title=Lab-only::Lab-only: Lab tests + Storybook build. This job runs the Lab package's tests, the source-reading guard tests and the repository guards (spec:AST-030 FR26)."
- name: Skip heavy work for a specialized lane
if: steps.scope.outputs.docsite_only == 'true' || steps.scope.outputs.spec_only == 'true' || steps.scope.outputs.tooling_only == 'true'
run: echo "Specialized PR lane — skipping unrelated test suite"
# A spec-only change runs no other test here, so it runs the tests that
# read a record it changes: a record such a test reads can break it, and
# the break would otherwise show first on main (spec:AST-030 FR2). One
# leg runs this project's readers; the lister needs only git and Node,
# so a change no test here reads sets up nothing.
- name: Check out the change for the tests that read its records
if: matrix.shard == 1 && steps.scope.outputs.spec_only == 'true'
uses: actions/checkout@v7
with:
fetch-depth: 2
- name: List the tests that read a changed record
id: spec-readers
if: matrix.shard == 1 && steps.scope.outputs.spec_only == 'true'
run: |
node .github/scripts/fs-guard-tests.mjs --spec-readers --project ui --changed HEAD^1 > "$RUNNER_TEMP/spec-readers.txt"
echo "UI tests that read a changed record:"
cat "$RUNNER_TEMP/spec-readers.txt"
if [ -s "$RUNNER_TEMP/spec-readers.txt" ]; then
echo 'any=true' >> "$GITHUB_OUTPUT"
else
echo 'any=false' >> "$GITHUB_OUTPUT"
fi
- uses: ./.github/actions/setup
if: steps.spec-readers.outputs.any == 'true'
# No --passWithNoTests: the list holds this project's tests only, so a
# run that finds none is a lister fault.
- name: Run the UI tests that read a changed record
if: steps.spec-readers.outputs.any == 'true'
run: |
mapfile -t READERS < "$RUNNER_TEMP/spec-readers.txt"
pnpm test:ui "${READERS[@]}"
# Two commits: a pull request's merge commit and its base parent, the
# diff a scoped run follows.
- uses: actions/checkout@v7
if: steps.scope.outputs.docsite_only != 'true' && steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true'
with:
fetch-depth: 2
- uses: ./.github/actions/setup
if: steps.scope.outputs.docsite_only != 'true' && steps.scope.outputs.spec_only != 'true' && steps.scope.outputs.tooling_only != 'true'
- name: Check knowledge schema history
if: >-
matrix.shard == 1 &&
github.event_name == 'pull_request' &&