Skip to content

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact tick count for bounded laige-run; CRLF-tolerant replay check script #90

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact tick count for bounded laige-run; CRLF-tolerant replay check script

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact tick count for bounded laige-run; CRLF-tolerant replay check script #90

Workflow file for this run

# ============================================================================
# Laige CI — P0 platform matrix on pull request (M0-CI-01)
#
# 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"): each pull request runs
# exactly ONE P0 OS, selected by the PR's ci:* label; the default (no
# label) is Linux. Merges to master run ALL P0 OSes via ci.yml, which is
# the "all per merge" half of the cadence.
#
# Label selector (apply at most one per PR; if several are present the
# highest-priority label wins, so exactly one P0 OS always runs):
#
# ci:macos → macos-arm64 + macos-intel jobs
# ci:windows → windows-msvc job
# ci:linux → linux-gcc + linux-clang jobs (also the no-label default)
#
# NOTE: the labels must be created once in the repository settings
# (GitHub does not create labels from workflows). A PR with an unknown
# or missing ci:* label simply falls back to the Linux default.
#
# Jobs, runner images, steps, and the 10-minute budget timeout are the
# same as in ci.yml (see its header for the full matrix documentation
# and the Windows multi-config note).
#
# Sanitizer lanes (M0-CI-02): linux-asan (LAIGE_ASAN=ON) and linux-tsan
# (LAIGE_TSAN=ON) run under the same condition as the default Linux
# jobs — i.e. on every PR that does not select another P0 OS — because
# they are part of the Linux P0 check (PRD §14: one P0 OS per PR). See
# the ci.yml header for the lane semantics (fatal sanitizer reports,
# report archiving, toolchain choice).
#
# Fuzz lane (PRD §14 "every commit (bounded)"; M0-TEST-01): no separate
# fuzz job — the `fuzz_json_parse` ctest entry (1000 deterministic runs
# of laige-fuzz on the json_parse target) runs inside every build
# job's ctest, instrumented in the ASan tree. Seed and nightly-long-run
# 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;
# vendored deps only included from their deps.lock `owner`) and reports
# the vendored-dependency count, which must stay ≤ 10 (PRD §11).
#
# Fork PRs: this workflow uses the pull_request event, which runs on the
# merge ref with a read-only token; checkout + build + ctest need nothing
# beyond contents:read. On such PRs the artifact upload (needs
# actions:write) fails with a 403, so the upload steps carry
# continue-on-error: true — the reports then remain in the job log and
# the tee'd output instead of the artifact.
# ============================================================================
name: CI (pull request)
on:
pull_request:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
linux-gcc:
name: Linux x64 (g++)
if: >-
!contains(github.event.pull_request.labels.*.name, 'ci:macos') &&
!contains(github.event.pull_request.labels.*.name, 'ci:windows')
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- 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++)
if: >-
!contains(github.event.pull_request.labels.*.name, 'ci:macos') &&
!contains(github.event.pull_request.labels.*.name, 'ci:windows')
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- 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++)
if: >-
!contains(github.event.pull_request.labels.*.name, 'ci:macos') &&
!contains(github.event.pull_request.labels.*.name, 'ci:windows')
runs-on: ubuntu-24.04
timeout-minutes: 10
# Job-scoped token: the archive step below needs actions:write for
# PRs from the base repository (fork PR tokens stay read-only,
# whatever this declares — see header).
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@v4
- 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). log_path keeps one
# report file per test process for the artifact upload below.
env:
ASAN_OPTIONS: "abort_on_error=1:halt_on_error=1:detect_leaks=1:log_path=${{ github.workspace }}/asan-reports/asan"
run: |
set -o pipefail
ctest --test-dir build-asan --output-on-failure 2>&1 | tee asan-ctest-output.txt
- name: Archive sanitizer reports
# continue-on-error: fork PRs run with a read-only token, so the
# upload 403s there (see header); the reports stay in the job log
# and the tee'd output. The job-level actions:write covers PRs
# from the base repository.
if: always()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: linux-asan-reports
path: |
asan-ctest-output.txt
asan-reports/
build-asan/Testing/Temporary/LastTest.log
linux-tsan:
name: Linux x64 TSan (clang++)
if: >-
!contains(github.event.pull_request.labels.*.name, 'ci:macos') &&
!contains(github.event.pull_request.labels.*.name, 'ci:windows')
runs-on: ubuntu-24.04
timeout-minutes: 10
# Job-scoped token: the archive step below needs actions:write (see
# the linux-asan job comment).
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@v4
- 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
- name: Archive sanitizer reports
# continue-on-error and job-level permissions: same rationale as
# the linux-asan job (fork PRs read-only, base-repo PRs can
# upload).
if: always()
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: linux-tsan-reports
path: |
tsan-ctest-output.txt
build-tsan/Testing/Temporary/LastTest.log
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:
- uses: actions/checkout@v4
- name: Configure
# CMAKE_BUILD_TYPE is ignored by the multi-config VS generator; the
# Debug configuration is pinned on the build/test steps below.
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build --config Debug -j
- name: Test (unit)
# The trait_compile_* fixtures invoke cl.exe DIRECTLY from a
# generated cmake -P script (execute_process), outside the VS
# generator's toolchain setup, so — unlike every other test in
# this job — they need the MSVC environment (INCLUDE/LIB/PATH)
# to find the CRT/STL headers; the plain runner shell lacks it
# and cl fails with C1083 on <cstdint> etc. This step imports
# the VS development environment into the current PowerShell
# process: vswhere locates the installation, VsDevCmd.bat (the
# canonical vcvars, present in every VS 2017+ install under
# Common7\Tools — called by relative name from that directory,
# so no path quoting is involved) sets it, and its `set` dump
# is imported variable by variable. ctest, cmake -P, and cl all
# inherit it. Everything else in this step is unchanged.
run: |
$vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -property installationPath
Push-Location (Join-Path $vs "Common7\Tools")
$envDump = cmd /c "call VsDevCmd.bat -arch=amd64 -host_arch=amd64 >nul && set"
Pop-Location
foreach ($line in $envDump) {
if ($line -match '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') {
Set-Item -Path "env:$($matches[1])" -Value $matches[2]
}
}
ctest --test-dir build -C Debug --output-on-failure
macos-arm64:
name: macOS arm64 (AppleClang)
if: contains(github.event.pull_request.labels.*.name, 'ci:macos')
runs-on: macos-15
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build -j
- name: Test (unit)
run: ctest --test-dir build --output-on-failure
macos-intel:
name: macOS Intel (AppleClang)
if: contains(github.event.pull_request.labels.*.name, 'ci:macos')
runs-on: macos-14
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build -j
- name: Test (unit)
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
# 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.
name: Include-graph lint + dependency count
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Lint include graph + report dependency count
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-*`).
name: Determinism source scan (sim module)
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Scan sim sources for raw FP / unordered containers
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`).
name: Public API manifest drift
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build scanner
run: cmake --build build --target laige-api-scanner -j
- name: Check manifest drift
run: ./build/bin/laige-api-scanner --root . --check laige-api.json
detcheck:
# M0-TOOL-02: runs on every PR (no ci:* condition). The determinism
# checker skeleton (FR-11.5): the built-in synthetic scenario
# self-check runs here so a broken checker lands red before the real
# scenarios exist. The real-scenario comparison (two build
# configurations of M1-SAMPLE-01's hello) is skipped until that
# sample lands; M1-DET-04 activates it (see the job in ci.yml for
# the full note).
name: Determinism check
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build -j
- name: Synthetic scenario self-check
run: ./build/bin/laige-detcheck --scenario=synthetic
- name: Real scenario (M1-SAMPLE-01)
# Skipped until M1-SAMPLE-01 lands the hello scenario binary;
# M1-DET-04 replaces this step with the two-configuration
# comparison (see the job in ci.yml).
run: |
if [ -x samples/hello/bin/hello ]; then
./build/bin/laige-detcheck \
--run-a=samples/hello/bin/hello \
--run-b=samples/hello/bin/hello
else
echo "skipped: no real scenario yet (M1-SAMPLE-01)"
fi