Skip to content

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact ti… #95

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact ti…

[M1-DET-03] Fix macOS/Windows CI: replay tool strncpy C4996; exact ti… #95

Workflow file for this run

# ============================================================================
# Laige CI — P0 platform matrix on merge (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"):
# * this workflow runs on every push to the default branch (master) —
# i.e. on merge — and on manual workflow_dispatch:
# ALL ten jobs run (the five P0 OS/variant jobs, the two sanitizer
# lanes (M0-CI-02), and the three platform-independent tooling jobs:
# the include-graph lint with dependency-count metric (M0-CI-03),
# the public API manifest drift check (M0-TOOL-01), the
# determinism source scan (M1-DET-01), and the determinism check
# (M0-TOOL-02)).
#
# Fuzz lane (PRD §14 "every commit (bounded)"; M0-TEST-01): there is 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 above, instrumented in the ASan tree. Seed and
# nightly-long-run conventions: docs/testing.md.
# * Pull requests run exactly ONE P0 OS (label-selectable, default
# Linux) in the companion workflow .github/workflows/ci-pull.yml.
#
# 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
# only in M0):
#
# linux-gcc Linux x64, g++ (ubuntu-24.04)
# linux-clang Linux x64, clang++ (ubuntu-24.04)
# linux-asan Linux x64, clang++, LAIGE_ASAN=ON (ubuntu-24.04)
# linux-tsan Linux x64, clang++, LAIGE_TSAN=ON (ubuntu-24.04)
# windows-msvc Windows x64, MSVC 2022 (windows-2022)
# macos-arm64 macOS arm64, AppleClang (macos-15)
# macos-intel macOS Intel, AppleClang (macos-14)
# include-lint Include-graph lint + dependency count (M0-CI-03)
# api-manifest Public API manifest drift check (M0-TOOL-01)
# det-lint Determinism source scan (M1-DET-01)
# detcheck Determinism check (M0-TOOL-02)
#
# The include-lint job (M0-CI-03; NFR-8.11, NFR-8.13) is platform-
# independent — it parses the #include edges of src/** (PRD §10.1 rules:
# laige-core includes nothing internal, arrows only downward in the
# module stack, vendored deps included only from their deps.lock owner)
# and asserts the vendored-dependency count stays ≤ 10 (PRD §11) while
# printing the list. It is Python 3 stdlib only, so one runner image
# suffices; it runs on every PR too (ci-pull.yml, same job definition).
#
# Sanitizer lanes (M0-CI-02; NFR-8.2 "Core is ASan + UBSan + TSan clean in
# CI", AGENTS TEST-006):
#
# * linux-asan builds the canonical build-asan tree (LAIGE_ASAN=ON:
# whole tree instrumented with ASan+UBSan) and runs the unit suites.
# Any sanitizer report is fatal to the test process — UBSan via
# -fno-sanitize-recover=all (wired by CMake), ASan via
# abort_on_error=1:halt_on_error=1 below — so ctest exits non-zero and
# the job fails: "fail build on any sanitizer report".
# * linux-tsan builds the canonical build-tsan tree (LAIGE_TSAN=ON:
# whole tree instrumented with TSan). tests/ applies
# TSAN_OPTIONS=halt_on_error=1 per test (M0-BUILD-01), so the first
# data race report kills the test process and ctest fails.
#
# Toolchain: clang++ for both lanes — TSan is most mature on Clang and
# -fsanitize=undefined enables a superset of the GCC UB checks; the GCC
# build itself is covered by the linux-gcc job. Scope is Linux-only per
# the M0-CI-02 roadmap step (the MSVC/AppleClang sanitizer flag wiring
# already exists in CMake for later extension).
#
# Reports are archived as artifacts on every run, green or red:
# - the tee'd ctest output carries every report that reached stderr
# (ASan and TSan both print reports to stderr);
# - ASan additionally writes one report file per test process under
# asan-reports/ (ASAN_OPTIONS log_path below);
# - <tree>/Testing/Temporary/LastTest.log keeps ctest's full raw
# output of every test.
#
# Build budget (PRD §8.1: clean build ≤ 10 min on CI; roadmap M0-CI-01
# "matrix runs in < 10 min"): every build job carries timeout-minutes: 10,
# so a job (and therefore the whole matrix, which runs the jobs in
# parallel) longer than the budget fails instead of dragging on. The
# include-lint job is a lint job, not a build, and carries its own
# timeout-minutes: 5.
#
# Windows note: CMake's default generator there is the multi-config
# "Visual Studio 17 2022" generator, which ignores CMAKE_BUILD_TYPE and
# would build every configuration from the canonical `cmake --build build
# -j`. The Windows job therefore pins the canonical Debug configuration
# explicitly on the build and test steps (--config Debug / -C Debug); the
# single-config platforms run the canonical commands verbatim. The
# Windows Test step additionally imports the VS 2022 development
# environment (vswhere + VsDevCmd.bat, same step, before ctest) because
# the trait_compile_* fixtures invoke cl.exe directly from a generated
# cmake -P script — outside the VS generator's toolchain setup — and
# need the MSVC environment (INCLUDE/LIB/PATH) to find the CRT/STL
# headers.
#
# AC-6.1 (same source tree, same feature set, no per-OS feature flags): no
# build job passes feature flags; every build job builds the identical
# tree with the only variation being the platform/compiler (include-lint
# does not build).
# ============================================================================
name: CI (merge)
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
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: 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: 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
# Job-scoped token: the archive step below needs actions:write, which
# the workflow-level contents:read token does not grant (GitHub
# Actions has no step-level permissions).
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). detect_leaks=1 (the
# Linux default) is stated explicitly. 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
# Runs green or red: on a red run the reports live in this
# artifact. The job-level token grants actions:write.
if: always()
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++)
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
# Runs green or red: on a red run the race report lives in this
# artifact. The job-level token grants actions:write.
if: always()
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)
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)
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)
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 (NFR-8.11, NFR-8.13): platform-independent repository
# check — the include-graph lint over src/** plus the vendored-
# dependency count (budget 10, PRD §11). See the header note.
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: platform-independent repository check — 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).
# Python 3 stdlib only, so one runner image suffices; it runs on
# every PR too (ci-pull.yml, same job definition). 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 (PRD §9.4, NFR-13.1): the checked-in public API manifest
# (laige-api.json) must stay in sync with the public headers. This
# job regenerates the manifest 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 (FR-11.5; AGENTS ARCH-010, TEST-004): the determinism
# checker skeleton. This job runs the built-in synthetic scenario
# self-check (two in-process runs, per-tick hash comparison — the
# step's Verify clause), so a broken checker lands red here before
# the real scenarios exist. The real-scenario comparison — two build
# configurations of M1-SAMPLE-01's hello scenario (Debug+ASan vs
# Release, plus g++ vs clang++ on Linux) — is SKIPPED until that
# sample lands; M1-DET-04 activates it and records the result 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).
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, e.g.:
# ./build/bin/laige-detcheck --run-a=build-asan/bin/hello
# --run-b=build/bin/hello
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