From 6c9ccbd1f122b41bb77d58b41b368ac66257a9b8 Mon Sep 17 00:00:00 2001 From: Pascal Severin Date: Sun, 11 Oct 2026 20:49:06 +0200 Subject: [PATCH] Fix the UiRenderSmoke golden probe and run the full CI matrix on every PR The 'P red' probe of UiRenderSmoke.GoldenUiScene read pixel (20, 20), which the smoke scene's button (16, 16, 20 x 14, green uv, declared after the panel, drawn on top by the monotone-z contract) covers: the correct color there is the button's green, so the probe failed on all four Linux lanes of the merge run (run 38163032436) while the full-frame CPU-reference comparison reported zero mismatches (the reference inherits the quad UVs and cannot catch the intent error). The probe now reads (12, 12), inside the panel and outside the button. The PR lane (.github/workflows/ci-pull.yml) now runs the same eleven jobs as the merge lane, unconditionally: the four Linux trees, Windows, both macOS lanes, and the four tooling jobs. The ci:windows / ci:macos label selector is removed from both workflows: no job is gated on a label and no test is ever skipped. The PR-lane sanitizer jobs upload no artifacts (the pull_request token is read-only, including fork PRs); their tee'd ctest output keeps every sanitizer report in the job log, and the merge lane still archives artifacts. AGENTS.md section 5 gains the local Linux test gate: before any push to GitHub, all four Linux CI trees (g++, clang++, ASan+UBSan, TSan) must be built and their full unfiltered ctest suites must pass locally, in parallel when the machine allows. --- .github/workflows/ci-pull.yml | 260 ++++++++++++++++++------- .github/workflows/ci.yml | 21 +- AGENTS.md | 33 ++++ tests/laige-render/ui_render_tests.cpp | 5 +- 4 files changed, 236 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci-pull.yml b/.github/workflows/ci-pull.yml index bf8228a..c2869ee 100644 --- a/.github/workflows/ci-pull.yml +++ b/.github/workflows/ci-pull.yml @@ -1,44 +1,39 @@ # ============================================================================ -# Laige CI — P0 platform matrix on pull request (M0-CI-01) +# Laige CI — full P0 platform matrix on pull request (M0-CI-01, +# re-baselined 2026-10-11) # # Roadmap: roadmap/M0-foundations.md, step M0-CI-01 # PRD refs: §6 (P0 platforms, AC-6.1), §14 (cadence), §8.1 (build budget) # -# Cadence: the PR lane covers only the P0 OSes that cannot be verified -# locally — Windows and macOS. Linux is verified on the developer -# machine with the canonical trees from -# docs/getting-started/building.md (including the LAIGE_ASAN=ON and -# LAIGE_TSAN=ON instrumented trees), so this workflow carries no Linux -# jobs. Merges to master run the FULL matrix — every P0 OS/variant job, -# both sanitizer lanes, and all tooling jobs — via ci.yml, which is -# the "all per merge" half of the PRD §14 cadence. +# Cadence (PRD §14, as amended 2026-10-11 — "test everything, all the +# time"): this workflow runs on EVERY pull request, and ALL eleven jobs +# run — unconditionally: # -# Label selector (apply at most one per PR; if both are present -# ci:macos wins, so exactly one P0 OS always runs): +# * the five P0 OS jobs (linux-gcc, linux-clang, windows-msvc, +# macos-arm64, macos-intel); +# * the two sanitizer lanes (linux-asan, linux-tsan; M0-CI-02); +# * the four platform-independent tooling jobs (include-lint, +# determinism-lint, api-manifest, detcheck). # -# ci:macos → macos-arm64 + macos-intel jobs -# ci:windows → windows-msvc job -# (no label) → no P0 OS build; the platform-independent tooling jobs -# (include-lint, determinism-lint, api-manifest, -# detcheck) still run on every PR. -# -# NOTE: the labels must be created once in the repository settings -# (GitHub does not create labels from workflows). A PR with no ci:* -# label simply runs the tooling jobs. The former ci:linux label is no -# longer a selector (Linux is verified locally); a PR carrying it gets -# the no-label treatment. +# The former ci:windows / ci:macos label selector is REMOVED: no job is +# gated on a label and no test is ever skipped. Linux is no longer +# "verified locally only" — it runs in the PR lane exactly as in the +# merge lane (ci.yml), so a PR that breaks a Linux tree fails in the +# PR lane before it can reach master. Local verification of the Linux +# trees before pushing remains a developer obligation (AGENTS.md, +# section 5: the local Linux test gate). # # Jobs, runner images, steps, and the build-budget timeouts are the # same as in ci.yml — including the macOS exception (the two macOS # jobs carry timeout-minutes: 15, the shared-runners tail latency), # documented in ci.yml's header (the full matrix documentation and -# the Windows multi-config note). -# -# Sanitizer lanes (M0-CI-02): the linux-asan (LAIGE_ASAN=ON) and -# linux-tsan (LAIGE_TSAN=ON) lanes no longer run on PRs (no Linux jobs -# in the PR lane); they run on every merge via ci.yml. See the ci.yml -# header for the lane semantics (fatal sanitizer reports, report -# archiving, toolchain choice). +# the Windows multi-config note). The one difference: the sanitizer +# jobs upload no artifacts here. The pull_request GITHUB_TOKEN is +# read-only (contents:read for same-repo PRs, read-only for fork PRs — +# Actions cannot grant actions:write on this event), so the report +# files are not archived; the tee'd ctest output keeps every sanitizer +# report in the job log, which is enough to diagnose a red PR. The +# merge lane (ci.yml) still archives them as artifacts. # # Fuzz lane (PRD §14 "every commit (bounded)"; M0-TEST-01): no separate # fuzz job — the `fuzz_json_parse` ctest entry (1000 deterministic runs @@ -47,11 +42,10 @@ # conventions: docs/testing.md. # # Include-graph lint (M0-CI-03; NFR-8.11, NFR-8.13): the `include-lint` -# job runs on EVERY pull request, independent of the ci:* label selector -# — it is a platform-independent repository check (Python 3 stdlib only), -# not a P0 OS build, so it does not interact with the "one P0 OS per PR" -# cadence. It enforces the PRD §10.1 include rules over src/** (laige-core -# depends on nothing internal; arrows only downward in the module stack; +# job runs on EVERY pull request — it is a platform-independent +# repository check (Python 3 stdlib only), not a P0 OS build. It +# enforces the PRD §10.1 include rules over src/** (laige-core depends +# on nothing internal; arrows only downward in the module stack; # vendored deps only included from their deps.lock `owner`) and reports # the vendored-dependency count, which must stay ≤ 10 (PRD §11). # @@ -73,11 +67,138 @@ concurrency: cancel-in-progress: true jobs: + linux-gcc: + name: Linux x64 (g++) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install GLFW X11 headers (laige-render) + # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/ + # CMakeLists.txt) needs only the X11 *headers* at configure time + # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the + # ubuntu-24.04 runner image ships none of them. The libegl/libgl + # packages are the RUNTIME GL stack for the headless GL smoke + # (Mesa surfaceless EGL + llvmpipe software GL; the engine and + # GLAD load them by dlopen at runtime) (M2-GL-01). + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq \ + libx11-dev libxcursor-dev libxrandr-dev \ + libxinerama-dev libxi-dev libxext-dev \ + libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ + - name: Build + run: cmake --build build -j + - name: Test (unit) + run: ctest --test-dir build --output-on-failure + + linux-clang: + name: Linux x64 (clang++) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install GLFW X11 headers (laige-render) + # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/ + # CMakeLists.txt) needs only the X11 *headers* at configure time + # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the + # ubuntu-24.04 runner image ships none of them. The libegl/libgl + # packages are the RUNTIME GL stack for the headless GL smoke + # (Mesa surfaceless EGL + llvmpipe software GL; the engine and + # GLAD load them by dlopen at runtime) (M2-GL-01). + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq \ + libx11-dev libxcursor-dev libxrandr-dev \ + libxinerama-dev libxi-dev libxext-dev \ + libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ + - name: Build + run: cmake --build build -j + - name: Test (unit) + run: ctest --test-dir build --output-on-failure + + linux-asan: + name: Linux x64 ASan+UBSan (clang++) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + # No artifact step here: the pull_request token is read-only (see + # the header). The tee keeps every report in the job log. + steps: + - uses: actions/checkout@v4 + - name: Install GLFW X11 headers (laige-render) + # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/ + # CMakeLists.txt) needs only the X11 *headers* at configure time + # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the + # ubuntu-24.04 runner image ships none of them. The libegl/libgl + # packages are the RUNTIME GL stack for the headless GL smoke + # (Mesa surfaceless EGL + llvmpipe software GL; the engine and + # GLAD load them by dlopen at runtime) (M2-GL-01). + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq \ + libx11-dev libxcursor-dev libxrandr-dev \ + libxinerama-dev libxi-dev libxext-dev \ + libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri + - name: Configure (ASan+UBSan) + run: cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DLAIGE_ASAN=ON + - name: Build (ASan+UBSan) + run: cmake --build build-asan -j + - name: Test (unit, ASan+UBSan) + # Every sanitizer report is fatal to the test process: ASan via + # abort_on_error=1:halt_on_error=1 here, UBSan via + # -fno-sanitize-recover=all (wired by CMake). detect_leaks=1 (the + # Linux default) is stated explicitly. + env: + ASAN_OPTIONS: "abort_on_error=1:halt_on_error=1:detect_leaks=1" + run: | + set -o pipefail + ctest --test-dir build-asan --output-on-failure 2>&1 | tee asan-ctest-output.txt + + linux-tsan: + name: Linux x64 TSan (clang++) + # 30 min (not 10): TSan is the slowest lane by design (~5-15x + # instrumentation overhead), and the suite has grown past the old + # margin (the documented rationale, with the evidence runs, is in + # the linux-tsan job of ci.yml). + # No artifact step here: the pull_request token is read-only (see + # the header). The tee keeps the race report in the job log. + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install GLFW X11 headers (laige-render) + # GLFW's X11 backend (GLFW_BUILD_WAYLAND=OFF in src/laige-render/ + # CMakeLists.txt) needs only the X11 *headers* at configure time + # (GLFW 3.5 dlopens the X11/GL libraries at runtime); the + # ubuntu-24.04 runner image ships none of them. The libegl/libgl + # packages are the RUNTIME GL stack for the headless GL smoke + # (Mesa surfaceless EGL + llvmpipe software GL; the engine and + # GLAD load them by dlopen at runtime) (M2-GL-01). + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq \ + libx11-dev libxcursor-dev libxrandr-dev \ + libxinerama-dev libxi-dev libxext-dev \ + libegl1 libgl1 libegl-mesa0 libglx-mesa0 libgl1-mesa-dri + - name: Configure (TSan) + run: cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ -DLAIGE_TSAN=ON + - name: Build (TSan) + run: cmake --build build-tsan -j + - name: Test (unit, TSan) + # TSAN_OPTIONS=halt_on_error=1 is applied per test by tests/ + # (M0-BUILD-01), so the first data race report kills the test + # process and ctest fails. The race report is printed to stderr + # and captured by the tee below. + run: | + set -o pipefail + ctest --test-dir build-tsan --output-on-failure 2>&1 | tee tsan-ctest-output.txt + windows-msvc: name: Windows x64 (MSVC 2022) - if: >- - contains(github.event.pull_request.labels.*.name, 'ci:windows') && - !contains(github.event.pull_request.labels.*.name, 'ci:macos') runs-on: windows-2022 timeout-minutes: 10 steps: @@ -116,7 +237,6 @@ jobs: macos-arm64: name: macOS arm64 (AppleClang) - if: contains(github.event.pull_request.labels.*.name, 'ci:macos') runs-on: macos-15 # The shared macOS runners' tail latency exceeds the 10-min build # budget (the macOS exception, documented in ci.yml's header). @@ -132,7 +252,6 @@ jobs: macos-intel: name: macOS Intel (AppleClang) - if: contains(github.event.pull_request.labels.*.name, 'ci:macos') runs-on: macos-14 # The shared macOS runners' tail latency exceeds the 10-min build # budget (the macOS exception, documented in ci.yml's header). @@ -147,12 +266,12 @@ jobs: run: ctest --test-dir build --output-on-failure include-lint: - # M0-CI-03: runs on every PR (no ci:* condition). Platform-independent: - # the lint is Python 3 stdlib only (no setup step needed) and checks - # the repository layout, not a compiler-specific build. The CTest + # M0-CI-03: runs on every PR. Platform-independent: the lint is + # Python 3 stdlib only (no setup step needed) and checks the + # repository layout, not a compiler-specific build. The CTest # suite runs it against the real tree in every P0 job as well # (tests/tools, test `include-lint-real-tree`), so a broken include - # fails even a PR that selects another P0 OS. + # fails every job, not just this one. name: Include-graph lint + dependency count runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -162,16 +281,15 @@ jobs: run: python3 tools/laige-include-lint determinism-lint: - # M1-DET-01: runs on every PR (no ci:* condition). The sim-source - # determinism scan (the second half of the G-R8 guarantee; the - # first is the compile-time trait checked by the - # trait_compile_* CTest fixtures): forbids raw float/double and - # unordered containers in src/laige-sim/**, with per-line - # LAIGE-DETERM-EXCEPTION markers as the documented false-positive - # policy (docs/concepts/determinism.md). Platform-independent: - # Python 3 stdlib only, no setup step. The CTest suite runs it - # against fixture trees and the real tree in every P0 job as well - # (tests/tools, tests `determinism-lint-*`). + # M1-DET-01: runs on every PR. The sim-source determinism scan + # (the second half of the G-R8 guarantee; the first is the + # compile-time trait checked by the trait_compile_* CTest + # fixtures): forbids raw float/double and unordered containers in + # src/laige-sim/**, with per-line LAIGE-DETERM-EXCEPTION markers as + # the documented false-positive policy (docs/concepts/determinism.md). + # Platform-independent: Python 3 stdlib only, no setup step. The + # CTest suite runs it against fixture trees and the real tree in + # every P0 job as well (tests/tools, tests `determinism-lint-*`). name: Determinism source scan (sim module) runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -181,13 +299,13 @@ jobs: run: python3 tools/laige-determinism-lint api-manifest: - # M0-TOOL-01: runs on every PR (no ci:* condition). The checked-in - # public API manifest (laige-api.json, PRD §9.4, NFR-13.1) must stay - # in sync with the public headers: this job regenerates it from the - # current headers with laige-api-scanner and fails on any drift, so - # adding a public symbol without regenerating the manifest fails CI. - # The CTest suite runs the same check against the real tree in every - # P0 job as well (tests/api, test `api-real-tree`). + # M0-TOOL-01: runs on every PR. The checked-in public API manifest + # (laige-api.json, PRD §9.4, NFR-13.1) must stay in sync with the + # public headers: this job regenerates it from the current headers + # with laige-api-scanner and fails on any drift, so adding a public + # symbol without regenerating the manifest fails CI. The CTest + # suite runs the same check against the real tree in every P0 job + # as well (tests/api, test `api-real-tree`). name: Public API manifest drift runs-on: ubuntu-24.04 timeout-minutes: 10 @@ -215,19 +333,15 @@ jobs: run: ./build/bin/laige-api-scanner --root . --check laige-api.json detcheck: - # M1-DET-04: runs on every PR (no ci:* condition). The determinism - # checker (FR-11.5): the built-in synthetic scenario self-check, - # then the M1-SAMPLE-01 hello scenario asserted against the - # committed per-tick hash baselines (samples/hello/baselines/) on - # BOTH SimMath backends (fixed_point_16_16, float_pinned_32 — ADR - # 0002) with `hello --expect` (the laige-replay --expect contract: - # 0 match, 1 first divergence, 2 baseline read/contract error). - # The PR's P0 OS job's ctest repeats the same baseline assertion on - # its own platform (tests/sample: hello_baseline_fpx / - # hello_baseline_fp32); this tooling job guarantees the check runs - # on every PR, including ones labelled for non-Linux P0 OSes. The - # full two-configuration matrix (g++ vs clang++, Debug+ASan vs - # Release) runs on merges in the detcheck job of ci.yml. + # M1-DET-04: runs on every PR. The determinism checker (FR-11.5): + # the built-in synthetic scenario self-check, then the M1-SAMPLE-01 + # hello scenario asserted against the committed per-tick hash + # baselines (samples/hello/baselines/) on BOTH SimMath backends + # (fixed_point_16_16, float_pinned_32 — ADR 0002) with `hello + # --expect` (the laige-replay --expect contract: 0 match, 1 first + # divergence, 2 baseline read/contract error). The full + # two-configuration matrix (g++ vs clang++, Debug+ASan vs Release) + # runs on merges in the detcheck job of ci.yml. name: Determinism check runs-on: ubuntu-24.04 timeout-minutes: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35eddc2..eaefd64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,8 @@ # Roadmap: roadmap/M0-foundations.md, step M0-CI-01 # PRD refs: §6 (P0 platforms, AC-6.1), §14 (cadence), §8.1 (build budget) # -# Cadence (PRD §14: "one per PR, all per merge"): +# Cadence (PRD §14, as amended 2026-10-11 — "test everything, all the +# time"): # * this workflow runs on every push to the default branch (master) — # i.e. on merge — and on manual workflow_dispatch: # ALL eleven jobs run (the seven P0 OS/variant jobs — the five P0 OS @@ -19,11 +20,13 @@ # deterministic runs of laige-fuzz on the json_parse target) runs inside # every build job's ctest above, instrumented in the ASan tree. Seed and # nightly-long-run conventions: docs/testing.md. -# * Pull requests run only the P0 OSes that cannot be verified -# locally — Windows and/or macOS, selected by the PR's ci:windows / -# ci:macos label (no label: no P0 OS build; Linux is verified -# locally) — in the companion workflow -# .github/workflows/ci-pull.yml. +# * Pull requests run the SAME eleven jobs, unconditionally, in the +# companion workflow .github/workflows/ci-pull.yml. The former +# ci:windows / ci:macos label selector is removed: no job is gated +# on a label and no test is ever skipped (Linux included). Local +# verification of the Linux trees before pushing remains a +# developer obligation (AGENTS.md, section 5: the local Linux test +# gate). # # One job per P0 OS/variant (PRD §6). Each job runs the canonical flow from # docs/getting-started/building.md — configure → build → ctest (unit tests @@ -472,9 +475,9 @@ jobs: # stated per ARCH-010). # # Like include-lint and api-manifest, the job runs on every PR and - # merge, independent of the ci:* label selector — it is a tooling - # check, not an additional P0 OS build (the PRD §14 cadence is - # unchanged). + # merge unconditionally (no label selector — see the header) — it is + # a tooling check, not an additional P0 OS build (the PRD §14 + # cadence is unchanged). name: Determinism check runs-on: ubuntu-24.04 timeout-minutes: 20 diff --git a/AGENTS.md b/AGENTS.md index e41ad50..d7cdf75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -128,6 +128,39 @@ Before finishing: 5. Do not claim success when required validation was skipped. Explain exactly what remains and why. +### Local Linux test gate (before any push to GitHub) + +Before pushing any branch to GitHub, the agent MUST build all four +Linux CI trees and run their FULL `ctest` suites locally to green, +using the canonical commands of +[docs/getting-started/building.md](docs/getting-started/building.md) +(the identical commands the Linux jobs of +`.github/workflows/ci.yml` run): + +1. Linux g++: `cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug + -DCMAKE_CXX_COMPILER=g++`, then `cmake --build build -j`, then + `ctest --test-dir build --output-on-failure`. +2. Linux clang++: the same canonical commands with + `-DCMAKE_CXX_COMPILER=clang++`, in its own build directory. +3. Linux ASan+UBSan: `cmake -S . -B build-asan + -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ + -DLAIGE_ASAN=ON`, then `cmake --build build-asan -j`, then + `ctest --test-dir build-asan --output-on-failure`. +4. Linux TSan: `cmake -S . -B build-tsan + -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++ + -DLAIGE_TSAN=ON`, then `cmake --build build-tsan -j`, then + `ctest --test-dir build-tsan --output-on-failure`. + +When the machine has enough cores and memory, the four trees MUST run +in parallel (independent build directories; one build plus one full +ctest per tree). "All the tests" means the complete unfiltered `ctest` +list of each tree — no filter, no subset: the merge-lane Linux jobs +run the unfiltered suite, and the local gate MUST match it. A push to +GitHub without the gate is a rule violation (CORE-008): the PR lane +(`.github/workflows/ci-pull.yml`) now runs the full matrix including +all four Linux trees, but the gate exists so a red Linux tree is +caught on the developer machine before it reaches GitHub. + ## 6. Architecture boundaries - **ARCH-001:** Keep simulation, rendering, platform, asset, audio, networking, diff --git a/tests/laige-render/ui_render_tests.cpp b/tests/laige-render/ui_render_tests.cpp index cc7eee9..07ba030 100644 --- a/tests/laige-render/ui_render_tests.cpp +++ b/tests/laige-render/ui_render_tests.cpp @@ -1327,8 +1327,11 @@ TEST(UiRenderSmoke, GoldenUiScene) { // The opaque solid texels over the opaque clear: src.a = 1 -> the // texel's color, exactly (the GL 8-bit conversion of the exact // blend result). + // (12, 12) is inside the panel but outside the button (16, 16, 20 x + // 14, declared later, drawn on top by the monotone-z contract): the + // button's green uv covers every panel pixel with x, y >= 16. { - const std::array p = readScreen(20, 20); // the P red + const std::array p = readScreen(12, 12); // the P red EXPECT_EQ(255u, p[0]); EXPECT_EQ(0u, p[1]); EXPECT_EQ(0u, p[2]);