11# ============================================================================
2- # Laige CI — P0 platform matrix on pull request (M0-CI-01)
2+ # Laige CI — full P0 platform matrix on pull request (M0-CI-01,
3+ # re-baselined 2026-10-11)
34#
45# Roadmap: roadmap/M0-foundations.md, step M0-CI-01
56# PRD refs: §6 (P0 platforms, AC-6.1), §14 (cadence), §8.1 (build budget)
67#
7- # Cadence: the PR lane covers only the P0 OSes that cannot be verified
8- # locally — Windows and macOS. Linux is verified on the developer
9- # machine with the canonical trees from
10- # docs/getting-started/building.md (including the LAIGE_ASAN=ON and
11- # LAIGE_TSAN=ON instrumented trees), so this workflow carries no Linux
12- # jobs. Merges to master run the FULL matrix — every P0 OS/variant job,
13- # both sanitizer lanes, and all tooling jobs — via ci.yml, which is
14- # the "all per merge" half of the PRD §14 cadence.
8+ # Cadence (PRD §14, as amended 2026-10-11 — "test everything, all the
9+ # time"): this workflow runs on EVERY pull request, and ALL eleven jobs
10+ # run — unconditionally:
1511#
16- # Label selector (apply at most one per PR; if both are present
17- # ci:macos wins, so exactly one P0 OS always runs):
12+ # * the five P0 OS jobs (linux-gcc, linux-clang, windows-msvc,
13+ # macos-arm64, macos-intel);
14+ # * the two sanitizer lanes (linux-asan, linux-tsan; M0-CI-02);
15+ # * the four platform-independent tooling jobs (include-lint,
16+ # determinism-lint, api-manifest, detcheck).
1817#
19- # ci:macos → macos-arm64 + macos-intel jobs
20- # ci:windows → windows-msvc job
21- # (no label) → no P0 OS build; the platform-independent tooling jobs
22- # (include-lint, determinism-lint, api-manifest,
23- # detcheck) still run on every PR.
24- #
25- # NOTE: the labels must be created once in the repository settings
26- # (GitHub does not create labels from workflows). A PR with no ci:*
27- # label simply runs the tooling jobs. The former ci:linux label is no
28- # longer a selector (Linux is verified locally); a PR carrying it gets
29- # the no-label treatment.
18+ # The former ci:windows / ci:macos label selector is REMOVED: no job is
19+ # gated on a label and no test is ever skipped. Linux is no longer
20+ # "verified locally only" — it runs in the PR lane exactly as in the
21+ # merge lane (ci.yml), so a PR that breaks a Linux tree fails in the
22+ # PR lane before it can reach master. Local verification of the Linux
23+ # trees before pushing remains a developer obligation (AGENTS.md,
24+ # section 5: the local Linux test gate).
3025#
3126# Jobs, runner images, steps, and the build-budget timeouts are the
3227# same as in ci.yml — including the macOS exception (the two macOS
3328# jobs carry timeout-minutes: 15, the shared-runners tail latency),
3429# documented in ci.yml's header (the full matrix documentation and
35- # the Windows multi-config note).
36- #
37- # Sanitizer lanes (M0-CI-02): the linux-asan (LAIGE_ASAN=ON) and
38- # linux-tsan (LAIGE_TSAN=ON) lanes no longer run on PRs (no Linux jobs
39- # in the PR lane); they run on every merge via ci.yml. See the ci.yml
40- # header for the lane semantics (fatal sanitizer reports, report
41- # archiving, toolchain choice) .
30+ # the Windows multi-config note). The one difference: the sanitizer
31+ # jobs upload no artifacts here. The pull_request GITHUB_TOKEN is
32+ # read-only (contents:read for same-repo PRs, read-only for fork PRs —
33+ # Actions cannot grant actions:write on this event), so the report
34+ # files are not archived; the tee'd ctest output keeps every sanitizer
35+ # report in the job log, which is enough to diagnose a red PR. The
36+ # merge lane (ci.yml) still archives them as artifacts .
4237#
4338# Fuzz lane (PRD §14 "every commit (bounded)"; M0-TEST-01): no separate
4439# fuzz job — the `fuzz_json_parse` ctest entry (1000 deterministic runs
4742# conventions: docs/testing.md.
4843#
4944# Include-graph lint (M0-CI-03; NFR-8.11, NFR-8.13): the `include-lint`
50- # job runs on EVERY pull request, independent of the ci:* label selector
51- # — it is a platform-independent repository check (Python 3 stdlib only),
52- # not a P0 OS build, so it does not interact with the "one P0 OS per PR"
53- # cadence. It enforces the PRD §10.1 include rules over src/** (laige-core
54- # depends on nothing internal; arrows only downward in the module stack;
45+ # job runs on EVERY pull request — it is a platform-independent
46+ # repository check (Python 3 stdlib only), not a P0 OS build. It
47+ # enforces the PRD §10.1 include rules over src/** (laige-core depends
48+ # on nothing internal; arrows only downward in the module stack;
5549# vendored deps only included from their deps.lock `owner`) and reports
5650# the vendored-dependency count, which must stay ≤ 10 (PRD §11).
5751#
@@ -73,11 +67,138 @@ concurrency:
7367 cancel-in-progress : true
7468
7569jobs :
70+ linux-gcc :
71+ name : Linux x64 (g++)
72+ runs-on : ubuntu-24.04
73+ timeout-minutes : 10
74+ steps :
75+ - uses : actions/checkout@v4
76+ - name : Install GLFW X11 headers (laige-render)
77+ # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/
78+ # CMakeLists.txt) needs only the X11 *headers* at configure time
79+ # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the
80+ # ubuntu-24.04 runner image ships none of them. The libegl/libgl
81+ # packages are the RUNTIME GL stack for the headless GL smoke
82+ # (Mesa surfaceless EGL + llvmpipe software GL; the engine and
83+ # GLAD load them by dlopen at runtime) (M2-GL-01).
84+ run : |
85+ sudo apt-get update -qq
86+ sudo apt-get install -y -qq \
87+ libx11-dev libxcursor-dev libxrandr-dev \
88+ libxinerama-dev libxi-dev libxext-dev \
89+ libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri
90+ - name : Configure
91+ run : cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++
92+ - name : Build
93+ run : cmake --build build -j
94+ - name : Test (unit)
95+ run : ctest --test-dir build --output-on-failure
96+
97+ linux-clang :
98+ name : Linux x64 (clang++)
99+ runs-on : ubuntu-24.04
100+ timeout-minutes : 10
101+ steps :
102+ - uses : actions/checkout@v4
103+ - name : Install GLFW X11 headers (laige-render)
104+ # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/
105+ # CMakeLists.txt) needs only the X11 *headers* at configure time
106+ # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the
107+ # ubuntu-24.04 runner image ships none of them. The libegl/libgl
108+ # packages are the RUNTIME GL stack for the headless GL smoke
109+ # (Mesa surfaceless EGL + llvmpipe software GL; the engine and
110+ # GLAD load them by dlopen at runtime) (M2-GL-01).
111+ run : |
112+ sudo apt-get update -qq
113+ sudo apt-get install -y -qq \
114+ libx11-dev libxcursor-dev libxrandr-dev \
115+ libxinerama-dev libxi-dev libxext-dev \
116+ libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri
117+ - name : Configure
118+ run : cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
119+ - name : Build
120+ run : cmake --build build -j
121+ - name : Test (unit)
122+ run : ctest --test-dir build --output-on-failure
123+
124+ linux-asan :
125+ name : Linux x64 ASan+UBSan (clang++)
126+ runs-on : ubuntu-24.04
127+ timeout-minutes : 10
128+ # No artifact step here: the pull_request token is read-only (see
129+ # the header). The tee keeps every report in the job log.
130+ steps :
131+ - uses : actions/checkout@v4
132+ - name : Install GLFW X11 headers (laige-render)
133+ # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/
134+ # CMakeLists.txt) needs only the X11 *headers* at configure time
135+ # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the
136+ # ubuntu-24.04 runner image ships none of them. The libegl/libgl
137+ # packages are the RUNTIME GL stack for the headless GL smoke
138+ # (Mesa surfaceless EGL + llvmpipe software GL; the engine and
139+ # GLAD load them by dlopen at runtime) (M2-GL-01).
140+ run : |
141+ sudo apt-get update -qq
142+ sudo apt-get install -y -qq \
143+ libx11-dev libxcursor-dev libxrandr-dev \
144+ libxinerama-dev libxi-dev libxext-dev \
145+ libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri
146+ - name : Configure (ASan+UBSan)
147+ run : cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DLAIGE_ASAN=ON
148+ - name : Build (ASan+UBSan)
149+ run : cmake --build build-asan -j
150+ - name : Test (unit, ASan+UBSan)
151+ # Every sanitizer report is fatal to the test process: ASan via
152+ # abort_on_error=1:halt_on_error=1 here, UBSan via
153+ # -fno-sanitize-recover=all (wired by CMake). detect_leaks=1 (the
154+ # Linux default) is stated explicitly.
155+ env :
156+ ASAN_OPTIONS : " abort_on_error=1:halt_on_error=1:detect_leaks=1"
157+ run : |
158+ set -o pipefail
159+ ctest --test-dir build-asan --output-on-failure 2>&1 | tee asan-ctest-output.txt
160+
161+ linux-tsan :
162+ name : Linux x64 TSan (clang++)
163+ # 30 min (not 10): TSan is the slowest lane by design (~5-15x
164+ # instrumentation overhead), and the suite has grown past the old
165+ # margin (the documented rationale, with the evidence runs, is in
166+ # the linux-tsan job of ci.yml).
167+ # No artifact step here: the pull_request token is read-only (see
168+ # the header). The tee keeps the race report in the job log.
169+ runs-on : ubuntu-24.04
170+ timeout-minutes : 30
171+ steps :
172+ - uses : actions/checkout@v4
173+ - name : Install GLFW X11 headers (laige-render)
174+ # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/
175+ # CMakeLists.txt) needs only the X11 *headers* at configure time
176+ # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the
177+ # ubuntu-24.04 runner image ships none of them. The libegl/libgl
178+ # packages are the RUNTIME GL stack for the headless GL smoke
179+ # (Mesa surfaceless EGL + llvmpipe software GL; the engine and
180+ # GLAD load them by dlopen at runtime) (M2-GL-01).
181+ run : |
182+ sudo apt-get update -qq
183+ sudo apt-get install -y -qq \
184+ libx11-dev libxcursor-dev libxrandr-dev \
185+ libxinerama-dev libxi-dev libxext-dev \
186+ libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri
187+ - name : Configure (TSan)
188+ run : cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DLAIGE_TSAN=ON
189+ - name : Build (TSan)
190+ run : cmake --build build-tsan -j
191+ - name : Test (unit, TSan)
192+ # TSAN_OPTIONS=halt_on_error=1 is applied per test by tests/
193+ # (M0-BUILD-01), so the first data race report kills the test
194+ # process and ctest fails. The race report is printed to stderr
195+ # and captured by the tee below.
196+ run : |
197+ set -o pipefail
198+ ctest --test-dir build-tsan --output-on-failure 2>&1 | tee tsan-ctest-output.txt
199+
76200 windows-msvc :
77201 name : Windows x64 (MSVC 2022)
78- if : >-
79- contains(github.event.pull_request.labels.*.name, 'ci:windows') &&
80- !contains(github.event.pull_request.labels.*.name, 'ci:macos')
81202 runs-on : windows-2022
82203 timeout-minutes : 10
83204 steps :
@@ -116,7 +237,6 @@ jobs:
116237
117238 macos-arm64 :
118239 name : macOS arm64 (AppleClang)
119- if : contains(github.event.pull_request.labels.*.name, 'ci:macos')
120240 runs-on : macos-15
121241 # The shared macOS runners' tail latency exceeds the 10-min build
122242 # budget (the macOS exception, documented in ci.yml's header).
@@ -132,7 +252,6 @@ jobs:
132252
133253 macos-intel :
134254 name : macOS Intel (AppleClang)
135- if : contains(github.event.pull_request.labels.*.name, 'ci:macos')
136255 runs-on : macos-14
137256 # The shared macOS runners' tail latency exceeds the 10-min build
138257 # budget (the macOS exception, documented in ci.yml's header).
@@ -147,12 +266,12 @@ jobs:
147266 run : ctest --test-dir build --output-on-failure
148267
149268 include-lint :
150- # M0-CI-03: runs on every PR (no ci:* condition) . Platform-independent:
151- # the lint is Python 3 stdlib only (no setup step needed) and checks
152- # the repository layout, not a compiler-specific build. The CTest
269+ # M0-CI-03: runs on every PR. Platform-independent: the lint is
270+ # Python 3 stdlib only (no setup step needed) and checks the
271+ # repository layout, not a compiler-specific build. The CTest
153272 # suite runs it against the real tree in every P0 job as well
154273 # (tests/tools, test `include-lint-real-tree`), so a broken include
155- # fails even a PR that selects another P0 OS .
274+ # fails every job, not just this one .
156275 name : Include-graph lint + dependency count
157276 runs-on : ubuntu-24.04
158277 timeout-minutes : 5
@@ -162,16 +281,15 @@ jobs:
162281 run : python3 tools/laige-include-lint
163282
164283 determinism-lint :
165- # M1-DET-01: runs on every PR (no ci:* condition). The sim-source
166- # determinism scan (the second half of the G-R8 guarantee; the
167- # first is the compile-time trait checked by the
168- # trait_compile_* CTest fixtures): forbids raw float/double and
169- # unordered containers in src/laige-sim/**, with per-line
170- # LAIGE-DETERM-EXCEPTION markers as the documented false-positive
171- # policy (docs/concepts/determinism.md). Platform-independent:
172- # Python 3 stdlib only, no setup step. The CTest suite runs it
173- # against fixture trees and the real tree in every P0 job as well
174- # (tests/tools, tests `determinism-lint-*`).
284+ # M1-DET-01: runs on every PR. The sim-source determinism scan
285+ # (the second half of the G-R8 guarantee; the first is the
286+ # compile-time trait checked by the trait_compile_* CTest
287+ # fixtures): forbids raw float/double and unordered containers in
288+ # src/laige-sim/**, with per-line LAIGE-DETERM-EXCEPTION markers as
289+ # the documented false-positive policy (docs/concepts/determinism.md).
290+ # Platform-independent: Python 3 stdlib only, no setup step. The
291+ # CTest suite runs it against fixture trees and the real tree in
292+ # every P0 job as well (tests/tools, tests `determinism-lint-*`).
175293 name : Determinism source scan (sim module)
176294 runs-on : ubuntu-24.04
177295 timeout-minutes : 5
@@ -181,13 +299,13 @@ jobs:
181299 run : python3 tools/laige-determinism-lint
182300
183301 api-manifest :
184- # M0-TOOL-01: runs on every PR (no ci:* condition) . The checked-in
185- # public API manifest (laige-api.json, PRD §9.4, NFR-13.1) must stay
186- # in sync with the public headers: this job regenerates it from the
187- # current headers with laige-api-scanner and fails on any drift, so
188- # adding a public symbol without regenerating the manifest fails CI.
189- # The CTest suite runs the same check against the real tree in every
190- # P0 job as well (tests/api, test `api-real-tree`).
302+ # M0-TOOL-01: runs on every PR. The checked-in public API manifest
303+ # (laige-api.json, PRD §9.4, NFR-13.1) must stay in sync with the
304+ # public headers: this job regenerates it from the current headers
305+ # with laige-api-scanner and fails on any drift, so adding a public
306+ # symbol without regenerating the manifest fails CI. The CTest
307+ # suite runs the same check against the real tree in every P0 job
308+ # as well (tests/api, test `api-real-tree`).
191309 name : Public API manifest drift
192310 runs-on : ubuntu-24.04
193311 timeout-minutes : 10
@@ -215,19 +333,15 @@ jobs:
215333 run : ./build/bin/laige-api-scanner --root . --check laige-api.json
216334
217335 detcheck :
218- # M1-DET-04: runs on every PR (no ci:* condition). The determinism
219- # checker (FR-11.5): the built-in synthetic scenario self-check,
220- # then the M1-SAMPLE-01 hello scenario asserted against the
221- # committed per-tick hash baselines (samples/hello/baselines/) on
222- # BOTH SimMath backends (fixed_point_16_16, float_pinned_32 — ADR
223- # 0002) with `hello --expect` (the laige-replay --expect contract:
224- # 0 match, 1 first divergence, 2 baseline read/contract error).
225- # The PR's P0 OS job's ctest repeats the same baseline assertion on
226- # its own platform (tests/sample: hello_baseline_fpx /
227- # hello_baseline_fp32); this tooling job guarantees the check runs
228- # on every PR, including ones labelled for non-Linux P0 OSes. The
229- # full two-configuration matrix (g++ vs clang++, Debug+ASan vs
230- # Release) runs on merges in the detcheck job of ci.yml.
336+ # M1-DET-04: runs on every PR. The determinism checker (FR-11.5):
337+ # the built-in synthetic scenario self-check, then the M1-SAMPLE-01
338+ # hello scenario asserted against the committed per-tick hash
339+ # baselines (samples/hello/baselines/) on BOTH SimMath backends
340+ # (fixed_point_16_16, float_pinned_32 — ADR 0002) with `hello
341+ # --expect` (the laige-replay --expect contract: 0 match, 1 first
342+ # divergence, 2 baseline read/contract error). The full
343+ # two-configuration matrix (g++ vs clang++, Debug+ASan vs Release)
344+ # runs on merges in the detcheck job of ci.yml.
231345 name : Determinism check
232346 runs-on : ubuntu-24.04
233347 timeout-minutes : 10
0 commit comments