Repository navigation
990 lines (966 loc) · 48.2 KB
/
Copy pathci.yml
File metadata and controls
990 lines (966 loc) · 48.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
name: CI
on:
push:
branches:
- main
- 'release/**'
pull_request:
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
merge_group:
types: [checks_requested]
permissions:
contents: read
# PR runs coalesce by PR number. Merge-group candidates plus main/release pushes use
# the exact candidate SHA as the group key so each integration candidate keeps its evidence.
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
changes:
# Classify only trusted event metadata plus base...head Git data. For PRs and
# merge-group candidates the classifier is loaded from the trusted base commit,
# so candidate changes cannot weaken their own native gates. If the trusted base
# predates the classifier, fail safe to
# the full daily native matrix instead of executing classifier code from the PR head.
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
needs_windows: ${{ steps.classify.outputs.needs_windows }}
needs_windows_core: ${{ steps.classify.outputs.needs_windows_core }}
needs_windows_runner: ${{ steps.classify.outputs.needs_windows_runner }}
needs_windows_package: ${{ steps.classify.outputs.needs_windows_package }}
needs_windows_desktop: ${{ steps.classify.outputs.needs_windows_desktop }}
needs_macos: ${{ steps.classify.outputs.needs_macos }}
needs_macos_desktop: ${{ steps.classify.outputs.needs_macos_desktop }}
# Older trusted PR bases do not expose the newer frontend/container/Plugin SDK outputs.
# A historical full-native result is the safe rollout fallback for those bases.
needs_frontend: ${{ steps.classify.outputs.needs_frontend == 'true' || (steps.classify.outputs.needs_frontend == '' && steps.classify.outputs.needs_full_native == 'true') }}
needs_desktop_frontend: ${{ steps.classify.outputs.needs_desktop_frontend == 'true' || (steps.classify.outputs.needs_desktop_frontend == '' && steps.classify.outputs.needs_full_native == 'true') }}
needs_plugin_sdk: ${{ steps.classify.outputs.needs_plugin_sdk == 'true' || (steps.classify.outputs.needs_plugin_sdk == '' && steps.classify.outputs.needs_full_native == 'true') }}
needs_docker: ${{ steps.classify.outputs.needs_docker == 'true' || (steps.classify.outputs.needs_docker == '' && steps.classify.outputs.needs_full_native == 'true') }}
needs_desktop_package: ${{ steps.classify.outputs.needs_desktop_package }}
needs_full_native: ${{ steps.classify.outputs.needs_full_native }}
categories: ${{ steps.classify.outputs.categories }}
reason: ${{ steps.classify.outputs.reason }}
steps:
- name: Checkout trusted classifier source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Classify native CI risk
id: classify
env:
EVENT_NAME: ${{ github.event_name }}
EXTERNAL_CONTRIBUTOR: ${{ github.event_name == 'pull_request' && github.event.pull_request.user.login != github.repository_owner }}
RUN_CI: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'run-ci') }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || '' }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha || github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
MERGE_GROUP_HEAD_REF: ${{ github.event.merge_group.head_ref || '' }}
run: |
set -euo pipefail
classifier="scripts/ci_path_risk.py"
if [ ! -f "$classifier" ]; then
{
for key in \
needs_windows \
needs_windows_core \
needs_windows_runner \
needs_windows_package \
needs_windows_desktop \
needs_macos \
needs_macos_desktop \
needs_frontend \
needs_desktop_frontend \
needs_plugin_sdk \
needs_docker \
needs_desktop_package \
needs_full_native; do
echo "$key=true"
done
echo "categories=bootstrap-full-native"
echo "reason=full-native:bootstrap-base-missing"
} >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$EVENT_NAME" = pull_request ] && \
[ "$EXTERNAL_CONTRIBUTOR" != true ] && \
[ "$RUN_CI" != true ] && \
! git cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null; then
git fetch --no-tags origin "refs/pull/$PR_NUMBER/head"
fetched_head="$(git rev-parse FETCH_HEAD)"
if [ "$fetched_head" != "$HEAD_SHA" ]; then
echo "fetched PR head does not match event head SHA" >&2
exit 1
fi
fi
if [ "$EVENT_NAME" = merge_group ] && \
! git cat-file -e "$HEAD_SHA^{commit}" 2>/dev/null; then
if [ -z "$MERGE_GROUP_HEAD_REF" ]; then
echo "merge-group head ref is missing" >&2
exit 1
fi
git fetch --no-tags origin "$MERGE_GROUP_HEAD_REF"
fetched_head="$(git rev-parse FETCH_HEAD)"
if [ "$fetched_head" != "$HEAD_SHA" ]; then
echo "fetched merge-group head does not match event head SHA" >&2
exit 1
fi
fi
python3 "$classifier" \
--event-name "$EVENT_NAME" \
--external-contributor "$EXTERNAL_CONTRIBUTOR" \
--run-ci "$RUN_CI" \
--base "$BASE_SHA" \
--head "$HEAD_SHA" \
--github-output "$GITHUB_OUTPUT"
contract:
# Mandatory contract lane for every workflow invocation. It waits only for the
# cheap path classifier so unrelated frontend dependency/install work can be
# skipped; Rust/schema/workspace contracts remain unconditional.
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
components: rustfmt
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: linux-ci
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: needs.changes.outputs.needs_frontend == 'true' || needs.changes.outputs.needs_desktop_frontend == 'true' || needs.changes.outputs.needs_plugin_sdk == 'true'
with:
node-version: 22
cache: npm
cache-dependency-path: |
frontend/package-lock.json
apps/desktop/package-lock.json
npm/plugin-sdk/package-lock.json
plugins/safe-delete/package-lock.json
plugins/repo-info/package-lock.json
plugins/repo-context/package-lock.json
plugins/campus-application/package-lock.json
plugins/agent-browser/package-lock.json
plugins/agent-environment/package-lock.json
- name: Install frontend dependencies
if: needs.changes.outputs.needs_frontend == 'true' || needs.changes.outputs.needs_desktop_frontend == 'true'
run: npm ci --prefix frontend
- name: Check frontend contracts
if: needs.changes.outputs.needs_frontend == 'true'
run: |
npm --prefix frontend run typecheck
npm --prefix frontend test
npm --prefix frontend run check:dist
- name: Install Desktop frontend dependencies
if: needs.changes.outputs.needs_desktop_frontend == 'true'
run: npm ci --prefix apps/desktop
- name: Check Desktop frontend contracts
if: needs.changes.outputs.needs_desktop_frontend == 'true'
run: |
npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop test
npm --prefix apps/desktop run build
- name: Install Plugin SDK dependencies
if: needs.changes.outputs.needs_plugin_sdk == 'true'
run: |
npm ci --prefix npm/plugin-sdk
npm ci --prefix plugins/safe-delete
npm ci --prefix plugins/repo-info
npm ci --prefix plugins/repo-context
npm ci --prefix plugins/campus-application
npm ci --prefix plugins/agent-browser
npm ci --prefix plugins/agent-environment
- name: Check Plugin SDK contracts and first-party dogfood
if: needs.changes.outputs.needs_plugin_sdk == 'true'
run: |
npm --prefix npm/plugin-sdk run typecheck
npm --prefix npm/plugin-sdk run build
npm --prefix npm/plugin-sdk test
npm --prefix npm/plugin-sdk run pack:dry-run
npm --prefix plugins/safe-delete run typecheck
npm --prefix plugins/safe-delete test
npm --prefix plugins/repo-info run typecheck
npm --prefix plugins/repo-info test
npm --prefix plugins/repo-context run typecheck
npm --prefix plugins/repo-context test
npm --prefix plugins/campus-application run typecheck
npm --prefix plugins/campus-application test
npm --prefix plugins/agent-browser run typecheck
npm --prefix plugins/agent-browser test
npm --prefix plugins/agent-environment test
npm --prefix plugins/agent-environment run test:pi
- name: Verify workspace boundaries
run: |
bash scripts/workspace_boundary_check.sh --self-test
bash scripts/workspace_boundary_check.sh
- name: Check build identity and incremental boundary
run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_build_identity
- name: Check Debian package lifecycle harness
# Fake package steps and local child processes; no native matrix acceptance.
run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_deb_lifecycle_acceptance
- name: Check formatting
run: cargo fmt --all -- --check
- name: Ensure test inventory dependency
run: |
if ! command -v rg >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y ripgrep
fi
- name: Check test inventory contract
run: |
bash scripts/test_inventory.sh --self-test
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_ci_rust_test scripts.tests.test_ci_aggregate_cancellation
bash scripts/test_inventory.sh
- name: Check macOS signing setup and release evidence contracts
# Fake signing commands and synthetic artifacts only; no Apple credentials.
run: PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts.tests.test_macos_signing_setup scripts.tests.test_macos_self_signed scripts.tests.test_macos_release_evidence
- name: Check canonical contracts including optional tool surfaces
# Root MCP/HTTP integration tests already run in the mandatory server
# shard. Do not compile the full Server a second time for three filters.
# This smaller crate also covers optional schemas absent from default builds.
run: |
python3 scripts/tests/test_runner_observability_contract.py
cargo test --locked -p webcodex-tool-contracts --all-features --lib
# Name/Serde/schema identity must also hold when either optional surface
# is absent. The all-features run above covers their joint presence.
cargo test --locked -p webcodex-tool-contracts --no-default-features --lib registration_consistency
cargo test --locked -p webcodex-tool-contracts --no-default-features --features workspace-checkpoints --lib registration_consistency
cargo test --locked -p webcodex-tool-contracts --no-default-features --features experimental-code-mode --lib registration_consistency
mcp-conformance:
# Dated MCP wire evidence is independent from ordinary unit/integration tests.
# The script pins the external referee and rejects missing or silently skipped
# coverage; known failures require exact check-level classification.
needs: contract
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: linux-ci
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Check conformance report gate
run: python3 scripts/tests/test_mcp_conformance_report.py
- name: Run pinned MCP dated-revision baseline
run: bash scripts/mcp_conformance.sh
- name: Upload MCP conformance evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: mcp-conformance-${{ github.sha }}
path: target/mcp-conformance/reports
if-no-files-found: ignore
retention-days: 7
test-linux-rust:
# Complete Linux Rust coverage is mandatory before merge, including owner PRs.
# Start it immediately in parallel with contract/frontend work; the stable
# `test` aggregate still requires both surfaces to succeed.
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- shard: server
packages: "-p webcodex"
- shard: runner
packages: "-p webcodex-runner -p webcodex-lsp --features runner-real-process-tests"
- shard: workspace-crates
packages: >-
--workspace
--exclude webcodex
--exclude webcodex-runner
--exclude webcodex-lsp
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Preflight static model guidance before server Cargo build
if: matrix.shard == 'server'
run: python3 scripts/check_builtin_guidance_contract.py
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: linux-ci
- name: Run ${{ matrix.shard }} Rust tests
env:
CI_RUST_SHARD: ${{ matrix.shard }}
run: python3 scripts/ci_rust_test.py --report target/ci-test-reports/${{ matrix.shard }}.json -- cargo test --locked ${{ matrix.packages }}
- name: Run experimental Code Mode and receipt tests
if: matrix.shard == 'server'
run: |
CI_RUST_SHARD=experimental-code-mode \
python3 scripts/ci_rust_test.py --report target/ci-test-reports/experimental-code-mode.json -- \
cargo test --locked -p webcodex --features experimental-code-mode code_mode
CI_RUST_SHARD=experimental-code-mode-receipts \
python3 scripts/ci_rust_test.py --report target/ci-test-reports/experimental-code-mode-receipts.json -- \
cargo test --locked -p webcodex --features experimental-code-mode receipt_tests
- name: Upload bounded Rust test evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: rust-tests-${{ matrix.shard }}-${{ github.sha }}-${{ github.run_attempt }}
path: target/ci-test-reports/*.json
if-no-files-found: warn
retention-days: 7
test-linux-tooling:
# Release/tooling checks are mandatory on every PR, but do not need contract
# output. Start them in parallel; the stable `test` aggregate joins the results.
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: linux-ci
# The retained npm release-smoke tooling below invokes node/npm directly.
# Frontend dependency installation and frontend-specific npm caching remain
# contract-lane responsibilities and are intentionally not repeated here.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Check MCP App DOM and message-order contracts
run: node --test src/mcp_tests/*.test.mjs
- name: Ensure Python tooling test dependencies
run: |
if ! command -v rg >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install --no-install-recommends -y ripgrep
fi
- name: Test optional Codex observation adapter
run: python3 -m unittest discover -s integrations/codex -p 'test_*.py' -v
- name: Test optional project knowledge provider
run: python3 -B -m unittest discover -s plugins/project-knowledge -p 'test_*.py' -v
- name: Test configured model API ACP adapter
run: python3 -m unittest discover -s integrations/model_gateway/tests -p 'test_*.py' -v
# Keep the cheap deterministic release gate ahead of the optional full
# workspace check so workflow/script drift is reported before a long Cargo pass.
- name: Test deterministic release contract and tooling
run: bash scripts/release_check.sh --static-only
- name: Check Linux workspace production surfaces without warnings
run: python3 scripts/check_production_warnings.py
- name: Upload Linux production build evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: production-linux-${{ github.sha }}-${{ github.run_attempt }}
path: target/ci-build-reports/*.json
retention-days: 7
- name: Check complete workspace production/test targets
if: github.event_name == 'pull_request' && (github.event.pull_request.user.login != github.repository_owner || contains(github.event.pull_request.labels.*.name, 'run-ci'))
run: cargo check --locked --workspace --all-targets
test:
needs: [contract, changes, test-linux-rust, test-linux-tooling]
# Preserve the historical aggregate status check and always evaluate upstream
# results so a failed or unexpectedly skipped Linux lane cannot disappear.
# Workflow cancellation is distinct from needs.<lane>.result. Superseded
# runs stop; a required child failure/cancellation is still rejected below.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require successful Linux lanes
env:
CONTRACT_RESULT: ${{ needs.contract.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
RUST_RESULT: ${{ needs.test-linux-rust.result }}
TOOLING_RESULT: ${{ needs.test-linux-tooling.result }}
run: |
set -euo pipefail
echo "contract=$CONTRACT_RESULT changes=$CHANGES_RESULT rust=$RUST_RESULT tooling=$TOOLING_RESULT"
if [ "$CONTRACT_RESULT" != success ] || \
[ "$CHANGES_RESULT" != success ] || \
[ "$RUST_RESULT" != success ] || \
[ "$TOOLING_RESULT" != success ]; then
echo "required Linux CI lane did not succeed" >&2
exit 1
fi
test-docker-server:
needs: changes
if: needs.changes.outputs.needs_docker == 'true'
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Resolve exact Server image build identity
shell: bash
run: |
set -euo pipefail
version="$(node -p "require('./npm/webcodex/package.json').version")"
{
echo "WEBCODEX_CI_VERSION=$version"
echo "WEBCODEX_CI_SOURCE_SHORT=$(git rev-parse --short=12 HEAD)"
echo "WEBCODEX_CI_BUILT_AT=$(git show -s --format=%ct HEAD)"
echo "WEBCODEX_CI_IMAGE=webcodex-server-ci:${GITHUB_SHA}"
} >> "$GITHUB_ENV"
git diff --check
test -z "$(git status --porcelain --untracked-files=all)"
- name: Build amd64 Server image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
load: true
push: false
tags: ${{ env.WEBCODEX_CI_IMAGE }}
build-args: |
WEBCODEX_GIT_COMMIT=${{ env.WEBCODEX_CI_SOURCE_SHORT }}
WEBCODEX_GIT_DIRTY=false
WEBCODEX_BUILT_AT=${{ env.WEBCODEX_CI_BUILT_AT }}
cache-from: type=gha,scope=server-image-ci-amd64
# GHA cache export is a performance optimization, not image evidence.
# BuildKit can transiently return layer-blob not_found after the image
# itself has built; keep build/load/smoke authoritative in that case.
cache-to: type=gha,mode=max,scope=server-image-ci-amd64,ignore-error=true
provenance: false
sbom: false
- name: Smoke amd64 Server image
shell: bash
run: |
set -euo pipefail
image="$WEBCODEX_CI_IMAGE"
test "$(docker image inspect --format '{{.Architecture}}' "$image")" = amd64
test "$(docker image inspect --format '{{.Config.User}}' "$image")" = "webcodex:webcodex"
docker run --rm --entrypoint sh "$image" -ec '
test "$(id -u)" = 10001
test "$(id -g)" = 10001
test ! -e /usr/local/bin/webcodex-runner
'
expected_server="webcodex-server $WEBCODEX_CI_VERSION (commit $WEBCODEX_CI_SOURCE_SHORT, dirty=false, built_at=$WEBCODEX_CI_BUILT_AT)"
expected_cli="webcodex $WEBCODEX_CI_VERSION (commit $WEBCODEX_CI_SOURCE_SHORT, dirty=false, built_at=$WEBCODEX_CI_BUILT_AT)"
test "$(docker run --rm "$image" --version)" = "$expected_server"
test "$(docker run --rm --entrypoint /usr/local/bin/webcodex "$image" --version)" = "$expected_cli"
name="webcodex-server-ci-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true; }
trap cleanup EXIT
docker run -d --name "$name" \
-e WEBCODEX_TOKEN=0000000000000000000000000000000000000000000000000000000000000000 \
-e WEBCODEX_PUBLIC_URL=https://webcodex.invalid \
"$image" >/dev/null
for _ in $(seq 1 40); do
health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$name")"
if [ "$health" = healthy ]; then break; fi
if [ "$health" = unhealthy ] || [ "$health" = missing ]; then
docker logs --tail 80 "$name" >&2 || true
exit 1
fi
sleep 2
done
test "$(docker inspect --format '{{.State.Health.Status}}' "$name")" = healthy
test-macos-core:
needs: changes
if: needs.changes.outputs.needs_macos == 'true'
# Daily macOS coverage uses Apple Silicon. Intel is intentionally reserved for
# explicit/release-time extended-native validation.
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
runner: macos-15
host_arch: arm64
rust_host: aarch64-apple-darwin
runs-on: ${{ matrix.runner }}
timeout-minutes: 75
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: macos-core-ci-${{ matrix.platform }}
- name: Verify native macOS host
env:
EXPECTED_HOST_ARCH: ${{ matrix.host_arch }}
EXPECTED_RUST_HOST: ${{ matrix.rust_host }}
run: |
test "$(uname -m)" = "$EXPECTED_HOST_ARCH"
rust_host="$(rustc -vV | sed -n 's/^host: //p')"
test "$rust_host" = "$EXPECTED_RUST_HOST"
- name: Check macOS workspace production surfaces without warnings
run: python3 scripts/check_production_warnings.py
- name: Upload macOS production build evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: production-macos-${{ github.sha }}-${{ github.run_attempt }}
path: target/ci-build-reports/*.json
retention-days: 7
- name: Run native macOS Runner and Computer tests
run: cargo test --locked -p webcodex-runner -p webcodex-lsp -p webcodex-computer --features runner-real-process-tests
test-macos-desktop:
needs: changes
if: needs.changes.outputs.needs_macos_desktop == 'true'
# Keep the common Apple-Silicon Desktop package validation path-aware. The
# corresponding Intel DMG build/smoke lives in extended-native CI.
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
runner: macos-15
host_arch: arm64
rust_host: aarch64-apple-darwin
runs-on: ${{ matrix.runner }}
timeout-minutes: 75
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: macos-desktop-ci-${{ matrix.platform }}
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
frontend/package-lock.json
apps/desktop/package-lock.json
- name: Verify native macOS host
env:
EXPECTED_HOST_ARCH: ${{ matrix.host_arch }}
EXPECTED_RUST_HOST: ${{ matrix.rust_host }}
run: |
test "$(uname -m)" = "$EXPECTED_HOST_ARCH"
rust_host="$(rustc -vV | sed -n 's/^host: //p')"
test "$rust_host" = "$EXPECTED_RUST_HOST"
- name: Probe native DMG services before compiling Desktop
run: bash scripts/macos_finalize_dmg.sh --preflight
- name: Install shared frontend dependencies
run: npm ci --prefix frontend
- name: Install Desktop dependencies
run: npm ci --prefix apps/desktop
- name: Check Desktop frontend
run: |
npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop test
npm --prefix apps/desktop run build
- name: Check Desktop Rust
run: |
cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets
cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml
- name: Build exact native WebCodex runtime for Desktop
id: desktop_runtime
env:
EXPECTED_FILE_ARCH: ${{ matrix.host_arch }}
run: |
set -euo pipefail
source_sha="$(git rev-parse HEAD)"
short_source="$(git rev-parse --short=12 HEAD)"
version="$(node -p "require('./npm/webcodex/package.json').version")"
built_at="$(git show -s --format=%ct HEAD)"
export WEBCODEX_BUILT_AT="$built_at"
export WEBCODEX_GIT_COMMIT="$source_sha"
export WEBCODEX_GIT_DIRTY=false
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner
for name in webcodex webcodex-server webcodex-runner; do
binary="target/dogfood/$name"
actual="$($binary --version)"
expected="$name $version (commit $source_sha, dirty=false, built_at=$built_at)"
test "$actual" = "$expected"
test "$(/usr/bin/lipo -archs "$binary")" = "$EXPECTED_FILE_ARCH"
done
bash scripts/macos_sign_runner.sh target/dogfood/webcodex-runner -
{
echo "source=$source_sha"
echo "short=$short_source"
echo "version=$version"
echo "built_at=$built_at"
} >> "$GITHUB_OUTPUT"
- name: Stage exact runtime for native macOS Desktop
id: desktop_bundle
run: |
set -euo pipefail
stage="$RUNNER_TEMP/webcodex-desktop-${{ matrix.platform }}-bundle"
python3 scripts/prepare_desktop_bundle_macos.py \
--bin-dir target/dogfood \
--version "${{ steps.desktop_runtime.outputs.version }}" \
--source-sha "${{ steps.desktop_runtime.outputs.source }}" \
--built-at "${{ steps.desktop_runtime.outputs.built_at }}" \
--platform "${{ matrix.platform }}" \
--signing-mode adhoc \
--output-dir "$stage"
{
echo "config=$stage/tauri.bundle.conf.json"
echo "metadata=$stage/desktop-bundle.json"
} >> "$GITHUB_OUTPUT"
- name: Build native ad-hoc macOS Desktop DMG
id: desktop_dmg
working-directory: apps/desktop
env:
CI: "true"
APPLE_SIGNING_IDENTITY: "-"
run: |
set -euo pipefail
export CARGO_TARGET_DIR="$RUNNER_TEMP/webcodex-desktop-${{ matrix.platform }}-tauri"
npm exec tauri -- build \
--bundles dmg \
--config "${{ steps.desktop_bundle.outputs.config }}" \
--ci -- --locked
bundle_dir="$CARGO_TARGET_DIR/release/bundle/dmg"
shopt -s nullglob
candidates=("$bundle_dir"/*.dmg)
test "${#candidates[@]}" -eq 1
dmg="$RUNNER_TEMP/webcodex-desktop-ci-${{ steps.desktop_runtime.outputs.short }}-v${{ steps.desktop_runtime.outputs.version }}-${{ matrix.platform }}.dmg"
cp "${candidates[0]}" "$dmg"
echo "path=$dmg" >> "$GITHUB_OUTPUT"
- name: Smoke native macOS Desktop DMG
run: |
set -euo pipefail
bash scripts/desktop_install_macos_smoke.sh \
--dmg "${{ steps.desktop_dmg.outputs.path }}" \
--version "${{ steps.desktop_runtime.outputs.version }}" \
--source-sha "${{ steps.desktop_runtime.outputs.source }}" \
--built-at "${{ steps.desktop_runtime.outputs.built_at }}" \
--platform "${{ matrix.platform }}" \
--stage-metadata "${{ steps.desktop_bundle.outputs.metadata }}" \
--signing-mode adhoc
test-macos:
needs: [contract, changes, test-macos-core, test-macos-desktop]
# Stable aggregate context: every configured macOS sublane must either succeed
# when required by policy or be intentionally skipped when not required.
# Workflow cancellation is distinct from needs.<lane>.result. Superseded
# runs stop; a required child failure/cancellation is still rejected below.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate macOS lane policy
env:
CONTRACT_RESULT: ${{ needs.contract.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
NEEDS_CORE: ${{ needs.changes.outputs.needs_macos }}
NEEDS_DESKTOP: ${{ needs.changes.outputs.needs_macos_desktop }}
CORE_RESULT: ${{ needs.test-macos-core.result }}
DESKTOP_RESULT: ${{ needs.test-macos-desktop.result }}
run: |
set -euo pipefail
echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT core=$CORE_RESULT desktop=$DESKTOP_RESULT needs_core=$NEEDS_CORE needs_desktop=$NEEDS_DESKTOP"
if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ]; then
echo "CI policy or contract classification did not succeed" >&2
exit 1
fi
require_lane() {
label="$1"
required="$2"
actual="$3"
expected=skipped
if [ "$required" = true ]; then expected=success; fi
if [ "$actual" != "$expected" ]; then
echo "$label macOS lane expected $expected, got $actual" >&2
exit 1
fi
}
require_lane core "$NEEDS_CORE" "$CORE_RESULT"
require_lane desktop "$NEEDS_DESKTOP" "$DESKTOP_RESULT"
# Ordinary Runner/Computer fixtures must tolerate default libtest parallelism.
# Timing-sensitive lifecycle/process-tree fixtures remain ignored here and run only
# by explicit local commands. Core/library, Runner, and packaging coverage are independent.
test-windows-core:
needs: changes
if: needs.changes.outputs.needs_windows_core == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12'
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: windows-core-ci
- name: Configure PowerShell 7 persistent-shell tests
shell: pwsh
run: |
$pwsh = (Get-Command pwsh -ErrorAction Stop).Source
"WEBCODEX_TEST_PWSH=$pwsh" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Run Windows persistent-shell tests (PowerShell 5.1 + PowerShell 7)
run: cargo test --locked -p webcodex-persistent-shell
- name: Check Windows workspace production surfaces without warnings
run: python scripts/check_production_warnings.py
- name: Upload Windows production build evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: production-windows-${{ github.sha }}-${{ github.run_attempt }}
path: target/ci-build-reports/*.json
retention-days: 7
- name: Run Windows library and CLI tests
run: cargo test --locked -p webcodex-runner-config -p webcodex-process -p webcodex-cli
- name: Test user-session task ownership after Windows account normalization
run: |
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -File scripts/tests/test_windows_user_service_identity.ps1
if ($LASTEXITCODE -ne 0) { throw "Windows user-session task ownership regression failed" }
- name: Run Windows unified upgrade transactions including native v0.4.3 fixture
run: |
cargo test --locked -p webcodex-environment --lib upgrade::
if ($LASTEXITCODE -ne 0) { throw "Windows upgrade tests failed" }
cargo test --locked -p webcodex-environment --lib service::windows::
if ($LASTEXITCODE -ne 0) { throw "Windows service tests failed" }
- name: Test Computer helper ownership and native Task Scheduler roundtrip
run: |
cargo test --locked -p webcodex-environment --lib session_service::
if ($LASTEXITCODE -ne 0) { throw "Computer helper ownership tests failed" }
cargo test --locked -p webcodex-environment --lib windows_helper_task_scheduler_roundtrip_is_owned -- --ignored
if ($LASTEXITCODE -ne 0) { throw "Computer helper Task Scheduler roundtrip failed" }
- name: Test Windows unified installer routing and Desktop build freshness
run: python -m unittest scripts.tests.test_prepare_windows_unified_installer scripts.tests.test_desktop_runtime_manifest scripts.tests.test_desktop_frontend_build
- name: Test optional Codex observation adapter on Windows
run: python -m unittest discover -s integrations/codex -p 'test_*.py' -v
- name: Run Windows search fallback tests
run: |
cargo test --locked -p webcodex --lib search_project_text_grep
if ($LASTEXITCODE -ne 0) { throw "Windows grep fallback tests failed" }
cargo test --locked -p webcodex --lib search_requires_ripgrep_based_on_effective_features_not_field_presence
if ($LASTEXITCODE -ne 0) { throw "Windows search capability test failed" }
test-windows-runner:
needs: changes
if: needs.changes.outputs.needs_windows_runner == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: windows-runner-ci
- name: Run Windows Runner and Computer tests
# This lane starts real PowerShell/MCP child processes. Keep the Rust
# harness bounded so hosted-runner scheduling does not consume their
# production startup/request deadlines.
env:
RUST_TEST_THREADS: "2"
run: cargo test --locked -p webcodex-runner -p webcodex-lsp -p webcodex-computer --features runner-real-process-tests
test-windows-package:
needs: changes
if: needs.changes.outputs.needs_windows_package == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: windows-package-ci
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
- name: Test npm installer and release manifest natively
run: npm --prefix npm/webcodex test
- name: Windows artifact to npm install smoke
# Builds the three Windows binaries, packages the win32-x64 artifact
# with the native PowerShell script, installs it through the npm
# installer into a temporary prefix, and verifies the wrapper plus
# --version/--help and the failure-preserves-old-binaries path. Never
# touches the npm registry.
shell: pwsh
run: powershell -NoProfile -ExecutionPolicy Bypass -File scripts/npm_install_windows_smoke.ps1
test-windows-desktop:
needs: changes
if: needs.changes.outputs.needs_windows_desktop == 'true'
runs-on: windows-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
shared-key: windows-desktop-ci
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
cache-dependency-path: |
frontend/package-lock.json
apps/desktop/package-lock.json
- name: Install shared frontend dependencies
run: npm ci --prefix frontend
- name: Install Desktop dependencies
run: npm ci --prefix apps/desktop
- name: Check Desktop frontend
run: |
npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop test
npm --prefix apps/desktop run build
- name: Check Desktop Rust
shell: pwsh
run: |
cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets
if ($LASTEXITCODE -ne 0) { throw "Desktop cargo check failed" }
cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml
if ($LASTEXITCODE -ne 0) { throw "Desktop cargo test failed" }
- name: Build exact local WebCodex runtime
id: runtime
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$source = (git rev-parse HEAD).Trim()
$short = (git rev-parse --short=12 HEAD).Trim()
$version = (node -e "console.log(require('./npm/webcodex/package.json').version)").Trim()
$builtAt = [Int64]((git show -s --format=%ct HEAD).Trim())
$env:WEBCODEX_BUILT_AT = "$builtAt"
$env:WEBCODEX_GIT_COMMIT = $source
$env:WEBCODEX_GIT_DIRTY = "false"
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner
if ($LASTEXITCODE -ne 0) { throw "WebCodex runtime build failed" }
foreach ($name in @("webcodex", "webcodex-server", "webcodex-runner")) {
$binary = Join-Path "target\dogfood" "$name.exe"
$output = @(& $binary --version)
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0 -or $output.Count -eq 0 -or -not $output[0]) {
throw "$name --version failed with exit code $exitCode"
}
$actual = $output[0].TrimEnd()
$expected = "$name $version (commit $source, dirty=false, built_at=$builtAt)"
if ($actual -ne $expected) { throw "Unexpected $name identity: $actual" }
}
"source=$source" >> $env:GITHUB_OUTPUT
"short=$short" >> $env:GITHUB_OUTPUT
"version=$version" >> $env:GITHUB_OUTPUT
"built_at=$builtAt" >> $env:GITHUB_OUTPUT
- name: Stage exact runtime for Desktop bundle
id: desktop_bundle
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$stage = Join-Path $env:RUNNER_TEMP "webcodex-desktop-ci-bundle"
& .\scripts\prepare_desktop_bundle.ps1 `
-BinDir "target\dogfood" `
-Version "${{ steps.runtime.outputs.version }}" `
-SourceSha "${{ steps.runtime.outputs.source }}" `
-BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") `
-OutputDir $stage
"config=$(Join-Path $stage 'tauri.bundle.conf.json')" >> $env:GITHUB_OUTPUT
- name: Build unsigned NSIS installer
working-directory: apps/desktop
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$env:CARGO_TARGET_DIR = Join-Path $env:GITHUB_WORKSPACE "target\desktop-ci-tauri"
npm exec tauri -- build --bundles nsis --config "${{ steps.desktop_bundle.outputs.config }}" --ci --no-sign
if ($LASTEXITCODE -ne 0) { throw "Tauri NSIS build failed" }
$bundleDir = Join-Path $env:CARGO_TARGET_DIR "release\bundle\nsis"
$built = @(Get-ChildItem -LiteralPath $bundleDir -Filter "*-setup.exe" -File)
if ($built.Count -ne 1) { throw "Expected exactly one NSIS installer, found $($built.Count)" }
New-Item -ItemType Directory -Force -Path "target\desktop-ci-dist" | Out-Null
$installer = Join-Path "target\desktop-ci-dist" "webcodex-desktop-ci-${{ steps.runtime.outputs.short }}-v${{ steps.runtime.outputs.version }}-win32-x64-setup.exe"
Copy-Item -LiteralPath $built[0].FullName -Destination $installer
"DESKTOP_CI_INSTALLER=$([System.IO.Path]::GetFullPath($installer))" >> $env:GITHUB_ENV
- name: Smoke newly-built Desktop installer
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
& .\scripts\desktop_install_windows_smoke.ps1 `
-Installer $env:DESKTOP_CI_INSTALLER `
-Version "${{ steps.runtime.outputs.version }}" `
-SourceSha "${{ steps.runtime.outputs.source }}" `
-BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") `
-Platform win32-x64
$customInstallDir = Join-Path $env:RUNNER_TEMP "WebCodex Custom Install"
& .\scripts\desktop_install_windows_smoke.ps1 `
-Installer $env:DESKTOP_CI_INSTALLER `
-Version "${{ steps.runtime.outputs.version }}" `
-SourceSha "${{ steps.runtime.outputs.source }}" `
-BuiltAt ([Int64]"${{ steps.runtime.outputs.built_at }}") `
-Platform win32-x64 `
-InstallDir $customInstallDir
test-windows:
needs: [contract, changes, test-windows-core, test-windows-runner, test-windows-package, test-windows-desktop]
# Preserve the stable Windows aggregate while allowing each heavy lane to be
# selected independently by the deterministic path-risk classifier.
# Workflow cancellation is distinct from needs.<lane>.result. Superseded
# runs stop; a required child failure/cancellation is still rejected below.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate Windows lane policy
env:
CONTRACT_RESULT: ${{ needs.contract.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
NEEDS_CORE: ${{ needs.changes.outputs.needs_windows_core }}
NEEDS_RUNNER: ${{ needs.changes.outputs.needs_windows_runner }}
NEEDS_PACKAGE: ${{ needs.changes.outputs.needs_windows_package }}
NEEDS_DESKTOP: ${{ needs.changes.outputs.needs_windows_desktop }}
CORE_RESULT: ${{ needs.test-windows-core.result }}
RUNNER_RESULT: ${{ needs.test-windows-runner.result }}
PACKAGE_RESULT: ${{ needs.test-windows-package.result }}
DESKTOP_RESULT: ${{ needs.test-windows-desktop.result }}
run: |
set -euo pipefail
echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT core=$CORE_RESULT runner=$RUNNER_RESULT package=$PACKAGE_RESULT desktop=$DESKTOP_RESULT"
if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ]; then
echo "CI policy or contract classification did not succeed" >&2
exit 1
fi
require_lane() {
label="$1"
required="$2"
actual="$3"
expected=skipped
if [ "$required" = true ]; then expected=success; fi
if [ "$actual" != "$expected" ]; then
echo "$label Windows lane expected $expected, got $actual" >&2
exit 1
fi
}
require_lane core "$NEEDS_CORE" "$CORE_RESULT"
require_lane runner "$NEEDS_RUNNER" "$RUNNER_RESULT"
require_lane package "$NEEDS_PACKAGE" "$PACKAGE_RESULT"
require_lane desktop "$NEEDS_DESKTOP" "$DESKTOP_RESULT"
test-native:
needs: [contract, changes, mcp-conformance, test-docker-server, test-macos, test-windows]
# Stable branch-protection context: daily native aggregates and the path-aware
# Server image smoke must resolve according to classifier policy.
# Workflow cancellation is distinct from needs.<lane>.result. Superseded
# runs stop; a required child failure/cancellation is still rejected below.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Validate native lane policy
env:
CONTRACT_RESULT: ${{ needs.contract.result }}
CHANGES_RESULT: ${{ needs.changes.result }}
NEEDS_DOCKER: ${{ needs.changes.outputs.needs_docker }}
MCP_CONFORMANCE_RESULT: ${{ needs.mcp-conformance.result }}
DOCKER_RESULT: ${{ needs.test-docker-server.result }}
MACOS_RESULT: ${{ needs.test-macos.result }}
WINDOWS_RESULT: ${{ needs.test-windows.result }}
run: |
set -euo pipefail
echo "changes=$CHANGES_RESULT contract=$CONTRACT_RESULT mcp_conformance=$MCP_CONFORMANCE_RESULT docker=$DOCKER_RESULT macos=$MACOS_RESULT windows=$WINDOWS_RESULT needs_docker=$NEEDS_DOCKER"
if [ "$CHANGES_RESULT" != success ] || [ "$CONTRACT_RESULT" != success ] || [ "$MCP_CONFORMANCE_RESULT" != success ]; then
echo "CI policy or contract classification did not succeed" >&2
exit 1
fi
if [ "$MACOS_RESULT" != success ] || [ "$WINDOWS_RESULT" != success ]; then
echo "native platform aggregate failed" >&2
exit 1
fi
expected_docker=skipped
if [ "$NEEDS_DOCKER" = true ]; then expected_docker=success; fi
if [ "$DOCKER_RESULT" != "$expected_docker" ]; then
echo "Server image lane expected $expected_docker, got $DOCKER_RESULT" >&2
exit 1
fi