-
Notifications
You must be signed in to change notification settings - Fork 0
1186 lines (1105 loc) · 56.5 KB
/
Copy pathpr.yml
File metadata and controls
1186 lines (1105 loc) · 56.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
# SPDX-FileCopyrightText: 2026 soundminds.ai
#
# SPDX-License-Identifier: Apache-2.0
name: pr
# infra_foundation Story 5.1 / FR-4.
# Runs on every PR to main.
#
# Three parallel jobs:
# - backend — uv sync · ruff check · ruff format --check · mypy · pytest
# with coverage gate (80% via pyproject.toml fail_under)
# - frontend — pnpm install · lint · typecheck · vitest · next build
# - docker — buildx build relyloop/api (no push; needs backend+frontend green)
#
# CI runs hermetically — service containers (Postgres, Redis, ES, OpenSearch)
# are launched by GitHub Actions, never against managed cloud (CLAUDE.md
# Common Pitfalls).
#
# TEMPORARY CI-BUDGET KILL-SWITCH (SKIP_HEAVY_CI repo variable)
# ------------------------------------------------------------
# The 5 jobs that take >1 min — `backend` (lint+typecheck+tests+coverage),
# `frontend`, `smoke-test`, `docker` (api buildx), `docker-ui` — each carry
# if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
# so they can be skipped en masse to conserve GitHub Actions minutes during a
# budget crunch. The sub-minute jobs (backend fast-lane unit tests, DCO,
# secrets guard, gitleaks) always run, retaining baseline signal. The two
# legal-compliance gates — `license-headers` (SPDX) and `license-inventory`
# (Apache-2.0 dependency check) — also ALWAYS run regardless of this switch:
# they're cheap (no service containers, no test suite) and must not let a
# missing header or a new GPL/AGPL runtime dep slip onto main while heavy jobs
# are off.
#
# Activate (skip heavy jobs): gh variable set SKIP_HEAVY_CI --body true
# Restore (run everything): gh variable delete SKIP_HEAVY_CI
#
# Default (variable unset) is the SAFE state — every job runs. While the
# switch is ON, PRs merge with only the fast-lane unit tests + secret scanning
# as automated signal, so lean on local `make test` / `pnpm test` + review.
# This is a temporary measure; restore full CI as soon as the budget allows.
#
# SMOKE-TEST OPT-IN SWITCH (SMOKE_TEST repo variable)
# ---------------------------------------------------
# The `smoke-test` (operator-path tutorial flow) job is OFF by default —
# operator-controlled. The runtime block that triggered the switch in the
# first place (demo-ubi.spec.ts beforeAll reseed exceeding the 25-min job
# cap) shipped a fix in infra_smoke_reseed_runtime_budget: demo-ubi.spec.ts
# is now CI-excluded via ui/playwright.config.ts's testIgnore CI branch,
# so the Playwright runtime is expected to fit within the 25-min cap.
# Verify before flipping SMOKE_TEST=true per the spec's §16 manual
# verification step (run `CI=true pnpm --dir ui exec playwright test --list
# | grep demo-ubi` — expect no matches). Once verified, operators may
# flip SMOKE_TEST=true to re-enable per-PR smoke signal:
# if: ${{ vars.SMOKE_TEST == 'true' && vars.SKIP_HEAVY_CI != 'true' }}
# Default (variable unset) → SMOKE_TEST is false → the smoke job is skipped.
# It still requires heavy CI to be on (SKIP_HEAVY_CI != 'true').
#
# Enable smoke (opt in): gh variable set SMOKE_TEST --body true
# Disable smoke (default): gh variable delete SMOKE_TEST (or set to false)
#
# Unlike SKIP_HEAVY_CI (whose SAFE default runs everything), SMOKE_TEST's
# default is OFF on purpose — even with the runtime block cleared the job
# is the only check that boots the full Compose stack, so flipping it
# on/off remains an explicit operator decision rather than a silent cost.
on:
pull_request:
branches: [main]
# Docs-only changes don't need backend tests / frontend build /
# smoke / docker buildx. A PR that touches BOTH docs and code
# still runs the full workflow (any non-ignored path matches —
# paths-ignore skips only when EVERY changed file matches).
#
# `website/**` is the mkdocs-material site (built + deployed by
# the separate, push-triggered deploy-docs.yml — NOT a PR gate).
# pr.yml has no website job, so a website-only PR validates
# nothing here. The dashboard-regen pre-commit hook writes
# website/docs/roadmap.md alongside docs/00_overview/*.{md,html};
# the latter are caught by `docs/**` but roadmap.md is under
# website/docs/ (not matched by `docs/**` or root-level `*.md`),
# so without this entry every finalization/dashboard-regen PR
# pulled in the full ~8-min backend suite for zero signal.
#
# CAVEAT: paths-ignore reports a skipped workflow's checks as
# perpetually "pending". That's fine TODAY (no required_status_checks
# on main — fast-lane posture). If branch protection with required
# checks is ever re-added, this needs a companion always-green
# passthrough job or merges on docs/website-only PRs will deadlock.
paths-ignore:
- 'docs/**'
- 'website/**'
- '*.md'
- '.gitignore'
- 'LICENSE'
- 'release-notes-*.md'
permissions:
contents: read
concurrency:
# Cancel in-flight runs of the same PR / branch when a new push lands.
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# The `secrets-files-guard` job moved to its own workflow file
# (.github/workflows/secrets-defense.yml) so it runs on EVERY PR
# regardless of pr.yml's paths-ignore filter — see
# docs/00_overview/planned_features/chore_ci_gitignore_paths_ignore_gap/idea.md
# for the rationale. The new workflow also bundles a content-scanning
# gitleaks job; both are parallel + ≤30s each.
backend-unit:
# Lane 1 of 2 that feed the coverage gate (Win 2′ of
# infra_pr_yml_split_backend_test_lanes). Replaces the previous
# `backend-unit-fast` lane, doing double duty:
#
# 1. Fast signal — unit tests run in ~30-60s (vs the heavy lane's ~7-8m),
# so a broken unit test still surfaces quickly. Service containers
# are NOT needed; unit tests are pure (no DB / Redis / ES / OS).
# 2. Coverage feed — runs with `--cov=backend` + `COVERAGE_FILE=.coverage.unit`
# so the data file lands at a unique name (NOT in coverage.py's
# parallel mode, which would litter every local-dev `pytest --cov`
# run with `.coverage.<host>.<pid>.<rand>` clutter). The single
# `.coverage.unit` file feeds the `backend-cov-gate` job that
# combines this lane's coverage with the heavy lane's and gates
# against the 80% fail_under threshold.
#
# `-n auto` parallelism is safe at the unit layer (the layer that
# broke under -n auto was integration, due to FK-teardown collisions —
# see the `backend-heavy` comment). The combined parallel + coverage
# cost is approximately a wash with the previous --no-cov serial run.
name: backend (unit + cov, parallel)
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install project deps (frozen)
run: uv sync --frozen
- name: pytest backend/tests/unit/ (-n auto, with coverage)
# `--cov-report=` (empty) suppresses the immediate terminal report —
# the report is generated by `backend-cov-gate` after the combine.
# `--cov-fail-under=0` disables pytest-cov's per-lane fail_under
# check (which would otherwise reject the unit lane at ~64% — unit
# tests alone don't exercise the full app; the 80% gate fires in
# `backend-cov-gate` against the COMBINED unit + heavy coverage).
env:
# Override the default `.coverage` base name so this lane's
# data file lands at `.coverage.unit`. `coverage combine` in
# `backend-cov-gate` matches any file starting with `.coverage.`
# so `.coverage.unit` is picked up alongside the heavy lane's
# `.coverage.heavy`.
COVERAGE_FILE: .coverage.unit
run: uv run pytest backend/tests/unit/ -n auto --cov=backend --cov-report= --cov-fail-under=0
- name: Upload partial coverage data (unit lane)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-data-unit
path: .coverage.unit*
if-no-files-found: error
include-hidden-files: true
retention-days: 1
# -------------------------------------------------------------------------
# License headers — REUSE (https://reuse.software/) SPDX compliance.
# ALWAYS runs (not gated by SKIP_HEAVY_CI): a legal-compliance gate must hold
# even during a CI-budget crunch. Cheap — no service containers, just resolve
# the `reuse` dev pin and lint every tracked file for an SPDX header (inline
# or via REUSE.toml). Fix locally with `uv run reuse annotate`
# (see CONTRIBUTING.md "License headers (SPDX)").
# -------------------------------------------------------------------------
license-headers:
name: license-headers
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install project deps (frozen)
run: uv sync --frozen
- name: Lint SPDX headers (reuse)
run: uv run reuse lint
# -------------------------------------------------------------------------
# Dependency license inventory — Apache-2.0 compatibility gate. ALWAYS runs
# (not gated by SKIP_HEAVY_CI). Regenerates docs/04_security/license-inventory.md
# from the locked closure (uv + pnpm) and fails if it's stale OR if any shipped
# dependency carries a forbidden (GPL/AGPL) or unclassified license. Needs both
# toolchains because the script spans Python (uv tree + pip-licenses) and
# frontend (pnpm licenses). Fix locally:
# `make license-inventory && git add docs/04_security/license-inventory.md`.
# -------------------------------------------------------------------------
license-inventory:
name: license-inventory
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install Python deps (frozen)
run: uv sync --frozen
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml
- name: Install pnpm deps (frozen)
run: pnpm --dir ui install --frozen-lockfile
- name: Check license inventory
run: uv run python scripts/gen_license_inventory.py --check
# -------------------------------------------------------------------------
# Generated-artifact freshness — Phase 2 (`infra_generated_artifact_
# freshness_gate`). Runs the snapshot guard (Story 2.2) here; Story 2.3
# appends the types-guard step to this same job. The copy-docs gate
# (Story 1.2) lives in its OWN workflow file `copy-docs-freshness.yml`
# so it survives pr.yml's `docs/**` paths-ignore filter (FR-3 escape).
#
# This job is NOT under paths-ignore — backend (**/*.py) and ui (**/*.ts)
# changes can both invalidate the snapshot, so the gate must run on every
# code-bearing PR. Job structure mirrors `license-inventory` above — uv
# for the Python exporter, plus pnpm/node so the future types-guard
# step in Story 2.3 has the pinned `openapi-typescript` binary in
# `ui/node_modules`.
# -------------------------------------------------------------------------
generated-artifacts-fresh:
name: generated-artifacts-fresh
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install Python deps (frozen)
run: uv sync --frozen
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml
- name: Install pnpm deps (frozen)
# Needed so the future Story 2.3 types-guard step finds the pinned
# `openapi-typescript` binary in `ui/node_modules`. The exporter
# itself only needs Python, but installing pnpm here keeps the
# whole `generated-artifacts-fresh` job self-contained.
run: pnpm --dir ui install --frozen-lockfile
- name: Self-test snapshot guard
run: bash scripts/ci/test_verify_openapi_snapshot_fresh.sh
- name: Verify ui/openapi.json snapshot is fresh
run: bash scripts/ci/verify_openapi_snapshot_fresh.sh
- name: Self-test types guard
run: bash scripts/ci/test_verify_types_fresh.sh
- name: Verify ui/src/lib/types.ts is fresh
run: bash scripts/ci/verify_types_fresh.sh
- name: Clean-tree determinism assertion (AC-7)
# After both guards have run their regenerators, the working
# tree must be clean across all three artifacts. Catches a
# regenerator that is itself non-deterministic across runs
# (the failure mode `infra_generated_artifact_freshness_gate`
# FR-6 names) — distinct from drift against the committed
# snapshot, which the two guards above catch.
run: |
REGEN_NO_STAGE=1 bash scripts/regen-generated-artifacts.sh
DRIFT="$(git status --porcelain -- ui/openapi.json ui/src/lib/types.ts ui/public/docs)"
if [[ -n "${DRIFT}" ]]; then
echo "ERROR: regenerator output is non-deterministic across runs." >&2
echo "Drift after a fresh canonical regen:" >&2
printf '%s\n' "${DRIFT}" >&2
exit 1
fi
echo "OK: clean-tree determinism assertion passed."
# -------------------------------------------------------------------------
# Static checks — ALWAYS run (not gated by SKIP_HEAVY_CI). Split into two
# parallel jobs by toolchain: the independent Python and Node dependency
# installs (the two big ~60-80s bottlenecks) now run concurrently on separate
# runners instead of back-to-back in one job, roughly halving wall-clock. tsc
# (Node) also overlaps the ruff/mypy chain (Python) for free. These are the
# cheap, service-free quality gates that must hold even during a CI-budget
# crunch: no Postgres/Redis/ES/OpenSearch, no `next build`, no pytest (those
# stay in the heavy `backend`/`frontend` jobs). When the kill-switch is OFF
# the same checks also run inside those jobs (minor, accepted duplication).
#
# NOTE: renamed from the former single `static-checks` job to
# `static-checks-backend` + `static-checks-frontend`. If branch protection
# lists the old name as a required check, update it to these two.
# -------------------------------------------------------------------------
static-checks-backend:
name: static-checks (backend — ruff + mypy + guards, always-run)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install project deps (frozen)
run: uv sync --frozen
- name: Restore mypy + ruff caches
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
.mypy_cache
.ruff_cache
key: mypy-ruff-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
restore-keys: |
mypy-ruff-${{ runner.os }}-
- name: ruff check
run: uv run ruff check .
- name: Format check (ruff)
run: uv run ruff format --check .
- name: mypy --strict
run: uv run mypy backend/
- name: Verify source-of-truth enum comments
run: bash scripts/ci/verify_enum_source_of_truth.sh
- name: Verify demo-cluster slug parity
run: bash scripts/ci/verify_demo_slug_parity.sh
- name: Verify install.sh builds every buildable Compose service
run: bash scripts/ci/verify_install_builds_all_services.sh
# parse_relyloop_engines regression (Phase 1 helper — never wired
# into CI; picked up as an inline tangential fix during
# feat_engine_version_selection Story 1.3).
- name: parse_relyloop_engines regression
run: bash scripts/ci/test_parse_relyloop_engines.sh
# parse_relyloop_llm regression (feat_bundled_local_llm Story 1 — the
# bundled-LLM opt-in selector + OPENAI_BASE_URL precedence).
- name: parse_relyloop_llm regression
run: bash scripts/ci/test_parse_relyloop_llm.sh
# resolve_native_ollama regression (feat_bundled_llm_native_detection —
# native-detect probe/shape-validation/sentinel/model-warning, mocked probe).
- name: relyloop_native_llm regression
run: bash scripts/ci/test_relyloop_native_llm.sh
# parse_relyloop_engine_versions regression (this story).
- name: parse_relyloop_engine_versions regression
run: bash scripts/ci/test_parse_relyloop_engine_versions.sh
# load_relyloop_env_file regression (bug_install_sh_env_file_not_loaded
# — install.sh now sources the RELYLOOP_* vars from .env).
- name: load_relyloop_env_file regression
run: bash scripts/ci/test_load_relyloop_env_file.sh
# feat_engine_version_selection Story 1.5 — matrix-Compose-default sync
# AND Python ↔ bash mirror sync (the matrix is the source of truth for
# backend, Compose default, and install.sh validation).
- name: Verify ENGINE_VERSION_MATRIX in sync with Compose defaults + bash mirror
run: bash scripts/ci/verify_engine_version_matrix_parity.sh
- name: verify_engine_version_matrix_parity self-test
run: bash scripts/ci/test_verify_engine_version_matrix_parity.sh
static-checks-frontend:
name: static-checks (frontend — prettier + eslint + tsc + vitest, always-run)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml
- name: pnpm install
run: pnpm --dir ui install --frozen-lockfile
- name: Format check (prettier)
run: pnpm --dir ui exec prettier --check src package.json tsconfig.json eslint.config.mjs .prettierrc.json
- name: ESLint
run: pnpm --dir ui lint
- name: tsc --noEmit
run: pnpm --dir ui typecheck
- name: vitest
run: pnpm --dir ui test
backend-heavy:
# Lane 2 of 2 that feed the coverage gate (Win 2′ of
# infra_pr_yml_split_backend_test_lanes). Runs contract + integration
# tests SERIALLY (no `-n auto`) because the integration layer hit
# FK-teardown collisions under parallel workers (PR #291 first run —
# `query_sets_cluster_id_fkey` violation when one worker held a FK
# reference to a cluster being deleted in another worker's teardown).
# Contract tests bundle here too — several boot the FastAPI app via
# `LifespanManager` (touches Postgres / Redis at startup), so they
# need the service containers this lane provides anyway. `pytest-xdist`
# remains in dev deps for local opt-in on the unit layer.
name: backend (contract + integration + cov)
# TEMP CI-budget kill-switch — see the SKIP_HEAVY_CI note at the top of
# this file. When the repo variable SKIP_HEAVY_CI == 'true', skip this
# >1-min job to conserve GitHub Actions minutes. Default (var unset) → runs.
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:17@sha256:0027bef26712baaee437a4ea48fdf3d2d2e2bc5f0d81615374408ca320f3c7e3
env:
POSTGRES_USER: relyloop
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: relyloop
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U relyloop -d relyloop"
--health-interval 5s
--health-timeout 5s
--health-retries 10
redis:
image: redis:8@sha256:aa049e689e141a4358ad1d4562dc49c88a89fbab711fd8fcc33f684c80b26301
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 10
elasticsearch:
image: elasticsearch:9.4.1@sha256:43b9eb68c147dd82aa0933812103631135aa95d283e46cf6d86f4c5c959ee3b7
env:
discovery.type: single-node
xpack.security.enabled: "false"
# 256m heap is sufficient for the CI integration-test workload
# (small synthetic indexes, no concurrency); halving from 512m
# shaves ~10-15s off the service-container init wait.
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ports:
- 9200:9200
options: >-
--health-cmd "curl -fs http://localhost:9200/_cluster/health || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 12
opensearch:
image: opensearchproject/opensearch:3.6.0@sha256:b5dd1512af2a99748c942cfbbd7f32162623336b210667d0fc6333c6321f171d
env:
discovery.type: single-node
DISABLE_SECURITY_PLUGIN: "true"
# Matches elasticsearch above — 256m heap is sufficient for the
# CI integration-test workload.
OPENSEARCH_JAVA_OPTS: "-Xms256m -Xmx256m"
ports:
- 9201:9200
options: >-
--health-cmd "curl -fs http://localhost:9200 || exit 1"
--health-interval 10s
--health-timeout 5s
--health-retries 12
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install project deps (frozen)
run: uv sync --frozen
- name: Write file-based secrets (CI parity with mounted-secrets pattern)
# Pydantic Settings reads DATABASE_URL_FILE / POSTGRES_PASSWORD_FILE /
# CLUSTER_CREDENTIALS_FILE. In CI we materialize them from the
# service-container env values. The cluster_credentials.yaml entry
# mirrors the heredoc in the "Seed cluster credentials" step further
# down — the smoke job and the pytest job both need local-es
# credentials so cluster registration + the
# infra_study_preflight_real_engine_integration helper's FR-6
# pre-flight both pass against the elasticsearch:9200 service container.
run: |
echo "testpassword" > "${RUNNER_TEMP}/postgres_password"
echo "postgresql+asyncpg://relyloop:testpassword@localhost:5432/relyloop" \
> "${RUNNER_TEMP}/database_url"
cat > "${RUNNER_TEMP}/cluster_credentials.yaml" <<'EOY'
local-es:
username: elastic
password: changeme
local-opensearch:
username: admin
password: admin
EOY
chmod 600 "${RUNNER_TEMP}/postgres_password" \
"${RUNNER_TEMP}/database_url" \
"${RUNNER_TEMP}/cluster_credentials.yaml"
# ruff check / ruff format --check / mypy --strict are intentionally NOT
# re-run here — the always-run `static-checks-backend` job
# (pr.yml ~L297) already covers all three with no service containers,
# giving sub-minute lint/type feedback in parallel with this heavy lane.
# Duplicating them here only burned ~30-40s of critical path. A lint/type
# failure still goes red via static-checks-backend; this lane no longer
# self-aborts early on such failures (acceptable — the PR is already red,
# and GHA does not auto-cancel sibling jobs). See
# chore_pr_yml_parallelize_backend_job/idea.md (Win-1 residual).
- name: pytest (contract + integration) with coverage
# Unit tests moved to the `backend-unit` lane (which runs in parallel
# with this job under `-n auto`). `--cov-report=` (empty) suppresses
# the immediate report — the final combined report + 80% gate run in
# `backend-cov-gate` after both lanes upload their data files.
# `--cov-fail-under=0` disables pytest-cov's per-lane fail_under
# check (the contract+integration lane alone measures at ~61% without
# the unit lane's contribution; the 80% gate fires in
# `backend-cov-gate` against the combined coverage). `COVERAGE_FILE`
# writes to `.coverage.heavy` (vs the unit lane's `.coverage.unit`)
# so the two artifacts coexist in `backend-cov-gate`'s cwd at
# combine time without colliding.
env:
DATABASE_URL_FILE: ${{ runner.temp }}/database_url
POSTGRES_PASSWORD_FILE: ${{ runner.temp }}/postgres_password
CLUSTER_CREDENTIALS_FILE: ${{ runner.temp }}/cluster_credentials.yaml
REDIS_URL: redis://localhost:6379/0
OPENAI_BASE_URL: https://api.openai.com/v1
OPENAI_MODEL: gpt-4o-2024-08-06
OPENAI_MODEL_CHAT: gpt-4o-mini-2024-07-18
# ES / OpenSearch are reachable on localhost — the /healthz integration
# test points at API_URL which expects the API container; we don't
# boot the API in CI yet (that lands at MVP3 with the deploy job).
# The health integration test will skip without a running API; that
# behavior is acceptable until the deploy workflow exists.
RELYLOOP_API_URL: http://localhost:8000
COVERAGE_FILE: .coverage.heavy
run: |
uv run pytest backend/tests/contract backend/tests/integration \
--cov=backend \
--cov-report= \
--cov-fail-under=0
- name: Upload partial coverage data (heavy lane)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-data-heavy
path: .coverage.heavy*
if-no-files-found: error
include-hidden-files: true
retention-days: 1
- name: Verify source-of-truth enum comments (feat_studies_ui Story 4.2)
# Fails when a backend Literal / frozenset gains or loses a value but
# the corresponding ui/src/lib/enums.ts array isn't updated. Runs in
# the backend job because the helper imports backend.app.api.v1.schemas
# via the project venv that uv-sync set up above.
run: bash scripts/ci/verify_enum_source_of_truth.sh
- name: Verify demo-cluster slug parity (feat_home_first_run_demo_nudge Story 4.2)
# Fails when the 4 demo cluster slugs in
# ui/src/lib/demo-data.ts DEMO_CLUSTER_SLUGS drift from the
# "slug": literals in scripts/seed_meaningful_demos.py SCENARIOS.
# Bash-only guard (no Python deps); ~2s wall time. Pure-text
# comparison so it runs cleanly even if the backend uv-sync
# changed in ways unrelated to demo data.
run: bash scripts/ci/verify_demo_slug_parity.sh
- name: Verify install.sh builds every buildable Compose service (bug_install_skip_ui_rebuild)
# Fails when scripts/install.sh's `docker compose build` line uses an
# explicit service list that doesn't cover every service with a
# `build:` block in docker-compose.yml. Catches the silent-drift class
# of bug where adding a service to Compose leaves operators running a
# stale image on the second `make up` after a code change (the
# smoke-test job runs on a fresh runner so it never sees that path).
run: bash scripts/ci/verify_install_builds_all_services.sh
backend-cov-gate:
# Coverage combine + 80% gate. Downloads partial `.coverage.*` data
# files from `backend-unit` (parallel unit lane) and `backend-heavy`
# (serial contract + integration lane), merges them via
# `coverage combine`, and fails the build if combined coverage is
# below the 80% threshold set in pyproject.toml's
# [tool.coverage.report] fail_under = 80.
#
# Win 2′ of infra_pr_yml_split_backend_test_lanes. The single tradeoff
# vs the previous all-in-one `backend (tests + coverage)` job: the gate
# is reported in its own job rather than inline with the test output,
# so a developer hunting a coverage regression in PR-checks UI clicks
# one more level to see the missing-line report. Acceptable for the
# ~1.5 min wall-clock recovery.
name: backend (coverage gate)
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
needs: [backend-unit, backend-heavy]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
version: "0.5.7"
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
- name: Install project deps (frozen)
# Need coverage.py + the project source tree (for the source path
# mapping in [tool.coverage.paths]). `uv sync` is the simplest way
# to materialize both.
run: uv sync --frozen
- name: Download partial coverage data (unit lane)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
with:
name: coverage-data-unit
# All `.coverage.*` files land in the workspace root so
# `coverage combine` picks them up.
path: .
- name: Download partial coverage data (heavy lane)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
with:
name: coverage-data-heavy
path: .
- name: Combine coverage data files
# `coverage combine` merges every file starting with `.coverage.`
# in the cwd into a single `.coverage` database. In our setup
# that's `.coverage.unit` (from backend-unit) and `.coverage.heavy`
# (from backend-heavy), downloaded by the two preceding steps.
# The [tool.coverage.paths] mapping in pyproject.toml maps per-runner
# absolute paths (e.g. /home/runner/work/relyloop/relyloop/backend)
# back to the canonical `backend/` source so the combined report
# deduplicates correctly across runners.
run: uv run coverage combine
- name: Coverage report (--fail-under=80)
# `fail_under = 80` is set in pyproject.toml's [tool.coverage.report]
# — `coverage report` honors it and exits non-zero if combined
# coverage drops below.
run: uv run coverage report --show-missing
- name: Generate coverage XML (for downstream tooling)
run: uv run coverage xml
- name: Upload combined coverage XML
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-xml
path: coverage.xml
if-no-files-found: warn
frontend:
name: frontend (lint + typecheck + tests + build)
# TEMP CI-budget kill-switch — see the SKIP_HEAVY_CI note at top of file.
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: "pnpm"
cache-dependency-path: ui/pnpm-lock.yaml
- name: pnpm install
run: pnpm --dir ui install --frozen-lockfile
- name: Format check (prettier)
# Mirrors the pre-commit `prettier-ui` hook entry in
# `.pre-commit-config.yaml` exactly. The pre-commit hook is the
# local-only gate; this CI step closes the gap when contributors
# bypass it (`--no-verify`, hosted edits, fresh checkout without
# `pre-commit install`). Argument list MUST stay in sync with the
# pre-commit hook — if the hook adds/removes a path here, mirror
# the change. Captured as chore_ci_prettier_check after PR #148
# surfaced PR #136 drift the pre-commit hook caught but CI had
# passed.
run: pnpm --dir ui exec prettier --check src package.json tsconfig.json eslint.config.mjs .prettierrc.json
- name: ESLint (next lint)
run: pnpm --dir ui lint
- name: tsc --noEmit
run: pnpm --dir ui typecheck
- name: vitest
run: pnpm --dir ui test
- name: Production build (next build)
run: pnpm --dir ui build
smoke-test:
name: smoke (operator-path tutorial flow)
# OFF by default — opt in with `gh variable set SMOKE_TEST --body true`.
# See the "SMOKE-TEST OPT-IN SWITCH" + "SKIP_HEAVY_CI" notes at top of
# file. The job runs ONLY when SMOKE_TEST is explicitly 'true' AND heavy
# CI isn't being skipped. Default (SMOKE_TEST unset → false) skips it.
# The reseed-runtime block that originally triggered the OFF default
# was cleared by infra_smoke_reseed_runtime_budget (demo-ubi CI-excluded
# in ui/playwright.config.ts testIgnore). `needs: [docker, docker-ui]`
# below stays consistent — when this skips, the images simply go unused.
# The final clause is a FORK-PR GUARD (security audit 2026-07-12): this job
# mounts secrets.OPENAI_API_KEY_TEST and then runs the PR head's checked-out
# code (make up / install.sh / app). Without this guard, a fork PR's
# untrusted code could exfiltrate the key the moment a maintainer approves
# its CI run while SMOKE_TEST is enabled. Same-repo branch PRs still run it.
if: ${{ vars.SMOKE_TEST == 'true' && vars.SKIP_HEAVY_CI != 'true' && github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-24.04
# Bumped 15 → 25 min as part of infra_solr_smoke_stability PR #383.
# Post infra_smoke_reseed_runtime_budget (demo-ubi CI-excluded in
# ui/playwright.config.ts testIgnore), the Playwright runtime is
# expected to fit within the 25-min cap (verify once via the spec
# §16 `playwright test --list` check before flipping SMOKE_TEST=true,
# and optionally re-confirm with one post-merge SMOKE_TEST=true run).
# The 25-min cap is expected headroom against the remaining specs,
# not a regular threat.
timeout-minutes: 25
# Depend on the parallel `docker` (API) + `docker-ui` jobs so both image
# artifacts are ready before `make up`. Pre-bumps this PR was paying ~10min
# for `docker compose up -d` (image pulls + API + UI builds inside the
# step). The artifact handoff (API + UI) + base-image cache + SKIP_BUILD
# below cut that to ~2-3min on a warm cache. See
# chore_ci_perf_buildx_artifact_image_cache_xdist/idea.md.
needs: [docker, docker-ui]
permissions:
contents: read
env:
# Pin Compose project name so container-name prefixes (relyloop-solr-1,
# relyloop-opensearch-1) are deterministic for the failure-diagnostics
# grep across both `make up` and the later log-collect step. GHA
# step-level env does NOT persist to later steps, so this MUST be
# job-level. See infra_solr_smoke_stability spec FR-2 + AC-2.
COMPOSE_PROJECT_NAME: "relyloop"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Python + uv so `uv run pytest` works on the smoke runner (parallel to
# the backend job; doesn't share its venv).
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version-file: "pyproject.toml"
- name: Install project deps (frozen)
run: uv sync --frozen
- name: Pre-generate secrets with container-readable perms
env:
OPENAI_API_KEY_TEST: ${{ secrets.OPENAI_API_KEY_TEST }}
# install.sh's secret-generation steps create files with chmod 600.
# Local Docker Desktop hides this via virtiofs UID mapping; on Linux
# runners the api container's non-root `relyloop` user can't read
# files owned by the runner UID with 0600 perms (api crashes at
# startup with PermissionError on /run/secrets/database_url). install.sh
# is idempotent — `[[ ! -s ]]` skips regen on existing files — so we
# pre-generate every secret here with 0644 perms.
run: |
mkdir -p ./secrets
openssl rand -base64 32 | tr -d '\n' > ./secrets/postgres_password
PG_PW="$(cat ./secrets/postgres_password)"
printf 'postgresql+asyncpg://relyloop:%s@postgres/relyloop' "$PG_PW" \
> ./secrets/database_url
# Strip trailing newline so the empty-secret case produces an empty
# file (size 0) instead of a one-newline file (which `[ -s ]` would
# accept). Sanity-check below uses grep for non-whitespace content.
printf '%s' "$OPENAI_API_KEY_TEST" > ./secrets/openai_key
cat > ./secrets/cluster_credentials.yaml <<'EOY'
local-es:
username: elastic
password: changeme
local-opensearch:
username: admin
password: admin
EOY
chmod 644 ./secrets/*
- name: Sanity-check OPENAI_API_KEY_TEST is populated
run: |
# Use grep so an unset secret (empty file OR file containing only
# whitespace) fails fast with a clear message. `[ -s ]` only checks
# size > 0, which is satisfied by a single newline.
if ! grep -q '[^[:space:]]' ./secrets/openai_key; then
echo "::error::OPENAI_API_KEY_TEST secret is empty — smoke gate requires it (per chore_tutorial_polish §3 + decision log M5)"
exit 1
fi
# CI-perf #1: download the pre-built API + UI images from the parallel
# docker / docker-ui jobs so `make up` skips both in-step `docker build`s
# (saves ~5min). Combined with RELYLOOP_SKIP_BUILD=1 (which makes
# install.sh skip its `docker compose build` step) compose just `up`s
# the loaded images. RELYLOOP_GIT_SHA below picks them up by tag.
- name: Download pre-built API image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: relyloop-api-image-${{ github.sha }}
path: /tmp/
- name: Download pre-built UI image
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: relyloop-ui-image-${{ github.sha }}
path: /tmp/
- name: Load pre-built API + UI images into Docker
run: |
docker load -i /tmp/relyloop-api-image.tar
docker load -i /tmp/relyloop-ui-image.tar
docker image ls 'relyloop/*'
# CI-perf #2: cache the base service-container images (postgres / redis /
# elasticsearch / opensearch) keyed on their tags. On cache hit we
# `docker load` 4 tars in ~5s vs ~60-90s for `docker pull` on miss.
# Key changes whenever any of the image tags in docker-compose.yml change
# (forces re-pull on a bump PR, hit on subsequent runs).
- name: Cache base service-container images
id: base-image-cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: /tmp/docker-base-images
key: docker-base-images-v1-${{ hashFiles('docker-compose.yml') }}
- name: Pre-pull + save base images on cache miss
if: steps.base-image-cache.outputs.cache-hit != 'true'
run: |
mkdir -p /tmp/docker-base-images
for img in postgres:17 redis:8 elasticsearch:9.4.1 opensearchproject/opensearch:3.6.0; do
docker pull "$img"
safe=$(echo "$img" | tr '/:' '__')
docker save "$img" -o "/tmp/docker-base-images/${safe}.tar"
done
- name: Load base images on cache hit
if: steps.base-image-cache.outputs.cache-hit == 'true'
run: |
for tar in /tmp/docker-base-images/*.tar; do
docker load -i "$tar"
done
docker image ls
# Compose's `image:` lines reference `relyloop/api:${RELYLOOP_GIT_SHA:-dev}`
# and `relyloop/ui:${RELYLOOP_GIT_SHA:-dev}` — setting RELYLOOP_GIT_SHA
# here makes compose pick up the loaded images instead of trying to
# build/pull them. RELYLOOP_SKIP_BUILD=1 also makes install.sh skip its
# explicit `docker compose build` step (added 2026-05-28; see install.sh
# step 6). Together these eliminate the API + UI build duplication in
# smoke that was eating ~5min per run.
- name: Bring up the stack
env:
RELYLOOP_GIT_SHA: ${{ github.sha }}
RELYLOOP_SKIP_BUILD: "1"
# Same rationale as RELYLOOP_SKIP_BUILD — the 2 demo-dependent
# E2E specs were skipped in CI on 2026-05-28
# (chore_drop_demo_seed_from_ci). Without this skip install.sh
# would still auto-seed ~5min of demo data on every CI run.
RELYLOOP_SKIP_AUTO_SEED: "1"
# Cap Solr heap to reduce JVM memory pressure on the GHA runner.
# Compose's solr service reads ${SOLR_HEAP_SIZE:-512m}; the 512m
# default is correct for local dev (docker-compose.yml:274) — this
# 256m cap is CI-only. Mirrors the backend job's
# ES_JAVA_OPTS: "-Xms256m -Xmx256m" precedent at line 287.
# See infra_solr_smoke_stability spec FR-2 + decision log D-1, D-5.
SOLR_HEAP_SIZE: "256m"
# feat_selective_engine_startup_and_demo Story 1.2 / FR-3 / AC-14.
# The three engine services (elasticsearch, opensearch, solr) are
# now Compose-profile-gated. install.sh defaults RELYLOOP_ENGINES
# to es,os,solr when unset → COMPOSE_PROFILES=es,os,solr, which
# preserves today's three-engine smoke coverage. We set this
# explicitly here so the smoke job never silently drops engine
# coverage if a future operator-default change flips the unset
# behavior — and so the workflow YAML itself documents the smoke
# job's three-engine expectation.
COMPOSE_PROFILES: "es,os,solr"
# Pre-create ./data/solr with Solr's container UID (8983) so the
# bind-mount target is writable from inside the container. The
# solr:10.0 image runs as UID/GID 8983; on the GHA runner the
# bind-mount target defaults to root ownership, which makes Solr's
# boot script fail at "Cannot write to /var/solr as 8983:8983" and
# exit(1) within ~500ms (verified empirically on PR #383 run
# 26788939356 — diagnostics fold-in caught this; lever cascade did
# not anticipate it because all three locked levers assumed
# JVM-internal crash modes, not filesystem permissions).
# CI-only; never edit docker-compose.yml's defaults for this.
run: |
mkdir -p ./data/solr
sudo chown 8983:8983 ./data/solr
make up
- name: Wait for /healthz
run: |
for i in $(seq 1 18); do
if curl -fsS http://127.0.0.1:8000/healthz | jq -e '.status == "ok"' > /dev/null; then
echo "API healthy after ${i}x5s"
exit 0
fi
sleep 5
done
echo "::error::API not healthy within 90s"
curl -s http://127.0.0.1:8000/healthz || true
exit 1
# Migrations now run via the `migrate` init container at boot
# (bug_worker_optuna_init_race). This step is a no-op re-run that
# exercises the operator-facing `make migrate` target so it doesn't
# bit-rot. The worker comes up clean — no restart workaround needed.
- name: Apply migrations (no-op — init container already ran)
run: make migrate
# IMPORTANT order: seed-clusters BEFORE seed-es. seed_es.py reads
# cluster.base_url via cluster_repo.get_active_cluster_by_name("local-es")
# — so the cluster row must exist first.
- name: Seed clusters
run: make seed-clusters
- name: Seed sample ES index
run: make seed-es
# NOTE: `make seed-demo FORCE=1` was REMOVED on 2026-05-28 (see
# chore_drop_demo_seed_from_ci/idea.md). The two specs that depended on
# it (dashboard.spec.ts, dashboard-reseed.spec.ts) were the persistent
# smoke-job failure source — they are now gated out via Playwright's
# `testIgnore` when `process.env.CI` is set (see ui/playwright.config.ts).
# The underlying banner + reset-to-demo components remain covered by
# vitest (`start-here-checklist.test.tsx` + demo-banner component tests).
# Operators running locally invoke `make seed-demo` and then `pnpm test:e2e`
# picks them up because CI is unset.
- name: Run smoke test (LLM judgment generation + study + alignment guard + digest)
run: uv run pytest backend/tests/smoke/test_tutorial_path.py -v --tb=short
- name: Verify UI container reachable
run: |
curl -fsS http://127.0.0.1:3000/ | grep -qi "<html" \
|| { echo "::error::UI container did not render Next.js shell"; exit 1; }
# E2E gate — feat_query_inline_crud Story 5.1 ships the first Playwright
# specs in the repo (5 cases against /query-sets/[id]). The smoke job
# already has the full stack up, so we piggyback rather than starting a
# second stack.
- name: pnpm setup (Node 20)
uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6
with:
version: 9
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 22
cache: "pnpm"