Repository navigation
951 lines (832 loc) · 43 KB
/
Copy pathci.yml
File metadata and controls
951 lines (832 loc) · 43 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
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
security-events: write
env:
# Every uv command in this workflow (including the ones the Makefile runs)
# must execute against the committed uv.lock and fail if pyproject.toml has
# drifted from it. The per-command --locked flags below say the same thing
# where a reader looks; this catches the `uv run` / `uv export` sites that
# have no flag. Measured on uv 0.12: the env var alone rejects a stale lock
# for sync, run and export; UV_FROZEN would install the OLD lock silently;
# and a flag beats the variable in either direction (uv prints "Ignoring
# UV_LOCKED because --frozen was provided" and proceeds), so the flags
# below are --locked, not --frozen, and must stay that way.
UV_LOCKED: "1"
jobs:
test:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --locked --all-extras --dev
- name: Install Trivy for inventory regressions
if: matrix.python-version == '3.12'
uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1
with:
version: v0.70.0
- name: Run unit tests with coverage
# CAO_MUST_RUN names the assurance gates this leg owns; a `must_run`
# test for a listed gate that skips or gets deselected fails the session
# instead of passing silently (see test/_must_run.py). Only the 3.12 leg
# installs Trivy, so only the 3.12 leg claims the "unit" gate.
env:
CAO_MUST_RUN: ${{ matrix.python-version == '3.12' && 'unit' || '' }}
run: |
if [ "${{ matrix.python-version }}" = "3.12" ]; then
trivy --version
fi
# CI intentionally includes integration-marked tests except for the Kiro
# provider test, which requires an authenticated external CLI.
uv run pytest test/ examples/workflow/tests/ examples/skills/tests/ \
--ignore=test/providers/test_kiro_cli_integration.py \
--ignore=test/e2e \
-m "not e2e" \
--cov=src/cli_agent_orchestrator \
--cov-branch \
--cov-report=xml \
--cov-fail-under=80 \
--cov-report=term-missing \
-v
# --cov-branch (#1005) also makes --cov-fail-under read the BLENDED
# line+branch figure rather than line coverage. Whether that reads harder
# or softer than line-only depends on whether branch % is below or above
# line % that run (today: 89.28 branch vs 93.10 line, so blended is a
# harder bar at 80 -- not an invariant, just the current numbers). The
# authoritative, separately-floored line metric is the ratchet's
# (.coverage-baseline.json, python.floor_pct = 87.0) in the cao-mcp-apps
# job below. If the blend lands under 80 on a first run, lower THIS
# number -- do not drop the branch data, and do not touch the line floor.
- name: Validate Markdown links
run: uv run python scripts/validate_markdown_links.py
# Agent Plugins 1.0.0 conformance guards. #573 requires the schemas
# validate on every PR; both steps are the same `make` targets a
# developer runs locally, so CI and pre-push execute identical code.
- name: Check Agent Plugins schema pin
run: make check-agent-plugins-schemas
# Also the on-every-PR validation of CAO's own manifests against the
# pinned schemas: the drift check runs the real validator over both
# committed packages.
- name: Check CAO agent plugin packages
run: make check-agent-plugin
- name: Upload coverage to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
web-build:
name: Web UI Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Install dependencies
run: npm ci
working-directory: web
- name: Type check
run: npx tsc --noEmit
working-directory: web
- name: Run tests
run: npm test
working-directory: web
- name: Build
run: npm run build
working-directory: web
# =================================================================================
# WHEEL-CONTENTS GATE (issues #610, #705) — this job is the ONLY place in CI where
# test/test_wheel_contents.py can execute at all.
#
# `Unit Tests` runs pytest but never builds the web UI, and this job built the web UI
# but ran no pytest. test_wheel_contents.py is gated on the built web UI existing, so
# dropped into `Unit Tests` it reports SKIPPED in all three legs and no job in the
# workflow runs it. Here it runs exactly once.
#
# WHY HERE and not in `Unit Tests`: the property under test is Python-version
# independent (does hatchling's `[tool.hatch.build] artifacts` glob cover the whole
# web_ui/ build output), so running it on 3.11/3.12 would assert the same thing
# twice and build the web UI twice to do it. It also keeps the repo's
# primary test gate free of an npm-registry dependency. Follows the `CAO MCP Apps`
# job's existing in-file pattern: a Node build plus setup-uv/uv sync plus targeted
# pytest files.
#
# SCOPE: the pytest step names its files EXPLICITLY, so it closes the gap for THOSE
# FILES and not for the class. Another web_ui-dependent test added elsewhere under
# test/ would still skip in `Unit Tests` and would not be collected here; wiring it
# in means naming it in this step too.
#
# test_hatch_artifacts_guard.py is listed alongside it, but is NOT dependent on this
# job: it never skips, so `Unit Tests` already runs it on 3.11/3.12. It is named
# here because this is the only job where `web_ui/` exists on disk, which is the only
# place its whole-directory wheel assertion can cover THAT root rather than skipping it.
# =================================================================================
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
# `--all-extras --dev`, not plain `uv sync`: `agui` is an optional extra and every
# other install site in this file passes both flags. Plain `uv sync` omits it and
# yields ~58 spurious agui collection failures.
- name: Install Python deps (for the wheel-contents packaging test)
run: uv sync --locked --all-extras --dev
- name: "Wheel-contents regression test (issues #610, #705)"
# The skip-guard is the load-bearing half. test_wheel_contents.py SKIPS when the
# built web UI is absent, and a skip keeps this job GREEN — the same false
# assurance the test exists to remove. `npm run build` exiting 0 while writing an
# incomplete tree is a failure this step exists to catch, so assert the
# precondition instead of trusting it.
#
# The guard calls `web_ui_build_is_complete()` from the test module rather than
# re-testing paths in shell: one source of truth, and it checks index.html AND
# every asset index.html loads (#705), not just the entry point.
#
# That import resolves `test` from the CURRENT DIRECTORY, so this step must stay at
# the repo root. The sibling steps in this job set `working-directory: web`; adding
# it here would fail with `ModuleNotFoundError: No module named 'test'`.
#
# Only these two files: the full suite already runs in `Unit Tests`, and the
# wheel build here is ~2s, so duplicating the suite would be pure waste.
run: |
uv run python -c 'import test.test_wheel_contents as t; raise SystemExit(0 if t.web_ui_build_is_complete() else 1)' || {
echo "The built web UI under src/cli_agent_orchestrator/web_ui/ is absent or incomplete" >&2
echo "after 'npm run build' (index.html missing, or it references assets that were" >&2
echo "not written). test/test_wheel_contents.py would SKIP rather than run, leaving" >&2
echo "this job green with the packaging assertion unexecuted (issues #610, #705)." >&2
exit 1
}
uv run pytest test/test_wheel_contents.py test/test_hatch_artifacts_guard.py --no-cov -q
aidlc-portfolio-example:
name: AI-DLC Portfolio Example
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.3.10"
- name: Install dependencies
run: bun install --frozen-lockfile
working-directory: examples/aidlc-portfolio
- name: Type check and test
run: bun run check
working-directory: examples/aidlc-portfolio
cao-mcp-apps:
name: CAO MCP Apps
runs-on: ubuntu-latest
# Per-tier test timing budgets (Requirement 20.3), surfaced as env so the
# vitest tiers (Phase II+) and CI can enforce them:
# Unit < 5s, Component < 8s, Integration < 15s, E2E < 60s.
env:
CAO_TIER_TIMEOUT_UNIT_MS: "5000"
CAO_TIER_TIMEOUT_COMPONENT_MS: "8000"
CAO_TIER_TIMEOUT_INTEGRATION_MS: "15000"
CAO_TIER_TIMEOUT_E2E_MS: "60000"
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
- name: Set up Python
run: uv python install 3.12
- name: Install Python deps (for the HTTP-only guard + coverage ratchet)
run: uv sync --locked --all-extras --dev
- name: Install MCP-apps deps
run: npm ci
working-directory: cao_mcp_apps
# Was wrapped in `if ls src/**/*.ts src/**/*.tsx; then ... else echo "no
# TypeScript sources yet (Phase 0)"; fi`. Phase 0 is over: there are 29
# .ts/.tsx files under src/{agent,dashboard,event-stream,graph,shared,test},
# so the else branch is dead. It was also fragile in the direction this PR
# cares about -- bash does not expand `**` recursively without `globstar`,
# so `src/**/*.ts` only ever matched one level down, and moving the sources
# one directory deeper would have silently turned tsc into an echo.
- name: Type check
working-directory: cao_mcp_apps
run: npm run typecheck
- name: Unit tests
working-directory: cao_mcp_apps
# --coverage emits coverage/coverage-summary.json for the ratchet floor.
run: npx vitest run --coverage --passWithNoTests
- name: Build single-file bundles
working-directory: cao_mcp_apps
run: npm run build:all
- name: JIT-free deny-list scan
working-directory: cao_mcp_apps
run: npm run scan:jit
- name: Bundle-size budget
working-directory: cao_mcp_apps
run: npm run check:size
- name: HTTP-only MCP boundary guard
run: uv run pytest test/test_http_only_boundary.py -q
- name: Backend coverage (for the ratchet floor)
# Produces coverage.json (python line + branch floors) consumed by
# coverage:ratchet. --cov-branch adds num_branches/covered_branches to
# totals; the ratchet floors line and branch SEPARATELY (#1005).
run: |
uv run pytest test/ examples/workflow/tests/ examples/skills/tests/ \
--ignore=test/providers/test_kiro_cli_integration.py \
--ignore=test/e2e \
-m "not e2e" \
--cov=src/cli_agent_orchestrator \
--cov-branch \
--cov-report=json \
--cov-fail-under=80 \
-q
- name: Coverage ratchet
working-directory: cao_mcp_apps
run: npm run coverage:ratchet
cao-mcp-apps-e2e:
name: CAO MCP Apps E2E (Playwright)
runs-on: ubuntu-latest
# E2E tier budget (Requirement 20.3): 60s per test (enforced in
# playwright.config.ts via `timeout: 60_000`).
env:
CAO_TIER_TIMEOUT_E2E_MS: "60000"
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Install MCP-apps deps
run: npm ci
working-directory: cao_mcp_apps
- name: Install Playwright browser (chromium)
run: npm run test:e2e:install
working-directory: cao_mcp_apps
- name: Run Playwright E2E
# playwright.config.ts builds the single-file bundles and starts the
# harness server (e2e/server.mjs) on 127.0.0.1:9889 before the run.
run: npm run test:e2e
working-directory: cao_mcp_apps
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync --locked --all-extras --dev
- name: Check code formatting with black
run: uv run black --check src/ test/ examples/robotics/cross-zone-transport/ examples/robotics/hierarchical-factory/ examples/skills/
- name: Check import sorting with isort
run: uv run isort --check-only src/ test/ examples/robotics/cross-zone-transport/ examples/robotics/hierarchical-factory/ examples/skills/
# The dead-code gate (#1005). BLOCKING, like the mypy ratchet step below:
# the selected rules (F401 unused import, F841 unused local, F821/F822
# undefined name -- a removed import still used in the SAME file; ruff is
# per-file, cross-module uses are the import guard's and the unit tests'
# job) are green on main as of this change, so any new finding is
# introduced by the PR under test.
#
# Scope is wider than black/isort above on purpose -- all of examples/, not
# just the robotics example -- because an unread binding in an example is
# still an unread binding, and ruff has nothing to say about formatting so
# the broader scope costs no churn. Rule selection and the deliberately
# deferred rules (F811, ARG, B) are documented in pyproject.toml.
- name: Lint for dead code with ruff
run: uv run ruff check src/ test/ examples/
- name: Run type checker with mypy (ratchet)
# Issue #1005. This step used to be `uv run mypy src/` with
# `continue-on-error: true`, so a red type check blocked nothing and the
# repo's 177 pre-existing errors were indistinguishable from 178.
#
# The ratchet blocks on NEW errors only, measured per file per error code
# against the committed `mypy-baseline.json`. Pre-existing errors pass,
# counts that fall pass (and the step tells you to bank them with
# `--update`), counts that rise fail. The full `mypy src/` output is
# echoed to this log before the verdict.
#
# Deliberately NO `continue-on-error` here: that key is the entire
# difference between a gate and a decoration, and
# test/scripts/test_mypy_ratchet.py asserts it stays absent.
run: uv run python scripts/mypy_ratchet.py
ag-ui-demo:
name: AG-UI demo (shift-left recording)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (cao-server for the live AG-UI surface)
run: uv sync --locked --all-extras --dev
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Install ffmpeg (GIF export)
run: sudo apt-get update && sudo apt-get install -y ffmpeg
- name: Install recorder deps
run: npm ci
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Install Playwright browser (chromium)
run: npm run playwright:install
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Record demo (renders the six components + refusal; gates on drift)
# The recorder boots a CAO_AGUI_ENABLED cao-server + a static server,
# drives the live generative-UI flow, ASSERTS every component renders and
# the off-list component is refused (non-zero exit on drift — shift-left),
# and exports docs/media/ag-ui-eventsource-viewer-demo.gif.
run: npm run record
working-directory: examples/ag-ui/ag-ui-eventsource-viewer/tools
- name: Upload demo GIF artifact
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ag-ui-eventsource-viewer-demo
path: docs/media/ag-ui-eventsource-viewer-demo.gif
if-no-files-found: warn
continue-on-error: true
ag-ui-construct-demos:
name: AG-UI construct demos (shift-left recordings)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (constructs + examples run under uv)
run: uv sync --locked --all-extras --dev
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Install recorder deps
# ffmpeg is provided by the ffmpeg-static npm package (gif-capable), so
# no apt install is needed here.
run: npm ci
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Install Playwright browser (chromium)
run: npm run playwright:install
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Record per-feature demos (asserts each construct; gates on drift)
# For each of the four L2 constructs, runs its asserting example in
# offline/synthetic mode and exports docs/media/ag-ui-<slug>-demo.gif.
# A non-zero example exit (a construct regressed) fails this job —
# this is the shift-left gate.
run: npm run record
working-directory: examples/ag-ui/ag-ui-construct-demos/tools
- name: Upload construct demo GIFs artifact
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ag-ui-construct-demos
path: docs/media/ag-ui-*-demo.gif
if-no-files-found: warn
continue-on-error: true
agent-plugins-dogfood:
name: Agent Plugins dog-food (shift-left recording)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (cao runs the dog-food pipeline under uv)
run: uv sync --locked --all-extras --dev
- name: Set up Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Install recorder deps
# ffmpeg is provided by the ffmpeg-static npm package (gif-capable), so
# no apt install is needed here.
run: npm ci
working-directory: examples/agent-plugins/agent-plugins-dogfood/tools
- name: Install Playwright browser (chromium)
run: npm run playwright:install
working-directory: examples/agent-plugins/agent-plugins-dogfood/tools
- name: Record dog-food demo (asserts the plugin pipeline; gates on drift)
# Runs run.sh OFFLINE (no provider binary, network, or secrets) — CAO
# installing its own `cao` plugin through its own pipeline — and asserts
# validate/add/install(Kiro+OpenCode)/remove, exporting
# docs/media/agent-plugins-dogfood-demo.gif. A non-zero exit or a missing
# PASS marker (the pipeline regressed) fails this job — the shift-left
# gate. The live OpenCode observational step (CAO_DOGFOOD_LIVE=1, needs
# the opencode binary) is deliberately NOT set here.
run: npm run record
working-directory: examples/agent-plugins/agent-plugins-dogfood/tools
- name: Upload dog-food demo GIF artifact
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: agent-plugins-dogfood
path: docs/media/agent-plugins-dogfood-demo.gif
if-no-files-found: warn
continue-on-error: true
ag-ui-stock-client-live:
name: AG-UI stock-client live (AC3)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies (cao-server + agui run plane)
run: uv sync --locked --all-extras --dev
- name: Set up Node.js (for the @ag-ui/client stock SDK)
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "20"
- name: Run stock-client live demo (asserts >=1 post-connect frame)
# Boots a CAO_AGUI_ENABLED cao-server with a keyless mock_cli fleet,
# POSTs a RunAgentInput to /agui/v1/run with the pinned @ag-ui/client
# HttpAgent, and asserts a live frame is received (AC3 / spec task 17.2).
# Non-zero exit fails the job; the script cleans up the server on exit.
run: ./examples/ag-ui/ag-ui-stock-client-live/run.sh
# =======================================================================================
# RUST TUI (issue #321, unit `rust-ci`). THE FIRST JOB IN ci.yml THAT INSPECTS THE RUST
# CRATE AT ALL — measured before this change: `cargo` appeared in only
# publish-to-pypi.yml, never here.
#
# So until this job existed, the crate's 46 tests across 6 targets were green ONLY because
# they had been run by hand. Nothing in CI would have noticed a regression, a clippy
# warning, a cargo-deny advisory, or an FFI dependency appearing in the graph.
#
# ALSO THIS FILE'S FIRST NON-LINUX RUNNER. `wheel-matrix` added macOS/Windows runners to
# the publish workflow; every `runs-on:` in ci.yml was ubuntu-latest.
# =======================================================================================
rust:
name: Rust TUI (${{ matrix.label }})
runs-on: ${{ matrix.os }}
# TS-2 and team.md: every job running a pty test carries an explicit `timeout-minutes`.
# ci.yml had ZERO before this job (the 6 in the repo are all wheel-matrix's, in the
# publish workflow), so a deadlocked pty test would have burned the 6-hour default with
# no diagnosis.
#
# 20 minutes is chosen against measurement, not by feel. Locally, from a COLD target dir:
# 9.6s to build all six test targets, 8.0s to build-and-run the suite, and the pty target
# itself takes 2.12s. A cold CI runner with no cargo cache is slower — toolchain install,
# crate downloads, and a debug build of 39 crates — but that is a minutes-scale budget,
# not a tens-of-minutes one. 20 minutes leaves roughly an order of magnitude of headroom
# over the measured cost while still failing a genuine hang fast.
#
# Deliberately NOT `timeout(1)` around the test command: that coreutils binary is absent
# on some developer machines (hit three times while building this unit) and cannot be
# assumed on a runner image either. `timeout-minutes` is the harness-level bound, and the
# pty harness carries its own per-read deadline as the inner one. (#321)
timeout-minutes: 20
strategy:
# TS-1 / interview Q4: pty buffering, EOF semantics, and drain ordering differ between
# Darwin and Linux, and macOS arm64 is the development platform — so LINUX IS THE
# UNTESTED HALF and this job is what finally exercises it.
#
# `fail-fast: false` for the same reason wheel-matrix uses it: knowing WHICH platform
# fails is the diagnostic value of a matrix whose second leg has never run.
fail-fast: false
matrix:
include:
- os: macos-latest
label: macOS arm64
- os: ubuntu-latest
label: Linux x86_64
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# SHA-pinned, not `@stable` (SR-5, interview Q10): `stable` is a mutable BRANCH on this
# action's repo, so `@stable` re-resolves on every run and is unreviewable supply chain.
# The toolchain channel is selected by the `toolchain` input, which is what `@stable`
# actually did. Same pin wheel-matrix resolved, reused rather than re-resolved.
#
# `components: rustfmt, clippy` is REQUIRED, not decorative: this action installs with
# `--profile minimal`, which ships neither. Without them `cargo fmt`/`cargo clippy`
# would fail as unknown subcommands — a red job for the wrong reason.
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable @ 2026-07-16
with:
toolchain: stable
components: rustfmt, clippy
# The no-FFI check below is a Python script, so Python is provisioned EXPLICITLY rather
# than relying on whatever the runner image happens to preinstall. That is the same
# class of mistake as assuming `timeout(1)` exists — a tool that is present on one
# machine and absent on another, discovered only when the gate fails for the wrong
# reason. GitHub's images do ship Python, but the version differs across the macOS and
# Ubuntu images and is not contractual.
#
# Same SHA pin `wheel-matrix` resolved for the smoke-test job, reused rather than
# re-resolved (SR-5). (#321)
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: '3.12'
# ---------------------------------------------------------------------------------
# BLOCKING GATE 1 — formatting. Mirrors black/isort's `--check` posture for Python.
# ---------------------------------------------------------------------------------
- name: cargo fmt --check
working-directory: tui
run: cargo fmt --check
# ---------------------------------------------------------------------------------
# BLOCKING GATE 2 — clippy.
#
# `--all-targets` IS LOAD-BEARING AND MUST NOT BE "SIMPLIFIED" AWAY.
#
# team.md affirms `cargo clippy -- -D warnings`. The bare form lints only the default
# target selection and SKIPS TEST TARGETS — so every one of this crate's integration
# tests (tests/pty.rs, tests/endpoint_contract.rs, tests/hermeticity_tripwire.rs,
# tests/no_backend_attach_call.rs, tests/binary_exits_zero.rs) would escape the gate
# entirely. That was discovered while linting a new integration test in
# `skeleton-crate`: the bare command passed on code the `--all-targets` form rejected.
#
# This is a deliberate CORRECTION to an affirmed practice, not an embellishment of it.
# Do not drop `--all-targets` to match the letter of team.md — the affirmed form is
# insufficient, and the test code is where a pty harness's unwrap-heavy mistakes live.
#
# `--locked` (TS-2, interview Q7) so a clippy run cannot quietly re-resolve the graph.
# The escape hatch for a genuine false positive stays per-site: `#[allow(lint_name)]`
# plus an issue-number comment (SR-3) — never a global relaxation here. (#321)
# ---------------------------------------------------------------------------------
- name: cargo clippy (--all-targets so TEST code is linted too)
working-directory: tui
run: cargo clippy --locked --all-targets -- -D warnings
# ---------------------------------------------------------------------------------
# THE TESTS — 46 across 6 targets, including the real-pty harness.
#
# `--locked` so the build FAILS if Cargo.lock would change (TS-2). Cargo.lock is
# committed, and without --locked a silent re-resolution would mean CI tested a
# different graph than the one under review.
#
# This is the step whose Linux half has never run. The pty tests assert real terminal
# behaviour — isatty, TIOCSWINSZ resize, non-UTF-8 byte passthrough, SIGTERM->SIGKILL
# escalation, and a concurrent drain that must not deadlock — and every one of those
# differs in detail between Darwin and Linux. (#321)
# ---------------------------------------------------------------------------------
- name: Assert no Python-FFI crate in the dependency graph (FR-5.2)
# Ordered BEFORE `cargo test` and marked `if: always()` deliberately. It used to run
# AFTER the tests with no condition, so a test failure -- which happened on EVERY PR,
# since no cao-server was started -- meant FR-5.2's no-FFI gate NEVER EXECUTED. A red
# job is loud; a gate that never ran is not. Found by the ss12a reviewer for
# skeleton-endpoint-verify. (#321)
if: always()
run: python scripts/assert_no_ffi.py --manifest-path tui/Cargo.toml
# The three live-contract tests in tui/tests/endpoint_contract.rs call a REAL cao-server;
# they are the deliberate hermeticity exemption. Without a server they fail on
# `Connection refused` on every PR (verified: with CAO_API_PORT=19999 they FAIL rather than
# skip, which is correct BR-7 behaviour and is why this gap was visible at all).
#
# `uv sync` is required because this job installs Python but NOT the project — it had
# `setup-python` and no `uv sync`, so `cao-server` did not exist here at all. (#321)
# This job runs `uv` but had NO `setup-uv` — it was the only uv-using job in the workflow
# without it, so `uv sync` would fail with `uv: command not found`. Caught by comparing every
# job's setup-uv presence against its uv usage rather than assuming the toolchain was there.
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the Python project (provides cao-server for the live contract test)
run: uv sync --locked --all-extras --dev
- name: Start cao-server and wait for readiness
# Poll /health rather than `sleep N`: a fixed sleep is either flaky or slow, and /health is
# the same endpoint the TUI itself probes. 30 x 1s matches the readiness cap the TUI uses.
# `timeout(1)` is NOT used — it does not exist on macOS runners by default. (#321)
run: |
# The PID IS recorded and IS read — by the teardown step below. An earlier revision
# wrote /tmp/cao-server.pid that nothing read, and the revision after that dropped the
# file entirely on the reasoning that "the runner is ephemeral and torn down with the
# job, so a cleanup step would be ceremony."
#
# THAT REASONING WAS WRONG, and the first CI run is what proved it: `setup-uv`'s
# POST-JOB step runs before the runner is torn down, and on macOS it hung for exactly
# 300s in `uv cache prune --ci` and then failed the job with exit 2 — after all 8 pty
# tests had PASSED. The job's own teardown line names the culprit: "Terminate orphan
# process: pid (12288) (Python)". The server was still running while the post step tried
# to prune the cache underneath it.
#
# Linux passed this step only by luck: its uv cache RESTORED, and `setup-uv` skips the
# prune-and-save path entirely on a cache hit. macOS missed ("Failed to restore: Cache
# service responded with 400"), took the prune path, and hit the contention. So this is
# a cache-miss-only failure on BOTH platforms, not a Darwin quirk — which is why the fix
# is an explicit shutdown rather than a platform condition. (#321)
uv run cao-server &
echo "$!" > "${RUNNER_TEMP}/cao-server.pid"
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:9889/health" >/dev/null 2>&1; then
echo "cao-server ready after ${i}s"
exit 0
fi
sleep 1
done
echo "cao-server did not become ready within 30s" >&2
exit 1
- name: cargo test --locked
working-directory: tui
run: cargo test --locked
# `if: always()` so the server is stopped even when the tests FAIL. Without it a red test
# run would leave the server up and turn one honest failure into two — the test failure plus
# a post-job cache error that has nothing to do with the change under review.
#
# Kills the process GROUP (`-PID`), not just the recorded pid: `uv run cao-server` is a
# parent `uv` process that execs the server as a child, and the Linux teardown listed BOTH
# ("pid (3510) (uv)", "pid (3517) (cao-server)"). Killing only the recorded pid would leave
# the other alive and holding the cache — the exact condition this step exists to remove.
#
# Never fails the job: `|| true` on every kill and `exit 0` at the end. A cleanup step that
# can fail is a second way for a green build to go red, and "the server had already exited"
# is not a defect. (#321)
- name: Stop cao-server before the post-job cache steps run
if: always()
run: |
pidfile="${RUNNER_TEMP}/cao-server.pid"
if [ ! -f "$pidfile" ]; then
echo "no pidfile at $pidfile; cao-server was never started"
exit 0
fi
pid="$(cat "$pidfile")"
# SIGTERM the group, give it a moment, then SIGKILL whatever ignored it. Mirrors the
# pty harness's own escalation rather than inventing a second shutdown policy.
kill -TERM "-${pid}" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$pid" 2>/dev/null || break
sleep 1
done
kill -KILL "-${pid}" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
rm -f "$pidfile"
echo "cao-server stopped"
exit 0
# ---------------------------------------------------------------------------------
# THE NO-FFI GATE (FR-5.2, SR-4, T-10) — hard gate, on BOTH platforms.
#
# T-10 forbids linking the TUI against the Python runtime: the boundary is subprocess
# for CLI invocation and HTTP for server reads. The affirmed rule requires this be a
# DETERMINISTIC dependency-graph check rather than a code-review convention, because an
# FFI crate does not arrive as a reviewable line in Cargo.toml — it arrives several
# levels down a transitive chain in a diff that shows only Cargo.lock churn.
#
# PROVEN ABLE TO FAIL, not assumed: `pyo3 = "0.22"` was planted in tui/Cargo.toml, the
# graph re-resolved (39 -> 53 crates), and the check rejected it — naming both `pyo3`
# and the transitively-pulled `pyo3-ffi` — before Cargo.toml/Cargo.lock were restored
# byte-identically. test/test_no_ffi_guard.py keeps that property asserted in the Python
# suite, since the live proof mutates the lockfile and cannot run in CI.
#
# Runs on both platforms deliberately: cargo-deny's own [bans] section carries an
# independent implementation of the same ban, but that job is a Docker action and
# therefore Linux-only. This step is the macOS coverage. (#321)
# ---------------------------------------------------------------------------------
# `python`, not `python3`: actions/setup-python puts `python` on PATH on every platform,
# and this matches how publish-to-pypi.yml already invokes scripts/assert_wheel_matrix.py.
# No `working-directory` — the script takes a repo-root-relative --manifest-path.
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@18d369b23da25a81e9458561248e1aafff6a70fa # v10.3.1
- name: Generate requirements.txt for scanning
run: |
uv export --locked --format requirements-txt > requirements.txt
# SR-4 (interview Q1): `exit-code: 1` makes this job FAIL on a CRITICAL/HIGH finding.
# Before this, the step had no exit-code, so it uploaded SARIF and continued — meaning
# "Security Scan passed" attested that a scan COMPLETED, not that it was clean. The
# rest of the repo already treats scanners as gates: secret-scan.yml passes
# `--exit-code 1` to gitleaks at both :68 and :82. This closes the inconsistency
# rather than introducing a new policy. (#321)
#
# SARIF is still produced and uploaded (see the next step's `if: always()`), so the
# Security tab keeps its history even on a failing run.
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.69.3
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: '1'
# FR-8: the SARIF step above writes findings to a FILE, so a failing scan
# logs an exit code and names nothing — diagnosing #568 took an out-of-band
# `gh api .../code-scanning/alerts` call to learn what had failed. This step
# re-runs the same scan in table format so the finding appears in the log.
#
# `if: failure()` — runs only when the gate above fails, so a green run pays
# nothing. `exit-code: '0'` — this step must not be the one that fails the
# job; the gate above already did, and a second failure would obscure which
# step is authoritative.
#
# No `severity:` input, matching the effective gate: trivy-action's
# entrypoint.sh unsets TRIVY_SEVERITY whenever format is sarif, so the step
# above scans at ALL severities. Passing CRITICAL,HIGH here would print a
# narrower set than the one that failed the build — and could print NOTHING
# while the job is red.
- name: Show Trivy findings in the log
if: failure()
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: 'fs'
scan-ref: '.'
format: 'table'
ignore-unfixed: true
exit-code: '0'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v3
# `always()` is load-bearing now that Trivy can fail: without it, a CRITICAL finding
# would fail the scan step and SKIP the upload, hiding the very finding that failed
# the build from the Security tab.
if: always()
with:
sarif_file: 'trivy-results.sarif'
dependency-security:
name: Dependency Security
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/dependency-security
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.repository == 'awslabs/cli-agent-orchestrator'
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Dependency Review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [actions, javascript-typescript, python, rust]
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run CodeQL analysis
uses: ./.github/actions/codeql
with:
language: ${{ matrix.language }}