-
Notifications
You must be signed in to change notification settings - Fork 4
1622 lines (1558 loc) · 79.5 KB
/
Copy pathci.yml
File metadata and controls
1622 lines (1558 loc) · 79.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
# A merge queue runs each candidate on a gh-readonly-queue/* ref. Every
# verification job already falls through to the ci-scope outputs when the
# event is not `push`, so they scope themselves here exactly as they do on a
# pull request. The deploy jobs additionally require
# `github.ref == 'refs/heads/main'`, which a queue ref never matches, so a
# queued candidate can never promote to production.
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
DO_NOT_TRACK: '1'
jobs:
ci-scope:
name: CI scope
runs-on: ubuntu-latest
outputs:
library: ${{ steps.scope.outputs.library }}
angular_compatibility: ${{ steps.scope.outputs.angular_compatibility }}
website: ${{ steps.scope.outputs.website }}
cockpit: ${{ steps.scope.outputs.cockpit }}
cockpit_examples: ${{ steps.scope.outputs.cockpit_examples }}
cockpit_smoke: ${{ steps.scope.outputs.cockpit_smoke }}
examples_chat: ${{ steps.scope.outputs.examples_chat }}
examples_ag_ui: ${{ steps.scope.outputs.examples_ag_ui }}
cockpit_e2e: ${{ steps.scope.outputs.cockpit_e2e }}
website_e2e: ${{ steps.scope.outputs.website_e2e }}
posthog: ${{ steps.scope.outputs.posthog }}
scripts_tests: ${{ steps.scope.outputs.scripts_tests }}
growth_lifecycle: ${{ steps.scope.outputs.growth_lifecycle }}
growth_research: ${{ steps.scope.outputs.growth_research }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
# ci-scope.mjs delegates project ownership to `nx show projects --affected`
# (introduced in PR 2 of the ci-scope thin-shim migration), which requires
# nx + the workspace's plugins to be installed. npm ci needs ~15-30s with
# the npm cache warm; ~60s cold. This is the one-time-per-PR cost of using
# nx's project graph as the source of truth instead of a hand-maintained
# classifier walk.
- run: npm ci
- name: Test CI scope classifier
run: node --test scripts/ci-scope.spec.mjs scripts/cockpit-matrix.spec.mjs scripts/cockpit-ports.spec.mjs scripts/cockpit-runtime-bridge-coverage.spec.mjs
- name: Detect changed CI surfaces
id: scope
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "push" ]; then
node scripts/ci-scope.mjs \
--event push \
--output "$GITHUB_OUTPUT"
exit 0
fi
if [ "${{ github.event_name }}" = "merge_group" ]; then
node scripts/ci-scope.mjs \
--event pull_request \
--base "${{ github.event.merge_group.base_sha }}" \
--head "${{ github.event.merge_group.head_sha }}" \
--output "$GITHUB_OUTPUT"
exit 0
fi
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
if ! git cat-file -e "$base_sha^{commit}" 2>/dev/null || ! git cat-file -e "$head_sha^{commit}" 2>/dev/null; then
base_sha="$(git rev-parse HEAD^1)"
head_sha="$(git rev-parse HEAD^2)"
fi
node scripts/ci-scope.mjs \
--event pull_request \
--base "$base_sha" \
--head "$head_sha" \
--output "$GITHUB_OUTPUT"
- name: Validate CI workflow guards
run: node --test scripts/ci-workflow.spec.mjs
scripts-tests:
name: Scripts — generator / proxy vitest suites
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.scripts_tests == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
# Vitest suites for the root deployment/proxy generators (drift checks,
# framework-adapter branching, requirements unions, rate limits). The
# node:test suites in scripts/ run in ci-scope and library instead —
# see scripts/vite.config.mts for the split.
- run: npx nx test scripts
- run: npx nx run-many -t lint test --projects=marketing-assets,marketing-channels --parallel=2
library:
name: Library — lint / test / build
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.library == 'true' || needs.ci-scope.outputs.angular_compatibility == 'true'
runs-on: ubuntu-latest
env:
LIBS: chat,langgraph,ag-ui,render,a2ui,telemetry
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx run-many -t lint --projects=$LIBS
- run: npx nx test langgraph --coverage --maxWorkers=2 --reporter=default
- run: npx nx run-many -t test --projects=chat,ag-ui,render,a2ui,telemetry --coverage --parallel=1 --maxWorkers=2
- run: npx nx run-many -t build --projects=$LIBS --configuration=production
- run: node scripts/verify-release-versions.mjs
- name: DX-coverage — public dev-facing functions must have a JSDoc summary
run: node scripts/check-dx-coverage.mjs
- run: node --test examples/chat/smoke/*.spec.mjs scripts/verify-angular-support.spec.mjs
- run: node scripts/verify-angular-support.mjs
- name: Upload production library artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: threadplane-library-dist
path: dist/libs
if-no-files-found: error
retention-days: 1
angular-compatibility:
name: 'Angular ${{ matrix.angular }} — packaged consumer'
needs: [ci-scope, library]
if: github.event_name == 'push' || needs.ci-scope.outputs.angular_compatibility == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
angular: [20, 21, 22]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22.22.3
cache: npm
- run: npm ci
- name: Download production library artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: threadplane-library-dist
path: dist/libs
- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Generate, install, build, and run consumer
run: >-
node examples/chat/smoke/cli.mjs
--non-interactive --fresh
--target "${{ runner.temp }}/threadplane-angular-${{ matrix.angular }}"
--local-dist-root dist/libs
--angular-major "${{ matrix.angular }}"
--install --build --runtime
- name: Upload compatibility diagnostics on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: angular-${{ matrix.angular }}-compatibility-diagnostics
path: |
${{ runner.temp }}/threadplane-angular-${{ matrix.angular }}/package.json
${{ runner.temp }}/threadplane-angular-${{ matrix.angular }}/package-lock.json
${{ runner.temp }}/threadplane-angular-${{ matrix.angular }}/runtime-smoke.png
${{ runner.temp }}/threadplane-angular-${{ matrix.angular }}/runtime-smoke-trace.zip
if-no-files-found: warn
retention-days: 7
website:
name: Website — lint / test / build
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.website == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
env:
# Server pages read the growth form policy while rendering, so the build
# needs the same switch the deployed environment sets. It is not a secret.
GROWTH_FORM_POLICY: growth_v1
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.sha }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx lint website
- run: npx nx test website
- run: npm run generate-api-docs
- name: Commit generated API docs to same-repo PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
env:
# Bind the attacker-controllable PR branch name to an env var so it is
# passed as a literal value, never interpolated into the script text
# (prevents shell injection — OSSF Scorecard Dangerous-Workflow).
HEAD_REF: ${{ github.head_ref }}
run: |
if git diff --quiet -- apps/website/content/docs/*/api/api-docs.json; then
echo "Generated API docs are already committed."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add apps/website/content/docs/*/api/api-docs.json
git commit -m "chore(docs): regenerate api docs"
git push origin "HEAD:$HEAD_REF"
- name: Verify generated API docs are committed
run: git diff --exit-code -- apps/website/content/docs/*/api/api-docs.json
- run: npx nx build website
growth-lifecycle:
name: Growth lifecycle — Node 22
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.growth_lifecycle == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx lint growth
- run: npx nx test growth
- run: npx nx run growth:test-operator-cli
- run: npx nx build growth
- run: npx nx test google-mailbox-poller
- run: npx nx lint google-mailbox-poller
lifecycle:
name: Lifecycle — Node 24
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.growth_lifecycle == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: npm
- run: npm ci
- run: npx nx lint lifecycle
- run: npx nx lint growth-capture
- run: npx nx test growth-capture
- run: npx nx run growth-capture:check
- run: npx nx test lifecycle
- run: npx nx run lifecycle:check
- run: npx nx build lifecycle
growth-research:
name: Growth Research — Node 24
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.growth_research == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: npm
- run: npm ci --ignore-scripts
- run: npx nx lint growth-research
- run: npx nx test growth-research
- run: npx nx check growth-research
- run: npx nx build growth-research
cockpit:
name: Workspace libraries — lint / test
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
# cockpit-registry, cockpit-shell and workspace-react carry `test`
# targets that nothing in CI invoked: `nx test` does not walk `^test`,
# and the `library` job runs a hardcoded LIBS list that excludes all
# three. Name them here so their specs actually execute. They all share
# the `scope:cockpit` tag, so ci-scope already gates this job correctly
# for changes under any of them. workspace-react's only other scope tag
# is `scope:shared`, which is not a SCOPE_KEY — adding it to LIBS would
# not have run it, because a workspace-react change never flips
# `library`.
- run: npx nx lint workspace-react
- run: npx nx run-many -t test --projects=cockpit-registry,cockpit-shell,workspace-react --skip-nx-cache
cockpit-examples-build:
name: Cockpit — build all examples
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_examples == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Build cockpit examples (affected on PR, all on push)
run: |
if [ "${{ github.event_name }}" = "push" ]; then
npx nx run-many -t build --projects='cockpit-*-angular' --skip-nx-cache
else
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
AFFECTED=$(npx nx show projects --affected --base="$BASE" --head="$HEAD" \
| grep -E '^cockpit-.*-angular$' | paste -sd, - || true)
if [ -z "$AFFECTED" ]; then
echo "No affected cockpit angular projects; nothing to build."
else
echo "Building affected: $AFFECTED"
npx nx run-many -t build --projects="$AFFECTED" --skip-nx-cache
fi
fi
cockpit-smoke:
name: Cockpit — representative capability smoke
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_smoke == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx nx run-many -t smoke --projects=cockpit-ag-ui-interrupts-python,cockpit-ag-ui-streaming-python,cockpit-deep-agents-planning-python,cockpit-deep-agents-filesystem-python,cockpit-deep-agents-subagents-python,cockpit-deep-agents-memory-python,cockpit-deep-agents-skills-python,cockpit-langgraph-persistence-python,cockpit-langgraph-durable-execution-python,cockpit-langgraph-streaming-python,cockpit-langgraph-interrupts-python,cockpit-langgraph-memory-python,cockpit-langgraph-subgraphs-python,cockpit-langgraph-time-travel-python,cockpit-langgraph-deployment-runtime-python,cockpit-chat-messages-python,cockpit-render-spec-rendering-python,cockpit-runtimes-microsoft-agent-framework-python,cockpit-runtimes-aws-strands-python --skip-nx-cache
examples-chat-smoke:
name: examples/chat — python smoke
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-chat python venv
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/chat/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }}
- working-directory: examples/chat/python
run: uv sync
- run: npx nx run examples-chat-python:smoke --skip-nx-cache
examples-chat-e2e:
name: 'examples/chat — e2e (${{ matrix.shard }}/4)'
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-chat python venv
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/chat/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }}
- working-directory: examples/chat/python
run: uv sync
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- run: npx nx e2e examples-chat-angular --skip-nx-cache -- --shard=${{ matrix.shard }}/4
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: examples-chat-e2e-trace-shard-${{ matrix.shard }}
path: |
test-results/
examples/chat/angular/e2e/test-results/
retention-days: 7
examples-chat-e2e-summary:
name: 'examples/chat — e2e'
needs: [ci-scope, examples-chat-e2e]
if: always() && (github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true')
runs-on: ubuntu-latest
steps:
- name: Aggregate matrix outcome
run: |
if [[ "${{ needs.examples-chat-e2e.result }}" != "success" ]]; then
echo "Matrix outcome: ${{ needs.examples-chat-e2e.result }}"
exit 1
fi
echo "All examples-chat-e2e matrix expansions passed."
examples-ag-ui-e2e:
name: 'examples/ag-ui — e2e'
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_ag_ui == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-ag-ui python venv
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/ag-ui/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/ag-ui/python/uv.lock') }}
- working-directory: examples/ag-ui/python
run: uv sync
- name: Check requirements.txt matches uv.lock
working-directory: examples/ag-ui/python
run: |
# requirements.txt is what the Dockerfile installs from; uv.lock is what
# dependency bumps actually update. Regenerate in place with the very
# command the file records in its own header — uv stamps both the flags
# and the -o path there, so exporting to a scratch path or dropping
# --no-dev can never match a correctly-generated file — and let
# `git diff` compare against the committed version.
if ! head -1 requirements.txt | grep -q 'autogenerated by uv'; then
echo "examples/ag-ui/python/requirements.txt is hand-maintained, not a uv export — skipping drift check."
exit 0
fi
flags="--no-hashes"
if sed -n 2p requirements.txt | grep -q -- '--no-dev'; then
flags="$flags --no-dev"
fi
uv export $flags -o requirements.txt
if ! git diff --exit-code -- requirements.txt; then
echo "::error::examples/ag-ui/python/requirements.txt is stale — the Railway image installs from this file, not uv.lock. Run 'uv export --no-hashes -o requirements.txt' in examples/ag-ui/python and commit the result."
exit 1
fi
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- run: npx nx e2e examples-ag-ui-angular --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: examples-ag-ui-e2e-trace
path: |
test-results/
examples/ag-ui/angular/e2e/test-results/
retention-days: 7
cockpit-e2e-dispatcher:
name: Cockpit — e2e dispatcher
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_e2e == 'true'
runs-on: ubuntu-latest
outputs:
caps: ${{ steps.matrix.outputs.caps }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Compute affected base + head
id: refs
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
echo "head=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
echo "full=false" >> "$GITHUB_OUTPUT"
else
echo "base=HEAD~1" >> "$GITHUB_OUTPUT"
echo "head=HEAD" >> "$GITHUB_OUTPUT"
echo "full=true" >> "$GITHUB_OUTPUT"
fi
- name: Emit cap matrix
id: matrix
run: |
node scripts/cockpit-matrix.mjs \
--base "${{ steps.refs.outputs.base }}" \
--head "${{ steps.refs.outputs.head }}" \
--full-fleet "${{ steps.refs.outputs.full }}"
cockpit-e2e:
name: 'Cockpit — e2e (${{ matrix.cap.angular }})'
needs: [ci-scope, cockpit-e2e-dispatcher]
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_e2e == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 5
matrix:
cap: ${{ fromJson(needs.cockpit-e2e-dispatcher.outputs.caps) }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
# matrix.cap.python is '' for caps whose backend is not Python (e.g.
# cockpit-runtimes-mastra-angular — its backend is the
# deployments/ag-ui-mastra Node service, installed by the example's
# own e2e global setup). Skip the uv/venv steps for those.
- name: Cache cap python venv
if: matrix.cap.python != ''
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ matrix.cap.python }}/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ matrix.cap.python }}-${{ hashFiles(format('{0}/uv.lock', matrix.cap.python)) }}
- name: uv sync per-cap python
if: matrix.cap.python != ''
working-directory: ${{ matrix.cap.python }}
run: uv sync
- name: Check requirements.txt matches uv.lock
if: matrix.cap.python != ''
working-directory: ${{ matrix.cap.python }}
run: |
# Only a handful of caps (the ones deployed via a Railway/Docker
# image, e.g. cockpit/ag-ui/*) check in a requirements.txt export
# alongside pyproject.toml/uv.lock. Skip caps that don't have one.
if [ ! -f requirements.txt ]; then
echo "No requirements.txt in ${{ matrix.cap.python }} — skipping drift check."
exit 0
fi
# requirements.txt is what the Dockerfile installs from; uv.lock is what
# dependency bumps actually update. Regenerate in place with the very
# command the file records in its own header — uv stamps both the flags
# and the -o path there, so exporting to a scratch path or dropping
# --no-dev can never match a correctly-generated file — and let
# `git diff` compare against the committed version.
if ! head -1 requirements.txt | grep -q 'autogenerated by uv'; then
echo "${{ matrix.cap.python }}/requirements.txt is hand-maintained, not a uv export — skipping drift check."
exit 0
fi
flags="--no-hashes"
if sed -n 2p requirements.txt | grep -q -- '--no-dev'; then
flags="$flags --no-dev"
fi
uv export $flags -o requirements.txt
if ! git diff --exit-code -- requirements.txt; then
echo "::error::${{ matrix.cap.python }}/requirements.txt is stale relative to uv.lock — deployments/ag-ui-dev is generated from this file. Run 'uv export --no-hashes -o requirements.txt' in ${{ matrix.cap.python }} and commit the result."
exit 1
fi
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- name: nx e2e ${{ matrix.cap.angular }}
run: npx nx e2e "${{ matrix.cap.angular }}" --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cockpit-e2e-trace-${{ matrix.cap.angular }}
path: |
cockpit/**/angular/e2e/test-results/
retention-days: 7
cockpit-e2e-summary:
name: 'Cockpit — e2e'
needs: [ci-scope, cockpit-e2e]
if: always() && (github.event_name == 'push' || needs.ci-scope.outputs.cockpit_e2e == 'true')
runs-on: ubuntu-latest
steps:
- name: Aggregate matrix outcome
run: |
if [[ "${{ needs.cockpit-e2e.result }}" != "success" ]]; then
echo "Matrix outcome: ${{ needs.cockpit-e2e.result }}"
exit 1
fi
echo "All cockpit-e2e matrix expansions passed."
website-e2e:
name: Website — e2e
needs: ci-scope
if: github.event_name == 'push' || needs.ci-scope.outputs.website_e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- run: npx nx e2e website --skip-nx-cache
# The public-copy gate reads served output, and `next dev` serves a
# different bundle than production. Run it a second time against a real
# production build so a claim that only appears in the built output — in a
# generated bundle or a response body — cannot reach the site unnoticed.
- name: Public copy boundary (production build)
env:
GROWTH_FORM_POLICY: growth_v1
WEBSITE_E2E_MODE: production
run: npx nx e2e website --skip-nx-cache --grep "public copy boundary|canonical policy surface"
# Spec §8: scroll-craft's own harness walks the homepage's pinned stage
# act and fails on dead scroll outside the declared hold and on cues that
# never peak. The Nx build lands in dist/apps/website with a rewritten
# next.config, which `nx serve --configuration=production` starts.
# Rebuild explicitly with the paired stage origin: serve reuses the
# prior output, whose public-copy build points at production. The content
# symlink, which mirrors playwright.config.ts's production mode, is
# added only after the server answers, so it lands on the dist the
# build has finished writing rather than on one it is about to replace.
- name: Stage scroll verification (scroll-craft harness)
env:
GROWTH_FORM_POLICY: growth_v1
SCROLLCRAFT_CHROME: /usr/bin/google-chrome
NEXT_PUBLIC_STAGE_DEMO_ORIGIN: http://localhost:4200
run: |
command -v ffmpeg > /dev/null || (sudo apt-get update && sudo apt-get install -y ffmpeg)
# Verify this commit's recording, not the older production demo.
(npx nx serve examples-chat-angular --configuration=production --port=4200 > /tmp/stage-demo.log 2>&1 &)
for i in $(seq 1 60); do curl -sf http://localhost:4200/stage > /dev/null && break; sleep 2; done
curl -sf http://localhost:4200/stage > /dev/null || { cat /tmp/stage-demo.log; exit 1; }
npx nx build website --configuration=production --skip-nx-cache
(npx nx serve website --configuration=production --port=4308 --skip-nx-cache > /tmp/next-start.log 2>&1 &)
for i in $(seq 1 60); do curl -sf http://127.0.0.1:4308/ > /dev/null && break; sleep 2; done
curl -sf http://127.0.0.1:4308/ > /dev/null || { cat /tmp/next-start.log; exit 1; }
ln -sfn ../../../apps/website/content dist/apps/website/content
STAGE_LIVE_FRAME=true BASE_URL=http://127.0.0.1:4308 npx playwright test apps/website/e2e/home-stage.spec.ts --config apps/website/playwright.config.ts
node apps/website/e2e/scroll-craft/verify-home.mjs --url http://127.0.0.1:4308 --out dist/stage-shots
- name: Upload stage contact sheets
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: stage-shots
path: dist/stage-shots/**/sheet.png
if-no-files-found: ignore
# The production-smoke spec is testIgnore'd outside PRODUCTION_SMOKE mode,
# so a module-load error in it is invisible until the post-merge Production
# smoke job runs against main — too late to gate a PR. Collect it here:
# --list loads every spec without hitting production.
- name: Production-smoke spec must load
env:
PRODUCTION_SMOKE: 'true'
BASE_URL: https://threadplane.ai
run: |
npx playwright test apps/website/e2e/platform-production-smoke.spec.ts \
--config apps/website/playwright.config.ts --list
website-preview-e2e:
name: Website — e2e (deployed preview)
timeout-minutes: 40
needs: ci-scope
# PR-side twin of the deploy job's post-promotion verification. The
# ordinary suite runs against a real, protected Vercel preview of the
# Website whose runtime iframe loads from a matching examples preview, so
# remote-target assumptions and platform behaviour surface before merge.
# Two deterministic aliases break the ordering problem: the examples are
# assembled with the Website alias in their parent-origin policy, and the
# Website is built with the examples alias as its runtime base. A later
# push re-points both aliases; the deployments behind them are kept for
# inspection. Needs repository secrets, so same-repo PRs and merge-queue
# candidates only.
if: >-
github.event_name != 'push' &&
needs.ci-scope.outputs.website_e2e == 'true' &&
(github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
playwright-${{ runner.os }}-
- run: npx playwright install --with-deps chromium
- name: Derive deterministic preview aliases
id: aliases
run: |
if [ "${{ github.event_name }}" = "merge_group" ]; then
key="mq-$(echo "${{ github.event.merge_group.head_sha }}" | cut -c1-8)"
else
key="pr-${{ github.event.pull_request.number }}"
fi
echo "website=threadplane-${key}-cacheplane.vercel.app" >> "$GITHUB_OUTPUT"
echo "examples=threadplane-examples-${key}-cacheplane.vercel.app" >> "$GITHUB_OUTPUT"
- name: Require preview bypass secrets
# Both previews sit behind deployment protection, and bypass secrets
# are issued per Vercel project. Fail before creating anything.
run: |
if [ -z "${VERCEL_AUTOMATION_BYPASS_SECRET}" ]; then
echo "::error::VERCEL_AUTOMATION_BYPASS_SECRET is unset — the protected Website preview cannot be verified. Enable 'Protection Bypass for Automation' on the Vercel threadplane project and store the value as this repository secret."
exit 1
fi
if [ -z "${VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET}" ]; then
echo "::error::VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET is unset — the runtime iframe from the protected examples preview cannot load. Enable 'Protection Bypass for Automation' on the Vercel threadplane-examples project and store the value as this repository secret."
exit 1
fi
env:
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET }}
- name: Build and assemble Angular examples for the preview
run: npx tsx scripts/assemble-examples.ts
env:
RUNTIME_PARENT_PREVIEW_ORIGINS: https://${{ steps.aliases.outputs.website }}
- name: Deploy examples preview and alias it
working-directory: deploy/examples
run: |
set -euo pipefail
mkdir -p .vercel
cat > .vercel/project.json <<'EOF'
{"projectId":"${{ secrets.VERCEL_EXAMPLES_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane-examples"}
EOF
npx vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
url=$(npx vercel deploy --prebuilt --yes --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1)
url=$(node -e 'const parsed = new URL(process.argv[1]); if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) throw new Error("Vercel returned a non-origin examples preview URL"); process.stdout.write(parsed.origin)' "$url")
echo "examples deployment: $url"
npx vercel alias set "$url" "${{ steps.aliases.outputs.examples }}" --scope=${{ secrets.VERCEL_ORG_ID }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Build, deploy, and alias the Website preview
env:
GROWTH_FORM_POLICY: growth_v1
NEXT_PUBLIC_COCKPIT_RUNTIME_BASE_URL: https://${{ steps.aliases.outputs.examples }}
run: |
set -euo pipefail
mkdir -p .vercel
cat > .vercel/project.json <<'EOF'
{"projectId":"${{ secrets.VERCEL_WEBSITE_PROJECT_ID }}","orgId":"${{ secrets.VERCEL_ORG_ID }}","projectName":"threadplane"}
EOF
npx vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
rm -rf .vercel/output
npx vercel build --token=${{ secrets.VERCEL_TOKEN }}
url=$(npx vercel deploy --prebuilt --archive=tgz --yes --token=${{ secrets.VERCEL_TOKEN }} | tail -n 1)
url=$(node -e 'const parsed = new URL(process.argv[1]); if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) throw new Error("Vercel returned a non-origin Website preview URL"); process.stdout.write(parsed.origin)' "$url")
echo "website deployment: $url"
npx vercel alias set "$url" "${{ steps.aliases.outputs.website }}" --scope=${{ secrets.VERCEL_ORG_ID }} --token=${{ secrets.VERCEL_TOKEN }}
- name: Run the Website suite against the aliased preview
run: npx nx e2e website --skip-nx-cache
env:
BASE_URL: https://${{ steps.aliases.outputs.website }}
RUNTIME_BYPASS_ORIGIN: https://${{ steps.aliases.outputs.examples }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_EXAMPLES_AUTOMATION_BYPASS_SECRET }}
required-pr-checks:
name: CI — required
needs:
- ci-scope
- library
- angular-compatibility
- website
- cockpit
- cockpit-examples-build
- cockpit-smoke
- examples-chat-smoke
- examples-chat-e2e
- examples-ag-ui-e2e
- cockpit-e2e-summary
- website-e2e
- website-preview-e2e
- posthog-sync-plan
- scripts-tests
- growth-lifecycle
- lifecycle
- growth-research
# `CI — required` is the only required status context. A merge queue
# waits on it for each candidate, so it must report on merge_group too —
# otherwise every queued merge blocks forever on a check that never runs.
if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}
runs-on: ubuntu-latest
steps:
- name: Verify scoped CI jobs
env:
RESULT_CI_SCOPE: ${{ needs.ci-scope.result }}
RESULT_LIBRARY: ${{ needs.library.result }}
RESULT_ANGULAR_COMPATIBILITY: ${{ needs.angular-compatibility.result }}
RESULT_WEBSITE: ${{ needs.website.result }}
RESULT_COCKPIT: ${{ needs.cockpit.result }}
RESULT_COCKPIT_EXAMPLES: ${{ needs.cockpit-examples-build.result }}
RESULT_COCKPIT_SMOKE: ${{ needs.cockpit-smoke.result }}
RESULT_EXAMPLES_CHAT_SMOKE: ${{ needs.examples-chat-smoke.result }}
RESULT_EXAMPLES_CHAT_E2E: ${{ needs.examples-chat-e2e.result }}
RESULT_EXAMPLES_AG_UI_E2E: ${{ needs.examples-ag-ui-e2e.result }}
RESULT_COCKPIT_E2E: ${{ needs.cockpit-e2e-summary.result }}
RESULT_WEBSITE_E2E: ${{ needs.website-e2e.result }}
RESULT_WEBSITE_PREVIEW_E2E: ${{ needs.website-preview-e2e.result }}
RESULT_POSTHOG: ${{ needs.posthog-sync-plan.result }}
RESULT_SCRIPTS_TESTS: ${{ needs.scripts-tests.result }}
RESULT_GROWTH_LIFECYCLE: ${{ needs.growth-lifecycle.result }}
RESULT_LIFECYCLE: ${{ needs.lifecycle.result }}
RESULT_GROWTH_RESEARCH: ${{ needs.growth-research.result }}
SCOPE_LIBRARY: ${{ needs.ci-scope.outputs.library }}
SCOPE_ANGULAR_COMPATIBILITY: ${{ needs.ci-scope.outputs.angular_compatibility }}
SCOPE_WEBSITE: ${{ needs.ci-scope.outputs.website }}
SCOPE_COCKPIT: ${{ needs.ci-scope.outputs.cockpit }}
SCOPE_COCKPIT_EXAMPLES: ${{ needs.ci-scope.outputs.cockpit_examples }}
SCOPE_COCKPIT_SMOKE: ${{ needs.ci-scope.outputs.cockpit_smoke }}
SCOPE_EXAMPLES_CHAT: ${{ needs.ci-scope.outputs.examples_chat }}
SCOPE_EXAMPLES_AG_UI: ${{ needs.ci-scope.outputs.examples_ag_ui }}
SCOPE_COCKPIT_E2E: ${{ needs.ci-scope.outputs.cockpit_e2e }}
SCOPE_WEBSITE_E2E: ${{ needs.ci-scope.outputs.website_e2e }}
SCOPE_POSTHOG: ${{ needs.ci-scope.outputs.posthog }}
SCOPE_SCRIPTS_TESTS: ${{ needs.ci-scope.outputs.scripts_tests }}
SCOPE_GROWTH_LIFECYCLE: ${{ needs.ci-scope.outputs.growth_lifecycle }}
SCOPE_GROWTH_RESEARCH: ${{ needs.ci-scope.outputs.growth_research }}
# The preview lanes need repository secrets, so they skip on fork
# PRs. Their scope keys are computed from changed files alone, so on
# a fork they can be in scope yet legitimately skipped. This mirrors
# the lanes' own `if` so the gate only demands them when they could run.
PREVIEW_LANES_ELIGIBLE: ${{ github.event_name == 'merge_group' || github.event.pull_request.head.repo.full_name == github.repository }}
run: |
set -euo pipefail
failed=0
require_always() {
local label="$1"
local result="$2"
if [[ "$result" != "success" ]]; then
echo "::error::${label} finished with ${result}; refusing to report CI green."
failed=1
fi
}
require_scoped() {
local scope_key="$1"
local label="$2"
local result="$3"
local scoped="$4"
if [[ "$scoped" == "true" ]]; then
if [[ "$result" != "success" ]]; then
echo "::error::${label} is required by scope ${scope_key} but finished with ${result}."
failed=1
fi
return
fi
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "::error::${label} was not selected by scope ${scope_key} but finished with ${result}."
failed=1
fi
}
# Scoped check for the PR-side preview lanes: enforced only when the
# lanes were eligible to run; otherwise treated as unselected, where
# only a real failure or cancellation is an error.
require_preview() {
local scope_key="$1"
local label="$2"
local result="$3"
local scoped="$4"
if [[ "$PREVIEW_LANES_ELIGIBLE" == "true" && "$scoped" == "true" && "$result" == "skipped" ]]; then
echo "::error::${label} was eligible and in scope ${scope_key} but was skipped — its job-level if no longer matches the gate's PREVIEW_LANES_ELIGIBLE expression."
failed=1
return
fi
if [[ "$PREVIEW_LANES_ELIGIBLE" == "true" ]]; then
require_scoped "$scope_key" "$label" "$result" "$scoped"
else
require_scoped "$scope_key" "$label" "$result" "false"
fi
}
require_always "CI scope" "$RESULT_CI_SCOPE"
require_scoped "library" "Library — lint / test / build" "$RESULT_LIBRARY" "$SCOPE_LIBRARY"
require_scoped \
"angular_compatibility" \
"Angular compatibility matrix" \
"$RESULT_ANGULAR_COMPATIBILITY" \
"$SCOPE_ANGULAR_COMPATIBILITY"
require_scoped "website" "Website — lint / test / build" "$RESULT_WEBSITE" "$SCOPE_WEBSITE"
require_scoped "cockpit" "Workspace libraries — lint / test" "$RESULT_COCKPIT" "$SCOPE_COCKPIT"
require_scoped "cockpit_examples" "Cockpit — build all examples" "$RESULT_COCKPIT_EXAMPLES" "$SCOPE_COCKPIT_EXAMPLES"
require_scoped "cockpit_smoke" "Cockpit — representative capability smoke" "$RESULT_COCKPIT_SMOKE" "$SCOPE_COCKPIT_SMOKE"
require_scoped "examples_chat" "examples/chat — python smoke" "$RESULT_EXAMPLES_CHAT_SMOKE" "$SCOPE_EXAMPLES_CHAT"
require_scoped "examples_chat" "examples/chat — e2e" "$RESULT_EXAMPLES_CHAT_E2E" "$SCOPE_EXAMPLES_CHAT"
require_scoped \
"examples_ag_ui" \
"examples/ag-ui — e2e" \
"$RESULT_EXAMPLES_AG_UI_E2E" \
"$SCOPE_EXAMPLES_AG_UI"
require_scoped "cockpit_e2e" "Cockpit — e2e" "$RESULT_COCKPIT_E2E" "$SCOPE_COCKPIT_E2E"
require_scoped "website_e2e" "Website — e2e" "$RESULT_WEBSITE_E2E" "$SCOPE_WEBSITE_E2E"
require_preview "website_e2e" "Website — e2e (deployed preview)" "$RESULT_WEBSITE_PREVIEW_E2E" "$SCOPE_WEBSITE_E2E"
require_scoped "posthog" "PostHog — dashboards-as-code drift check" "$RESULT_POSTHOG" "$SCOPE_POSTHOG"
require_scoped "scripts_tests" "Scripts — generator / proxy vitest suites" "$RESULT_SCRIPTS_TESTS" "$SCOPE_SCRIPTS_TESTS"
require_scoped "growth_lifecycle" "Growth lifecycle — Node 22" "$RESULT_GROWTH_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
require_scoped "growth_lifecycle" "Lifecycle — Node 24" "$RESULT_LIFECYCLE" "$SCOPE_GROWTH_LIFECYCLE"
require_scoped "growth_research" "Growth Research — Node 24" "$RESULT_GROWTH_RESEARCH" "$SCOPE_GROWTH_RESEARCH"
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
echo "All scoped PR checks passed."
deploy:
name: Deploy → Vercel
timeout-minutes: 30 # fail fast instead of blocking the main concurrency group on a hang
needs:
- library
- website
- cockpit
- cockpit-examples-build
- cockpit-smoke
- examples-chat-smoke
- examples-chat-e2e
- cockpit-e2e-summary
- website-e2e
- growth-lifecycle
- lifecycle
runs-on: ubuntu-latest
outputs:
runtime_parent_preview_origin: ${{ steps.deploy_website.outputs.preview_origin }}
# Only deploy on pushes to main, not on pull requests