scenario: fill the corpus until both coverage axes are green #744
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main, master] | |
| pull_request: | |
| # Supersede a run that a newer commit on the same ref has already made | |
| # obsolete. Without this every push queued a *fresh* full run of the ~16 legs | |
| # below and left the previous one in the queue -- so a burst of six merges to | |
| # master queued six complete generations of CI, none of which could be | |
| # cancelled by the next. | |
| # | |
| # That is not only this repository's problem. GitHub's concurrency allowance is | |
| # per ACCOUNT, not per repository, so those generations sit in one queue shared | |
| # with every other repo in the org and starve them: `fastcached` had fifteen | |
| # jobs waiting behind this workflow with none running, and the oldest run here | |
| # had been queued for over five hours. | |
| # | |
| # `docs.yml`, `wasm-demo.yml` and `wasm-ladder.yml` already do this; `ci.yml` is | |
| # by far the largest workflow and was the one without it. | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| # vcpkg's x-gha binary caching backend has been removed upstream (confirmed | |
| # live: every Windows CI run logged "warning: The 'x-gha' binary caching | |
| # backend has been removed" and silently cached nothing, rebuilding every | |
| # vcpkg dependency from source on every run). Replaced with the officially | |
| # documented successor: a NuGet-based provider backed by GitHub Packages' | |
| # own NuGet feed, which every repo already has -- no new secret, just the | |
| # windows job's own GITHUB_TOKEN and packages: write (see that job's | |
| # permissions: block and its "Add NuGet source" step, which is what | |
| # actually defines the `github` source this string names). | |
| VCPKG_BINARY_SOURCES: "clear;nuget,github,readwrite" | |
| SCCACHE_DIR: /home/runner/.cache/sccache | |
| # Caps each cache family's on-disk sccache blob. Cache keys are scoped by | |
| # preset name alone (gcc-debug, gcc-release, clang-debug, clang-release, | |
| # clang-asan, clang-tsan, clang-ubsan, clang-coverage), not by job name -- | |
| # sccache disambiguates by the actual compiler invocation, so every job that | |
| # builds a given preset (e.g. linux-compilers', linux-qt's, ladder-tests' | |
| # and valgrind's gcc-debug legs) shares one cache family instead of keeping | |
| # its own. That is 8 families total rather than one per job, so this cap | |
| # times 8 stays comfortably inside GitHub's 10 GB per-repo actions-cache | |
| # budget where 16 job-scoped 1 GB blobs did not, and every job's build now | |
| # also warms the cache the next job's build reads from. | |
| # | |
| # The write side still only fires on a push to master (see each job's "Save | |
| # sccache" step) so generations do not keep multiplying underneath this. | |
| # Whether 1 GB is the right cap per family is a question for the | |
| # `sccache --show-stats` output those steps emit -- deliberately left | |
| # unchanged here so the effect of the cache-sharing change can be read on | |
| # its own. | |
| SCCACHE_CACHE_SIZE: "1G" | |
| CLANG_VERSION: "22" | |
| # MORPH_BUILD_FORMS_QML needs Qt 6.5+; ubuntu-24.04 apt still ships 6.4.2. | |
| QT_VERSION: "6.8.1" | |
| jobs: | |
| # ── Windows: MSVC + clang-cl ────────────────────────────────────────── | |
| windows: | |
| name: Windows / ${{ matrix.preset }} | |
| runs-on: windows-latest | |
| # packages: write is what lets vcpkg push built binaries to this repo's | |
| # GitHub Packages NuGet feed (see "Add NuGet source" below); nothing else | |
| # in this job needs more than the default contents: read the workflow | |
| # grants at the top level. | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| preset: [cl-debug, cl-release, clangcl-debug, clangcl-release] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup MSVC environment | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| - name: Setup vcpkg | |
| uses: lukka/run-vcpkg@v11 | |
| with: | |
| vcpkgGitCommitId: c3867e714dd3a51c272826eea77267876517ed99 | |
| # Defines the `github` source VCPKG_BINARY_SOURCES (top-level env:) | |
| # names. GITHUB_TOKEN is scoped to this repo and already carries | |
| # packages: write from this job's permissions: block above -- no new | |
| # secret needed. `dotnet nuget` rather than `nuget.exe`: it ships with | |
| # the .NET SDK already on every GitHub-hosted Windows runner, so | |
| # nothing extra needs installing. | |
| - name: Add NuGet source (GitHub Packages, for vcpkg binary caching) | |
| shell: pwsh | |
| run: | | |
| dotnet nuget add source "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" ` | |
| --name github ` | |
| --username "${{ github.repository_owner }}" ` | |
| --password "${{ secrets.GITHUB_TOKEN }}" ` | |
| --store-password-in-clear-text | |
| - name: Configure | |
| run: cmake --preset ${{ matrix.preset }} | |
| - name: Build | |
| run: cmake --build --preset ${{ matrix.preset }} | |
| - name: Test | |
| if: matrix.preset == 'cl-debug' || matrix.preset == 'clangcl-debug' | |
| run: ctest --preset ${{ matrix.preset }} | |
| # ── Linux: GCC and Clang plain builds ──────────────────────────────── | |
| linux-compilers: | |
| name: Linux / ${{ matrix.preset }} | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| preset: [gcc-debug, gcc-release, clang-debug, clang-release] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-compilers-${{ matrix.preset }}-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-compilers-${{ matrix.preset }}- | |
| - name: Install GCC 15 and ninja | |
| if: startsWith(matrix.preset, 'gcc-') | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 | |
| sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 | |
| sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 | |
| - name: Install Clang ${{ env.CLANG_VERSION }} from apt.llvm.org | |
| if: startsWith(matrix.preset, 'clang-') | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y ninja-build catch2 | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| # Keyed by preset alone, not this job's name: gcc-debug/clang-debug's | |
| # object files are the same ones linux-qt, ladder-tests, valgrind (for | |
| # gcc-debug) and clang-tidy (for clang-debug) compile with extra | |
| # -DMORPH_BUILD_* flags layered on top. sccache disambiguates by the | |
| # actual compiler invocation, not by this key, so sharing one cache | |
| # entry per preset across jobs only raises the hit rate -- it cannot | |
| # serve a wrong object for a different flag set. | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| restore-keys: sccache-${{ matrix.preset }}- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| - name: Configure (gcc) | |
| if: startsWith(matrix.preset, 'gcc-') | |
| run: | | |
| cmake --preset ${{ matrix.preset }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| - name: Configure (clang) | |
| if: startsWith(matrix.preset, 'clang-') | |
| run: | | |
| cmake --preset ${{ matrix.preset }} \ | |
| -DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| - name: Build | |
| run: cmake --build --preset ${{ matrix.preset }} | |
| - name: Test | |
| if: endsWith(matrix.preset, '-debug') | |
| run: ctest --preset ${{ matrix.preset }} | |
| # ── Linux: sanitizers + coverage (all clang) ───────────────────────── | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| linux-sanitizers: | |
| name: Linux / ${{ matrix.preset }} | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| preset: [clang-asan, clang-tsan, clang-ubsan, clang-coverage] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-sanitizers-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-sanitizers- | |
| - name: Install Clang ${{ env.CLANG_VERSION }} from apt.llvm.org | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y ninja-build catch2 libsqlite3-dev | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| # Only the coverage leg builds the ladder: examples/common's | |
| # hand-written GUI/testkit code is real coverage of morph's client | |
| # stack (Bridge, backends, QtExecutor, completions — see | |
| # examples/TESTING.md's "round-7 T4 reframe"), so it belongs in the | |
| # coverage number the same way the models it will host later do | |
| # (examples/IMPLEMENTATION.md rule 5). asan/tsan/ubsan skip this, same | |
| # as before — "a GUI stack under TSan is mostly noise" — coverage | |
| # instrumentation carries none of that risk. | |
| - name: Install ODBC + SQLite driver (coverage leg only) | |
| if: matrix.preset == 'clang-coverage' | |
| run: | | |
| # unixodbc-dev + libsqliteodbc: the application ladder (built by this | |
| # leg only) fetches the Lightweight ORM, whose CMake runs | |
| # `pkg_check_modules(ODBC REQUIRED odbc)`, and whose ladder fixtures | |
| # open a real `DRIVER=SQLite3` connection at test time. | |
| # Named explicitly rather than relied on from the runner image. | |
| # libyaml-cpp-dev + libzip-dev: Lightweight's own CMakeLists.txt | |
| # does `find_package(yaml-cpp)`/`find_package(libzip)` as system | |
| # CONFIG packages, not through CPM (examples/bank/CMakeLists.txt's | |
| # comment on the identical fetch) — without these, configure fails | |
| # the moment this leg's MORPH_BUILD_LADDER=ON pulls Lightweight in. | |
| # Dropped from this step by mistake when it was renamed from | |
| # "Install Qt6 WebSockets" to "Install ODBC + SQLite driver" — | |
| # every other job that builds the ladder on Linux (Application | |
| # ladder, all optional features) already carries this pair. | |
| sudo apt-get install -y libgl1-mesa-dev unixodbc-dev libsqliteodbc libyaml-cpp-dev libzip-dev | |
| # Not the distro's Qt: examples/common/CMakeLists.txt requires 6.5+ | |
| # unconditionally (not gated on MORPH_BUILD_FORMS_QML) and Ubuntu | |
| # 24.04 still ships 6.4.2 -- the identical gap the "all optional | |
| # features" and "Application ladder" jobs' own install-qt-action steps | |
| # already document. Named qt6-base-dev/qt6-websockets-dev/qt6-tools-dev | |
| # used to be installed above; replaced wholesale rather than kept | |
| # alongside aqtinstall's Qt, which would leave two Qt6 installs on the | |
| # same runner for find_package() to pick between. | |
| - name: Install Qt ${{ env.QT_VERSION }} (coverage leg only) | |
| if: matrix.preset == 'clang-coverage' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Keyed by preset alone, matching linux-compilers' cache above: this | |
| # matrix's clang-asan/clang-tsan legs share the same preset name (and | |
| # much of the same object set) as ladder-sanitizers and kanban-tsan | |
| # respectively, which build the identical compiler+preset with extra | |
| # -DMORPH_BUILD_* flags on top. See linux-compilers' identical comment | |
| # for why sharing the key across jobs is safe. | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| restore-keys: sccache-${{ matrix.preset }}- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| # morph::net and the SQLite offline queue are opt-in, but they are also | |
| # where the memory/threading/UB risk actually lives (raw sockets, an I/O | |
| # thread, a hand-rolled frame reader, a C API). Left off, the sanitizers | |
| # and the coverage number both silently skipped them. QML and the | |
| # fuzzers stay out of this matrix entirely — they are covered by the | |
| # linux-all-features job, and a GUI stack under TSan is mostly noise; | |
| # the ladder (Qt6::WebSockets, no QML) is the one exception, built only | |
| # on the coverage leg, for the reason in the Qt install step above. | |
| - name: Configure | |
| run: | | |
| EXTRA_ARGS=() | |
| if [ "${{ matrix.preset }}" = "clang-coverage" ]; then | |
| EXTRA_ARGS+=(-DMORPH_BUILD_QT=ON -DMORPH_BUILD_LADDER=ON -DMORPH_LADDER_RUNGS=all) | |
| fi | |
| cmake --preset ${{ matrix.preset }} \ | |
| -DMORPH_BUILD_NET=ON \ | |
| -DMORPH_BUILD_OFFLINE_SQLITE=ON \ | |
| -DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache \ | |
| "${EXTRA_ARGS[@]}" | |
| # QT_QPA_PLATFORM=offscreen here too, not just on Test below: Catch2's | |
| # catch_discover_tests() runs each Qt-linked test binary once at BUILD | |
| # time to enumerate its cases, which can abort on this headless runner | |
| # without it — see "Linux / all optional features"'s own Build step | |
| # for the identical failure this leg's coverage build hit once the | |
| # ladder actually started compiling (this leg has no QML, a narrower | |
| # Qt surface, but ladder_common_tests still links Qt6::WebSockets). | |
| # Harmless for the non-Qt legs (nothing reads it). | |
| - name: Build | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: cmake --build --preset ${{ matrix.preset }} | |
| - name: Test | |
| env: | |
| # Harmless for the non-Qt legs (nothing reads it); required for the | |
| # coverage leg's ladder tests, which open real Qt widgets/sockets | |
| # on a runner with no display. | |
| QT_QPA_PLATFORM: offscreen | |
| run: | | |
| # morph::testkit::OomInjector (tests/oom_injector.cpp) overrides | |
| # the process-wide operator new/delete to force std::bad_alloc on | |
| # demand -- ASan and TSan's own runtimes already interpose | |
| # operator new/delete themselves, and linking a second, competing | |
| # definition fails with "multiple definition of `operator | |
| # new(unsigned long)'" (confirmed in CI). Excluded by test name on | |
| # exactly these two legs; every other CI leg (plain clang/gcc, | |
| # Windows, ubsan, coverage) runs these tests normally. | |
| EXCLUDE_ARGS=() | |
| if [ "${{ matrix.preset }}" = "clang-asan" ] || [ "${{ matrix.preset }}" = "clang-tsan" ]; then | |
| EXCLUDE_ARGS+=(-E "OomInjector|morph#108") | |
| fi | |
| if [ "${{ matrix.preset }}" = "clang-coverage" ]; then | |
| LLVM_PROFILE_FILE="build/clang-coverage/%p.profraw" ctest --preset clang-coverage "${EXCLUDE_ARGS[@]}" | |
| else | |
| ctest --preset ${{ matrix.preset }} "${EXCLUDE_ARGS[@]}" | |
| fi | |
| - name: Generate coverage report | |
| if: matrix.preset == 'clang-coverage' | |
| run: bash scripts/coverage.sh | |
| - name: Upload coverage to Codecov | |
| if: matrix.preset == 'clang-coverage' | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: build/clang-coverage/coverage.lcov | |
| # Without this, codecov-action's own auto-discovery finds every | |
| # other *.lcov/coverage.* file under the build tree too (its own | |
| # log says so: "Found 5 coverage files to report") -- including | |
| # coverage.lcov.raw, the *pre-aggregation* file | |
| # aggregate_lcov_branches.py's morph#93/#92 fixes rewrite away | |
| # from, plus unrelated fetched-dependency fixtures | |
| # (_deps/nlohmann_json-src/.../coverage.test). Codecov then merges | |
| # all of them, silently re-introducing every record `coverage.lcov` | |
| # was built to remove. `files:` alone does not disable that scan -- | |
| # this does. | |
| disable_search: true | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| - name: Upload coverage HTML | |
| if: matrix.preset == 'clang-coverage' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: build/clang-coverage/html/ | |
| # ── Linux: kanban's concurrent-move stress test under ThreadSanitizer ── | |
| # test_kanban_stress.cpp's [tsan]-tagged TEST_CASE drives BoardModel through | |
| # a bare morph::bridge::Bridge/morph::backend::LocalBackend directly, on a | |
| # real morph::exec::ThreadPoolExecutor{4}, with zero Qt frames anywhere in | |
| # its call graph (see the test file's own header comment) -- so the "a GUI | |
| # stack under TSan is mostly noise" rationale that keeps the ladder out of | |
| # linux-sanitizers does not apply to this one test. This job builds only | |
| # what that test needs -- MORPH_BUILD_LADDER=ON, MORPH_LADDER_RUNGS=kanban, | |
| # no Qt GUI modules beyond the WebSockets backend the ladder testkit itself | |
| # requires -- to keep it a minimal, fast, TSan-clean addition rather than | |
| # pulling every rung's Qt Quick/QML code into the sanitizer matrix. | |
| # | |
| # (History: an earlier version of this test drove the same scenario through | |
| # BackendRig{Mode::Local, ...}, whose Mode::Local unconditionally | |
| # constructs a real morph::qt::QtExecutor for client-facing callback | |
| # delivery -- morph#128 found 165 ThreadSanitizer warnings bottoming out in | |
| # genuine Qt-internal frames reached through it, undetectable as real bugs | |
| # or false positives from outside a TSan-instrumented Qt build. Rewriting | |
| # the test to never construct a QtExecutor at all sidesteps the ambiguity | |
| # entirely rather than resolving it.) | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| kanban-tsan: | |
| name: Kanban / ThreadSanitizer | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-kanban-tsan-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-kanban-tsan- | |
| - name: Install Clang ${{ env.CLANG_VERSION }} from apt.llvm.org | |
| run: | | |
| sudo apt-get update -q | |
| # unixodbc-dev + libsqliteodbc: the ladder (built by this job) | |
| # fetches the Lightweight ORM, whose CMake runs | |
| # `pkg_check_modules(ODBC REQUIRED odbc)`, and whose DbFixture | |
| # opens a real `DRIVER=SQLite3` connection at test time (identical | |
| # rationale to linux-sanitizers' coverage-leg step and ladder-tests' | |
| # own install step). Named explicitly rather than relied on from the | |
| # runner image. | |
| # libyaml-cpp-dev + libzip-dev: Lightweight's own CMakeLists.txt does | |
| # `find_package(yaml-cpp)`/`find_package(libzip)` as system CONFIG | |
| # packages, not through CPM (examples/bank/CMakeLists.txt's comment | |
| # on the identical fetch) — without these, configure fails the | |
| # moment MORPH_BUILD_LADDER=ON pulls Lightweight in. | |
| # libgl1-mesa-dev: every other job that configures MORPH_BUILD_QT=ON | |
| # together with MORPH_BUILD_LADDER=ON installs this (linux-sanitizers' | |
| # coverage leg, ladder-tests, linux-all-features) for Qt's GL platform | |
| # integration; carried here for the same reason even though this leg's | |
| # test run itself stays off-GUI. | |
| sudo apt-get install -y ninja-build catch2 libsqlite3-dev \ | |
| unixodbc-dev libsqliteodbc libyaml-cpp-dev libzip-dev libgl1-mesa-dev | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| # Not the distro's Qt: examples/common/CMakeLists.txt requires 6.5+ | |
| # unconditionally and Ubuntu 24.04 still ships 6.4.2 — the same gap | |
| # every other job that builds the ladder on Linux already documents. | |
| - name: Install Qt ${{ env.QT_VERSION }} | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Keyed by preset alone (clang-tsan), matching linux-sanitizers' cache: | |
| # this job's clang-tsan+Qt+kanban build shares the same compiler and | |
| # much of the same core-library object set as that job's clang-tsan | |
| # leg, which builds plain clang-tsan+net+offline_sqlite. See | |
| # linux-compilers' identical comment for why sharing the key is safe. | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-clang-tsan-${{ github.sha }} | |
| restore-keys: sccache-clang-tsan- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| # MORPH_LADDER_RUNGS=kanban (a single rung, not "all"): examples/CMakeLists.txt's | |
| # rung-selection loop (`if(MORPH_LADDER_RUNGS STREQUAL "all" OR _rung IN_LIST | |
| # MORPH_LADDER_RUNGS)`) matches a single-value list correctly, and examples/common | |
| # (the testkit every rung's tests link) is always added regardless of which rungs | |
| # are selected — so this configures and builds only kanban's ladder targets, not | |
| # the whole ladder. | |
| - name: Configure (clang-tsan, kanban only) | |
| run: | | |
| cmake --preset clang-tsan \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DMORPH_BUILD_LADDER=ON \ | |
| -DMORPH_LADDER_RUNGS=kanban \ | |
| -DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| # QT_QPA_PLATFORM=offscreen here too, not just on Test below: Catch2's | |
| # catch_discover_tests() runs ladder_kanban_tests once at BUILD time to | |
| # enumerate its cases, which can abort on this headless runner (no X | |
| # server) without it — see linux-sanitizers' and ladder-tests' own Build | |
| # steps for the identical note. | |
| - name: Build | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: cmake --build --preset clang-tsan | |
| # Every ladder ctest case only ever carries the "ladder"/"ladder-<rung>" | |
| # labels morph_add_rung() applies (cmake/morph_add_rung.cmake) — Catch2's | |
| # own tags ([kanban][stress][tsan]) are never translated into ctest | |
| # labels anywhere in this repo's CMake (no catch_discover_tests call | |
| # passes ADD_TAGS_AS_LABELS). A "-L tsan" filter would therefore match | |
| # zero tests and silently run nothing. This test's name is the only | |
| # thing distinguishing it, and "ThreadSanitizer" appears in exactly one | |
| # TEST_CASE name across the whole kanban tree (confirmed by grep), so | |
| # -R selects it precisely. | |
| - name: Test (kanban's TSan-tagged stress test only) | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --preset clang-tsan -L ladder-kanban -R ThreadSanitizer --output-on-failure | |
| # ── Linux: Qt WebSocket backend build + tests ───────────────────────── | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-clang-tsan-${{ github.sha }} | |
| linux-qt: | |
| name: Linux / Qt6 WebSockets | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-qt-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-qt- | |
| - name: Install GCC 15, ninja, catch2, Qt6 WebSockets | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 \ | |
| qt6-base-dev qt6-websockets-dev qt6-tools-dev libgl1-mesa-dev | |
| sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 | |
| sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 | |
| # Keyed by preset alone (gcc-debug), matching linux-compilers' cache: | |
| # this job's gcc-debug+Qt build shares the same core-library object set | |
| # as linux-compilers' plain gcc-debug leg, ladder-tests' gcc-debug+Qt+ | |
| # ladder build, and valgrind's gcc-debug+net build. See linux-compilers' | |
| # identical comment for why sharing the key across jobs is safe. | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| restore-keys: sccache-gcc-debug- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| - name: Configure (gcc-debug with MORPH_BUILD_QT=ON) | |
| run: | | |
| cmake --preset gcc-debug \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| - name: Build | |
| run: cmake --build --preset gcc-debug | |
| - name: Test (offscreen Qt platform) | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --preset gcc-debug | |
| # ── Linux: application ladder testkit (path-filtered) ───────────────── | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| ladder-tests: | |
| name: Application ladder | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need history for the changed-paths diff below | |
| - name: Determine whether the ladder needs to run | |
| id: filter | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| base="${{ github.event.pull_request.base.sha }}" | |
| else | |
| base="${{ github.event.before }}" | |
| fi | |
| if [ -z "$base" ] || ! git cat-file -e "$base" 2>/dev/null; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| changed=$(git diff --name-only "$base" HEAD) | |
| # The pattern is generated from examples/rungs.txt, never written | |
| # here. It used to be a hand-copied rung alternation, and it had | |
| # drifted a full rung behind: it stopped at kanban, so a change | |
| # confined to examples/ledger/ (rung 5) or examples/lims/ (rung 6) | |
| # matched nothing and skipped this job -- silently, because a filter | |
| # that matches nothing looks exactly like one that correctly decided | |
| # there was nothing to do (morph#179). See scripts/ladder_rungs.sh | |
| # for what the non-rung half of the pattern covers and why. | |
| pattern="$(bash scripts/ladder_rungs.sh ci-path-regex)" | |
| echo "ladder path filter: $pattern" | |
| if echo "$changed" | grep -qE "$pattern"; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Cache apt packages | |
| if: steps.filter.outputs.run == 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-qt-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-qt- | |
| - name: Install GCC 15, ninja, catch2 | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| # unixodbc-dev + libsqliteodbc: the application ladder fetches the | |
| # Lightweight ORM, whose CMake runs | |
| # `pkg_check_modules(ODBC REQUIRED odbc)`, and whose ladder fixtures | |
| # open a real `DRIVER=SQLite3` connection at test time. | |
| # Named explicitly rather than relied on from the runner image. | |
| # libyaml-cpp-dev + libzip-dev: Lightweight's own CMakeLists.txt | |
| # does `find_package(yaml-cpp)`/`find_package(libzip)` as system | |
| # CONFIG packages, not through CPM (examples/bank/CMakeLists.txt's | |
| # comment on the identical fetch) — without these, configure fails | |
| # the moment MORPH_BUILD_LADDER=ON pulls Lightweight in. | |
| # Qt itself is installed by the aqtinstall step below, not apt: see | |
| # that step's comment for why the distro package is unusable here. | |
| sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 \ | |
| libyaml-cpp-dev libzip-dev libgl1-mesa-dev \ | |
| unixodbc-dev libsqliteodbc | |
| sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 | |
| sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 | |
| # Not the distro's Qt: examples/common/CMakeLists.txt requires 6.5+ | |
| # (QQmlApplicationEngine::loadFromModule, used by MORPH_BUILD_FORMS_QML | |
| # rungs) and Ubuntu 24.04 still ships 6.4.2 — the exact gap the "all | |
| # optional features" job's identical step already documents. This job | |
| # configures MORPH_BUILD_LADDER=ON without MORPH_BUILD_FORMS_QML, but | |
| # examples/common/CMakeLists.txt's Qt6 6.5 REQUIRED applies unconditionally | |
| # (it is not gated on MORPH_BUILD_FORMS_QML), so the floor still bites here. | |
| - name: Install Qt ${{ env.QT_VERSION }} | |
| if: steps.filter.outputs.run == 'true' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Keyed by preset alone (gcc-debug), matching linux-qt's cache above: | |
| # this job's gcc-debug+Qt+ladder build shares the same core-library | |
| # objects as linux-compilers' plain gcc-debug leg and linux-qt's | |
| # gcc-debug+Qt build. | |
| - name: Restore sccache | |
| if: steps.filter.outputs.run == 'true' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| restore-keys: sccache-gcc-debug- | |
| - name: Install sccache | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| - name: Configure (gcc-debug, ladder + Qt on) | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| cmake --preset gcc-debug \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DMORPH_BUILD_LADDER=ON \ | |
| -DMORPH_LADDER_RUNGS=all \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| # QT_QPA_PLATFORM=offscreen here too, not just on Test below: Catch2's | |
| # catch_discover_tests() runs each Qt-linked test binary once at BUILD | |
| # time to enumerate its cases, which aborts on a headless runner (no X | |
| # server) without it — see "Linux / all optional features"'s own Build | |
| # step for the identical note. This job has not hit it in practice | |
| # (its ladder test binaries' discovery apparently succeeds without a | |
| # platform anyway), but the risk is structurally identical, so it is | |
| # set defensively rather than left to reappear the next time a rung | |
| # adds a Qt Quick-linked test binary here. | |
| - name: Build | |
| if: steps.filter.outputs.run == 'true' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: cmake --build --preset gcc-debug | |
| - name: Test (offscreen Qt platform, ladder tests only, stress excluded) | |
| if: steps.filter.outputs.run == 'true' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --preset gcc-debug -L ladder -LE stress --output-on-failure | |
| # ── Linux: every rung's tests under AddressSanitizer + UBSan ────────── | |
| # Before this job, no rung test ran under any sanitizer: ladder-tests above | |
| # builds plain gcc-debug, and the linux-sanitizers matrix deliberately skips | |
| # the ladder (only its clang-coverage leg sets MORPH_BUILD_LADDER). So every | |
| # rung's models, presenters and QML adapters — the code the ladder exists to | |
| # exercise — were compiled and run with no memory or UB instrumentation | |
| # anywhere in CI. | |
| # | |
| # ASan and UBSan, not TSan. `apply_sanitizers(<target> asan)` compiles with | |
| # -fsanitize=address,undefined (cmake/compiler_options.cmake), so this one | |
| # preset delivers both and a separate ubsan leg for the ladder would re-run | |
| # a strict subset. TSan is deliberately absent: a rung's tests drive Qt on | |
| # every path, and against an uninstrumented system Qt that produces warnings | |
| # bottoming out in Qt-internal frames which cannot be classified as real | |
| # races or false positives from outside a TSan-instrumented Qt build — | |
| # morph#128 hit exactly that, 165 warnings deep. The resolution there was to | |
| # rewrite the one test that mattered to construct no QtExecutor at all and | |
| # run only it under TSan, which is what kanban-tsan above does. Thread- | |
| # sanitising a rung means following that pattern per test, not adding a | |
| # blanket -DAF_SANITIZER=tsan leg here. | |
| # | |
| # -LE stress mirrors ladder-tests: the stress cases are long-running by | |
| # design and ASan's shadow-memory overhead compounds that. | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() && steps.filter.outputs.run == 'true' | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: steps.filter.outputs.run == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| ladder-sanitizers: | |
| name: Application ladder / ASan+UBSan | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need history for the changed-paths diff below | |
| # Same filter as ladder-tests, and now literally the same generator | |
| # rather than a second hand-copy of the same list: the two jobs build the | |
| # identical tree and differ only in instrumentation, so a change that | |
| # warrants running one warrants running the other, and the two patterns | |
| # must not be able to diverge. This job is the only one in the repository | |
| # that sanitizer-instruments a rung, which is what the drifted filter | |
| # actually cost (morph#179). | |
| - name: Determine whether the ladder needs to run | |
| id: filter | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| base="${{ github.event.pull_request.base.sha }}" | |
| else | |
| base="${{ github.event.before }}" | |
| fi | |
| if [ -z "$base" ] || ! git cat-file -e "$base" 2>/dev/null; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| changed=$(git diff --name-only "$base" HEAD) | |
| pattern="$(bash scripts/ladder_rungs.sh ci-path-regex)" | |
| echo "ladder path filter: $pattern" | |
| if echo "$changed" | grep -qE "$pattern"; then | |
| echo "run=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "run=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Cache apt packages | |
| if: steps.filter.outputs.run == 'true' | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-ladder-asan-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-ladder-asan- | |
| - name: Install Clang ${{ env.CLANG_VERSION }}, ninja, catch2, ODBC | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| sudo apt-get update -q | |
| # unixodbc-dev + libsqliteodbc: the ladder fetches the Lightweight | |
| # ORM, whose CMake runs `pkg_check_modules(ODBC REQUIRED odbc)` and | |
| # whose fixtures open a real `DRIVER=SQLite3` connection at test | |
| # time. libyaml-cpp-dev/libzip-dev/libgl1-mesa-dev round out the | |
| # same set every other ladder-building Linux job installs. | |
| sudo apt-get install -y ninja-build catch2 libsqlite3-dev \ | |
| unixodbc-dev libsqliteodbc libyaml-cpp-dev libzip-dev libgl1-mesa-dev | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| # Not the distro's Qt: examples/common/CMakeLists.txt requires 6.5+ | |
| # unconditionally and Ubuntu 24.04 still ships 6.4.2 — the same gap | |
| # every other job that builds the ladder on Linux already documents. | |
| - name: Install Qt ${{ env.QT_VERSION }} | |
| if: steps.filter.outputs.run == 'true' | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Keyed by preset alone (clang-asan), matching linux-sanitizers' cache: | |
| # this job's clang-asan+Qt+ladder build shares the same compiler and | |
| # much of the same core-library object set as that job's clang-asan leg. | |
| - name: Restore sccache | |
| if: steps.filter.outputs.run == 'true' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-clang-asan-${{ github.sha }} | |
| restore-keys: sccache-clang-asan- | |
| - name: Install sccache | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| - name: Configure (clang-asan, ladder + Qt on) | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| cmake --preset clang-asan \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DMORPH_BUILD_LADDER=ON \ | |
| -DMORPH_LADDER_RUNGS=all \ | |
| -DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| # QT_QPA_PLATFORM=offscreen at build time as well as test time: Catch2's | |
| # catch_discover_tests() runs each Qt-linked test binary once during the | |
| # build to enumerate its cases — see ladder-tests' own Build step. | |
| - name: Build | |
| if: steps.filter.outputs.run == 'true' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: cmake --build --preset clang-asan | |
| # A sanitizer job whose binaries are not actually instrumented is worse | |
| # than no job: it runs the full suite, costs the full runtime and always | |
| # passes, so a missing apply_sanitizers() call in morph_add_rung.cmake | |
| # would read as "rungs are ASan-clean" rather than "rungs were never | |
| # checked". That is not hypothetical — before the AF_SANITIZER blocks | |
| # landed in cmake/morph_add_rung.cmake and examples/common/CMakeLists.txt, | |
| # -DAF_SANITIZER=asan instrumented morph_tests and nothing under | |
| # examples/. Assert the instrumentation is really there before trusting | |
| # a green run. | |
| - name: Verify rung binaries are actually instrumented | |
| if: steps.filter.outputs.run == 'true' | |
| run: | | |
| set -euo pipefail | |
| missing=0 | |
| for exe in build/clang-asan/examples/*/ladder_*_tests; do | |
| [ -x "$exe" ] || continue | |
| if nm -C "$exe" 2>/dev/null | grep -q "__asan_"; then | |
| echo "instrumented: $exe" | |
| else | |
| echo "NOT INSTRUMENTED: $exe" >&2 | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" -ne 0 ]; then | |
| echo "::error::ladder test binaries were built without ASan instrumentation" >&2 | |
| exit 1 | |
| fi | |
| # detect_leaks=0: LeakSanitizer reports allocations the Qt platform | |
| # plugins and QML engine intentionally keep for process lifetime, which | |
| # are not leaks this repo can fix or meaningfully suppress per-frame. | |
| # The memory-error and UB checks — what this job is actually for — stay | |
| # fully on. Drop this once a Qt-aware suppression file proves workable. | |
| - name: Test (offscreen Qt platform, ladder tests only, stress excluded) | |
| if: steps.filter.outputs.run == 'true' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| ASAN_OPTIONS: detect_leaks=0 | |
| UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 | |
| run: ctest --preset clang-asan -L ladder -LE stress --output-on-failure | |
| # ── Linux: every optional feature enabled at once ───────────────────── | |
| # Every MORPH_BUILD_* option below is off by default, and until this job | |
| # existed no CI configuration turned any of them on — so several thousand | |
| # lines (morph::net, the Qt/QML forms renderer, the SQLite queue, the fuzz | |
| # harnesses, the vetted-HMAC adapters, the soak/bench targets) were never | |
| # compiled here, let alone tested. That is how a build that cannot configure | |
| # (MORPH_REQUIRE_VETTED_HMAC) and a replay test matching the wrong file | |
| # extension both reached master green. Enabling them together also proves | |
| # they compose, which building each alone would not. | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() && steps.filter.outputs.run == 'true' | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: steps.filter.outputs.run == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-clang-asan-${{ github.sha }} | |
| linux-all-features: | |
| name: Linux / all optional features (${{ matrix.compiler }}) | |
| runs-on: ubuntu-24.04 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Clang additionally builds the libFuzzer harnesses, which require it. | |
| - compiler: clang | |
| preset: clang-debug | |
| fuzzers: 'ON' | |
| - compiler: gcc | |
| preset: gcc-debug | |
| fuzzers: 'OFF' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-all-features-${{ matrix.compiler }}-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-all-features-${{ matrix.compiler }}- | |
| - name: Install toolchain and the non-Qt dependencies | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| # unixodbc-dev + libsqliteodbc: the application ladder (enabled in | |
| # the configure step below) fetches the Lightweight ORM, whose CMake | |
| # runs `pkg_check_modules(ODBC REQUIRED odbc)`, and whose ladder | |
| # fixtures open a real `DRIVER=SQLite3` connection at test time. | |
| # Named explicitly rather than relied on from the runner image. | |
| # libyaml-cpp-dev + libzip-dev: Lightweight's own CMakeLists.txt | |
| # (examples/bank/CMakeLists.txt's comment on the same fetch) does | |
| # `find_package(yaml-cpp)`/`find_package(libzip)` as system CONFIG | |
| # packages, not through CPM — without these, Lightweight's configure | |
| # fails with "could not find a package configuration file" the | |
| # moment MORPH_BUILD_LADDER=ON pulls it in here. | |
| sudo apt-get install -y ninja-build catch2 \ | |
| libsqlite3-dev libsodium-dev libssl-dev \ | |
| unixodbc-dev libsqliteodbc \ | |
| libyaml-cpp-dev libzip-dev \ | |
| libgl1-mesa-dev libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 \ | |
| libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 | |
| if [ "${{ matrix.compiler }}" = "gcc" ]; then | |
| sudo apt-get install -y gcc-15 g++-15 | |
| sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 | |
| sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 | |
| else | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| fi | |
| # Not the distro's Qt: MORPH_BUILD_FORMS_QML needs 6.5+ (see the version | |
| # floor in CMakeLists.txt) and Ubuntu 24.04 still ships 6.4.2, whose | |
| # QQmlApplicationEngine has no loadFromModule. | |
| - name: Install Qt ${{ env.QT_VERSION }} | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Keyed by preset alone, matching linux-compilers' cache: this matrix's | |
| # gcc-debug/clang-debug legs (with every optional feature layered on | |
| # top) share their core-library objects with linux-compilers' plain | |
| # gcc-debug/clang-debug legs and every other job building the same | |
| # preset (linux-qt, ladder-tests, valgrind for gcc-debug). | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| restore-keys: sccache-${{ matrix.preset }}- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| # MORPH_REQUIRE_VETTED_HMAC is deliberately combined with | |
| # MORPH_BUILD_TESTS=ON (the preset default). docs/spec/security.md | |
| # documents that pairing as supported; this is what keeps it that way. | |
| - name: Configure (every optional feature ON) | |
| run: | | |
| EXTRA="" | |
| if [ "${{ matrix.compiler }}" = "clang" ]; then | |
| EXTRA="-DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }}" | |
| fi | |
| # shellcheck disable=SC2086 | |
| # MORPH_BUILD_LADDER belongs in this job by its own charter ("every | |
| # MORPH_BUILD_* option … enabling them together also proves they | |
| # compose") and closes a real hole: until it was added here, *no* CI | |
| # leg configured MORPH_BUILD_LADDER=ON together with | |
| # MORPH_BUILD_FORMS_QML=ON. The ladder-tests job below cannot — its | |
| # distro Qt is 6.4.2, under the 6.5 floor MORPH_BUILD_FORMS_QML | |
| # requires — so each rung's QML module, desktop client and offscreen | |
| # engine-load smoke test were built by nothing at all. This job has | |
| # Qt ${{ env.QT_VERSION }} from aqtinstall, so here they are built, | |
| # and the smoke test runs, on every push. | |
| cmake --preset ${{ matrix.preset }} \ | |
| -DMORPH_BUILD_NET=ON \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DMORPH_BUILD_FORMS_QML=ON \ | |
| -DMORPH_BUILD_LADDER=ON \ | |
| -DMORPH_LADDER_RUNGS=all \ | |
| -DMORPH_BUILD_OFFLINE_SQLITE=ON \ | |
| -DMORPH_BUILD_LOAD_TESTS=ON \ | |
| -DMORPH_BUILD_HMAC_EXAMPLES=ON \ | |
| -DMORPH_BUILD_FUZZERS=${{ matrix.fuzzers }} \ | |
| -DMORPH_REQUIRE_VETTED_HMAC=ON \ | |
| $EXTRA \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| # QT_QPA_PLATFORM=offscreen here too, not just on Test below: Catch2's | |
| # catch_discover_tests() runs each Qt-linked test binary once at BUILD | |
| # time to enumerate its cases (CatchAddTests.cmake), not only when | |
| # ctest later executes them — a ladder_<rung>_tests binary aborts at | |
| # that discovery step on this headless runner (no X server, xcb | |
| # platform plugin fails to load) without it, before any real test ever | |
| # runs. Only bites once MORPH_BUILD_LADDER=ON actually reaches a rung's | |
| # own Qt-linked test binary, which is why this job's build only started | |
| # failing here after the yaml-cpp/libzip configure gap (fixed earlier | |
| # this branch) stopped masking it. | |
| - name: Build | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: cmake --build --preset ${{ matrix.preset }} | |
| # Includes the fuzz *replay* tests on the clang leg: each committed seed | |
| # and crash reproducer is replayed once and must not crash. This is a | |
| # deterministic regression check, not a fuzzing campaign. | |
| - name: Test (offscreen Qt platform) | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --preset ${{ matrix.preset }} --output-on-failure | |
| # A guard that never fires is indistinguishable from one that works, so | |
| # assert the fuzz replay actually ran the committed crash reproducers | |
| # rather than silently matching nothing (the `.txt`-vs-`.bin` glob bug). | |
| - name: Verify the fuzz replay covered the committed reproducers | |
| if: matrix.fuzzers == 'ON' | |
| run: | | |
| ctest --preset ${{ matrix.preset }} -R fuzz -V > fuzz-replay.log 2>&1 | |
| status=0 | |
| for finding in tests/fuzz/findings/*/*; do | |
| if ! grep -qF "$(basename "$finding")" fuzz-replay.log; then | |
| echo "::error::fuzz replay never referenced committed reproducer $finding" | |
| status=1 | |
| fi | |
| done | |
| exit "$status" | |
| # ── Valgrind (memcheck) ─────────────────────────────────────────────── | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-${{ matrix.preset }}-${{ github.sha }} | |
| valgrind: | |
| name: Valgrind memcheck | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-valgrind-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-valgrind- | |
| - name: Install GCC 15, ninja, catch2, valgrind | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| sudo apt-get install -y gcc-15 g++-15 ninja-build catch2 valgrind | |
| sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-15 15 | |
| sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-15 15 | |
| # Keyed by preset alone (gcc-debug), matching linux-compilers' cache: | |
| # this job's gcc-debug+net+offline_sqlite build shares its | |
| # core-library objects with every other job building plain gcc-debug. | |
| - name: Restore sccache | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| restore-keys: sccache-gcc-debug- | |
| - name: Install sccache | |
| run: | | |
| curl -sSL https://github.com/mozilla/sccache/releases/download/v0.9.1/sccache-v0.9.1-x86_64-unknown-linux-musl.tar.gz \ | |
| | tar -xz --strip-components=1 -C /usr/local/bin sccache-v0.9.1-x86_64-unknown-linux-musl/sccache | |
| # morph::net and the SQLite queue are where the raw pointers, manual | |
| # buffers and C API calls live — precisely what memcheck is for — so they | |
| # are built here rather than left to the default (Qt/QML stays out: it | |
| # drags in a GUI stack whose own allocations dominate the report). | |
| - name: Configure | |
| run: | | |
| cmake --preset gcc-debug \ | |
| -DMORPH_BUILD_NET=ON \ | |
| -DMORPH_BUILD_OFFLINE_SQLITE=ON \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=sccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=sccache | |
| - name: Build | |
| run: cmake --build --preset gcc-debug | |
| - name: Run tests under Valgrind | |
| run: | | |
| status=0 | |
| for suite in tests/morph_tests tests/net/morph_net_tests tests/offline_sqlite/morph_offline_sqlite_tests; do | |
| binary="build/gcc-debug/$suite" | |
| if [ ! -x "$binary" ]; then | |
| echo "::error::expected Valgrind suite $binary was not built" | |
| exit 1 | |
| fi | |
| echo "── memcheck: $suite ──" | |
| # [oom-injector]/[issue108]: morph::testkit::OomInjector | |
| # overrides the process-wide operator new/delete | |
| # (tests/oom_injector.cpp) to force std::bad_alloc on demand -- | |
| # Valgrind's own memcheck instrumentation intercepts allocations | |
| # at a layer this override does not reach, so the injector | |
| # silently never fires under Valgrind (confirmed: even its own | |
| # self-tests fail here). Skipped for this leg only; every other | |
| # CI leg (plain clang/gcc, Windows) runs these tests normally. | |
| EXTRA_ARGS=() | |
| if [ "$suite" = "tests/morph_tests" ]; then | |
| EXTRA_ARGS=("~[oom-injector]" "~[issue108]") | |
| fi | |
| valgrind \ | |
| --tool=memcheck \ | |
| --leak-check=full \ | |
| --show-leak-kinds=definite,indirect \ | |
| --errors-for-leak-kinds=definite,indirect \ | |
| --error-exitcode=1 \ | |
| --suppressions=cmake/valgrind.supp \ | |
| "$binary" "${EXTRA_ARGS[@]}" || status=1 | |
| done | |
| exit "$status" | |
| # Cumulative hit/miss for this leg. Without it the cache is | |
| # unfalsifiable: a thrashing cache and a working one look identical | |
| # from the outside, and only the build-step duration hints at which | |
| # one you have. `|| true` so a stats failure never fails the job. | |
| - name: sccache stats | |
| if: always() | |
| run: sccache --show-stats || true | |
| # Save only on a push to master. Every run used to write a new | |
| # per-commit entry, and with 16 cache-writing legs at 1 GB each one | |
| # generation already exceeds GitHub's 10 GB per-repo budget -- so | |
| # legs evicted each other and entries rarely survived to be reused. | |
| # Restricting writes to master keeps morph#109's property (only one | |
| # branch ever writes, so no branch can delete another's entry) while | |
| # collapsing the generations that actually blow the budget. Pull | |
| # requests still restore, read-only, and get the full benefit. | |
| - name: Save sccache | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /home/runner/.cache/sccache | |
| key: sccache-gcc-debug-${{ github.sha }} | |
| # ── clang-format (whole tree) ───────────────────────────────────────── | |
| # | |
| # .clang-format has always been in the tree, and nothing enforced it: 471 of | |
| # 677 tracked .hpp/.cpp files did not match it (morph#210). An unenforced | |
| # config is worse than none, because every editor with format-on-save obeys it | |
| # faithfully -- so touching one file rewrites parts the author never meant to | |
| # change, and the reviewer's job becomes separating intent from churn. | |
| # | |
| # Whole tree, not changed lines. A changed-lines check passes as long as | |
| # nobody touches the non-conforming lines, so it would have reported success | |
| # over a tree that was 70% non-conforming -- a control that measures almost | |
| # nothing. The tree was reformatted wholesale first (see | |
| # .git-blame-ignore-revs), which is what makes the wider check possible. | |
| # | |
| # Pinned to the same CLANG_VERSION as clang-tidy-diff: clang-format output is | |
| # not stable across major versions, so an unpinned check would disagree with | |
| # itself between runners. | |
| clang-format: | |
| name: clang-format | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install clang-format ${{ env.CLANG_VERSION }} | |
| run: | | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| sudo apt-get install -y clang-format-${{ env.CLANG_VERSION }} | |
| - name: Check every tracked C++ file against .clang-format | |
| run: | | |
| git ls-files -z '*.hpp' '*.cpp' > cpp-files.z | |
| COUNT=$(tr -cd '\0' < cpp-files.z | wc -c) | |
| # An empty or truncated file list would make this job pass while | |
| # checking nothing, which is the exact failure it exists to prevent. | |
| # Refuse rather than report a vacuous success. | |
| if [ "$COUNT" -lt 100 ]; then | |
| echo "::error::only $COUNT C++ files matched -- the check would be vacuous" | |
| exit 1 | |
| fi | |
| echo "Checking $COUNT files" | |
| xargs -0 clang-format-${{ env.CLANG_VERSION }} --dry-run -Werror \ | |
| < cpp-files.z 2> clang-format-report.txt || true | |
| if [ -s clang-format-report.txt ]; then | |
| cat clang-format-report.txt | |
| echo "::error::tree does not match .clang-format -- run: git ls-files -z '*.hpp' '*.cpp' | xargs -0 clang-format -i" | |
| exit 1 | |
| fi | |
| - name: Upload clang-format report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: clang-format-report | |
| path: clang-format-report.txt | |
| if-no-files-found: ignore | |
| # ── clang-tidy-diff (changed lines only) ────────────────────────────── | |
| clang-tidy: | |
| name: clang-tidy-diff | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # need full history for git diff against base | |
| - name: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: /var/cache/apt/archives | |
| key: apt-clang-tidy-${{ hashFiles('.github/workflows/ci.yml') }} | |
| restore-keys: apt-clang-tidy- | |
| - name: Install Clang ${{ env.CLANG_VERSION }}, clang-tidy, and the non-Qt deps | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y ninja-build catch2 \ | |
| libsqlite3-dev libsodium-dev libssl-dev \ | |
| libgl1-mesa-dev libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 \ | |
| libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0 | |
| wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- ${{ env.CLANG_VERSION }} | |
| sudo apt-get install -y clang-tidy-${{ env.CLANG_VERSION }} | |
| # See the linux-all-features job: the QML renderer needs Qt 6.5+. | |
| - name: Install Qt ${{ env.QT_VERSION }} | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| version: ${{ env.QT_VERSION }} | |
| modules: qtwebsockets | |
| cache: true | |
| # Every optional feature is ON here purely so its sources land in | |
| # compile_commands.json. clang-tidy only ever analyses *changed lines*, so | |
| # this costs a wider build, not a wider lint. Configured narrowly before, | |
| # the whole opt-in surface — morph::net, the Qt transport, the QML forms | |
| # renderer, the SQLite queue, the fuzz harnesses — was invisible to the | |
| # one gate in this workflow that reads code rather than running it. | |
| # No Build step: clang-tidy-diff.py reads compile_commands.json (a | |
| # configure-time artifact, CMAKE_EXPORT_COMPILE_COMMANDS=ON in the base | |
| # preset) and runs clang-tidy itself per translation unit -- it neither | |
| # needs the project actually compiled nor linked. pinned_facts.cmake's | |
| # generated header is likewise a configure_file() (configure-time), not | |
| # a build-time add_custom_command, so it's already on disk too. Compiler | |
| # caching (sccache) accordingly has nothing to do in this job, unlike | |
| # every other Linux job here -- so those steps, and the Build step that | |
| # was their only reason to run, are gone rather than merely skipped. | |
| - name: Configure (generates compile_commands.json over every optional feature) | |
| run: | | |
| cmake --preset clang-debug \ | |
| -DMORPH_BUILD_NET=ON \ | |
| -DMORPH_BUILD_QT=ON \ | |
| -DMORPH_BUILD_FORMS_QML=ON \ | |
| -DMORPH_BUILD_OFFLINE_SQLITE=ON \ | |
| -DMORPH_BUILD_LOAD_TESTS=ON \ | |
| -DMORPH_BUILD_HMAC_EXAMPLES=ON \ | |
| -DMORPH_BUILD_FUZZERS=ON \ | |
| -DCMAKE_C_COMPILER=clang-${{ env.CLANG_VERSION }} \ | |
| -DCMAKE_CXX_COMPILER=clang++-${{ env.CLANG_VERSION }} | |
| - name: Run clang-tidy-diff on changed lines | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| else | |
| BASE_SHA="${{ github.event.before }}" | |
| fi | |
| CLANG_TIDY_DIFF="$(find /usr/lib/llvm-${{ env.CLANG_VERSION }}/share/clang /usr/share/clang \ | |
| -name 'clang-tidy-diff.py' 2>/dev/null | head -1)" | |
| if [ -z "$CLANG_TIDY_DIFF" ]; then | |
| echo "::error::clang-tidy-diff.py not found" | |
| exit 1 | |
| fi | |
| echo "Base SHA: $BASE_SHA" | |
| echo "Script: $CLANG_TIDY_DIFF" | |
| git diff -U0 "$BASE_SHA" | \ | |
| python3 "$CLANG_TIDY_DIFF" \ | |
| -path build/clang-debug \ | |
| -clang-tidy-binary clang-tidy-${{ env.CLANG_VERSION }} \ | |
| -p1 \ | |
| -j "$(nproc)" \ | |
| -extra-arg=-std=c++23 \ | |
| -extra-arg=-warnings-as-errors=* \ | |
| -quiet \ | |
| 2>&1 | tee clang-tidy-report.txt | |
| - name: Upload clang-tidy report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: clang-tidy-report | |
| path: clang-tidy-report.txt | |
| # ── Deprecation-marker format lint ──────────────────────────────────── | |
| deprecation-lint: | |
| name: Deprecation marker format | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # The checker itself is tested first. A lint gate nobody tests reports | |
| # green whether or not it still detects anything — this one was blind to | |
| # both the bare `[[deprecated]]` form and any message wrapped across | |
| # lines, which is exactly what VERSIONING.md forbids. | |
| - name: Self-test the deprecation-marker checker | |
| run: bash scripts/test_check_deprecated_markers.sh | |
| - name: Check [[deprecated]] markers cite a replacement and removal version | |
| run: bash scripts/check_deprecated_markers.sh include/morph | |
| # ── Test file-scope type name collision lint ─────────────────────────── | |
| test-type-name-lint: | |
| name: Test file-scope type name collisions | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # See scripts/test_check_test_type_names.sh's own comment: a lint gate | |
| # nobody tests reports green whether or not it still detects anything. | |
| - name: Self-test the test-type-name checker | |
| run: bash scripts/test_check_test_type_names.sh | |
| # Catches issue #84's bug class: two file-scope (external-linkage) | |
| # struct/class declarations with the same name in different test | |
| # files is an ODR violation that neither the compiler nor the linker | |
| # diagnoses, and which resolves link-order-dependently per build. | |
| - name: Check for file-scope struct/class name collisions across tests/ | |
| run: bash scripts/check_test_type_names.sh | |
| # ── Journal payload-fingerprint stamp lint ───────────────────────────── | |
| journal-stamp-lint: | |
| name: Hand-rolled LogEntry stamps its payload fingerprint | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # See scripts/test_check_journal_stamps.sh's own comment: a lint gate | |
| # nobody tests reports green whether or not it still detects anything. | |
| # This checker has two specific ways to go blind, and the self-test | |
| # pins both. | |
| - name: Self-test the journal-stamp checker | |
| run: bash scripts/test_check_journal_stamps.sh | |
| # Catches issue #244's bug class: a model that hand-rolls its own | |
| # journaling records entries with no `LogEntry::schema`, which | |
| # `journal::replay()` can only replay unverified -- so a later renamed | |
| # field decodes to its default and the reconstruction is confidently | |
| # wrong, which is precisely the failure the fingerprint exists to make | |
| # loud. See docs/spec/journal/journal.md, "Payload schema fingerprint". | |
| - name: Check hand-rolled LogEntry constructions stamp schema | |
| run: bash scripts/check_journal_stamps.sh examples | |
| # ── Install / export: find_package(morph CONFIG) must work ───────────── | |
| # | |
| # Its own job rather than a step on an existing leg. It configures, installs | |
| # and consumes morph three times over (once for the checker, twice more for | |
| # the self-test's slow cases) and would add several minutes to whichever | |
| # matrix leg it was bolted onto; here it runs in parallel with them and | |
| # costs nothing on the critical path. | |
| # | |
| # Nothing else in CI installs morph at all, which is how morph#232 survived: | |
| # every ladder rung builds from inside the tree, so `cmake --install` | |
| # exiting 0 with none of morph in the prefix was invisible to all ~16 legs. | |
| install-export: | |
| name: find_package(morph CONFIG) consumability | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # GCC 15 for the same reason the gcc-* presets use it: the default | |
| # logger needs a standard library with <print>, and ubuntu-24.04 ships | |
| # GCC 13. | |
| - name: Install GCC 15 and ninja | |
| run: | | |
| sudo apt-get update -q | |
| sudo apt-get install -y software-properties-common | |
| sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test | |
| sudo apt-get update -q | |
| sudo apt-get install -y gcc-15 g++-15 ninja-build | |
| # See scripts/test_check_install_export.sh's own comment: a gate nobody | |
| # tests reports green whether or not it still detects anything, and this | |
| # one guards a defect whose whole character was reporting success. | |
| - name: Self-test the install/export checker | |
| env: | |
| CC: gcc-15 | |
| CXX: g++-15 | |
| run: bash scripts/test_check_install_export.sh | |
| # The check itself: install to a scratch prefix, then configure, build | |
| # and run a consumer project outside the tree against morph::morph and | |
| # morph::net via find_package(morph CONFIG REQUIRED). | |
| - name: Install morph and build a consumer against the prefix | |
| env: | |
| CC: gcc-15 | |
| CXX: g++-15 | |
| run: bash scripts/check_install_export.sh |