ci: ARM cross-compile, heap-free & supply-chain safety gates #78
Workflow file for this run
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: Static & Dynamic Analysis | |
| on: | |
| push: | |
| branches: ["master"] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Docs-only gate. Branch protection requires the analysis jobs below BY NAME, | |
| # so they must always run and report a green context -- skipping a required | |
| # job (via `paths-ignore` or a job-level `if:`) leaves its context unreported | |
| # and blocks the PR forever. Instead this job classifies the diff and every | |
| # heavy step downstream is guarded on `needs.changes.outputs.code == 'true'`. | |
| # A docs-only PR thus completes each required job green in seconds with no | |
| # apt-get / build. Non-PR events (push to master, manual dispatch) always run | |
| # fully. | |
| changes: | |
| name: changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| code: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: filter | |
| name: Classify diff (docs-only vs code) | |
| run: | | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| echo "Event ${{ github.event_name }} -> full run" | |
| echo "code=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| base='${{ github.event.pull_request.base.sha }}' | |
| head='${{ github.event.pull_request.head.sha }}' | |
| files="$(git diff --name-only "$base" "$head")" | |
| echo "Changed files:" | |
| echo "$files" | |
| # Docs-only == every changed path is under docs/ or ends in .md. | |
| # Anything else (code, workflow YAML, plot.py, datasets) -> full run. | |
| # An empty diff also forces a full run, to be safe. | |
| nondoc="$(echo "$files" | grep -vE '^docs/|\.md$' || true)" | |
| if [ -z "$files" ] || [ -n "$nondoc" ]; then | |
| echo "Non-docs changes present -> full run" | |
| echo "code=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Docs-only change -> heavy steps skipped" | |
| echo "code=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| sanitize: | |
| name: ASan + UBSan | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install Boost | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y libboost-dev libboost-test-dev | |
| - name: Build and run all runtime suites + int8 examples under ASan/UBSan | |
| if: needs.changes.outputs.code == 'true' | |
| env: | |
| BOOST_HOME: /usr | |
| run: make sanitize | |
| sanitize-avx2: | |
| # Same ASan+UBSan bar but with the AVX2 SIMD backend compiled in and | |
| # executing -- the scalar sanitize job never runs the hand-written | |
| # intrinsics in cpp/include/simd/. Golden-output example checks double as | |
| # a layer-level scalar-vs-AVX2 equivalence assertion. All GitHub-hosted | |
| # x64 runners have AVX2. | |
| name: ASan + UBSan (AVX2 backend) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install Boost | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y libboost-dev libboost-test-dev | |
| - name: Build and run dispatch-consumer suites with AVX2 under ASan/UBSan | |
| if: needs.changes.outputs.code == 'true' | |
| env: | |
| BOOST_HOME: /usr | |
| run: make sanitize-avx2 | |
| tsan: | |
| # Data races in the TINYMIND_ENABLE_OPENMP=1 conv output-filter loop -- | |
| # the library's only concurrent code. No other gate covers this class: | |
| # ASan/UBSan don't detect races and the OPENMP=1 corner otherwise never | |
| # executes in CI. Clang + libomp (TSan-annotated); GCC's libgomp would | |
| # report false races on its own barriers. | |
| name: TSan (OpenMP conv path) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install toolchain | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y clang libomp-dev libboost-dev libboost-test-dev | |
| - name: Build and run OpenMP-enabled suites under TSan | |
| if: needs.changes.outputs.code == 'true' | |
| env: | |
| BOOST_HOME: /usr | |
| run: make tsan | |
| cppcheck: | |
| name: cppcheck | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install cppcheck | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y cppcheck | |
| - name: cppcheck (warning + portability) | |
| if: needs.changes.outputs.code == 'true' | |
| run: make cppcheck | |
| coverage: | |
| name: Coverage gate | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install Boost + lcov | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y libboost-dev libboost-test-dev lcov | |
| - name: Capture coverage and enforce floor | |
| if: needs.changes.outputs.code == 'true' | |
| env: | |
| BOOST_HOME: /usr | |
| run: | | |
| make coverage | |
| make coverage-check | |
| fuzz-replay: | |
| # Name pinned to the branch-protection required-check context | |
| # ("Fuzz int8 kernels"). The job is now a deterministic replay gate (see | |
| # the step) rather than the old time-boxed exploratory run, which moved to | |
| # fuzz-nightly.yml. | |
| name: Fuzz int8 kernels | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Hard gate: deterministic replay of the committed seed + crash corpus | |
| # (-runs=0, no new mutation). Fast and reproducible -- a regressed kernel | |
| # re-triggers its archived crash input. Exploratory fuzzing runs nightly | |
| # (fuzz-nightly.yml), not here, so the PR gate never flaps on a path found | |
| # at second 89 vs 91. | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install Clang | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y clang | |
| - name: Replay committed corpus (ASan+UBSan) | |
| if: needs.changes.outputs.code == 'true' | |
| run: make -C fuzz replay FUZZ_CXX=clang++ | |
| cbmc: | |
| name: CBMC proofs (qformat kernels) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install CBMC | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y cbmc | |
| - name: Prove fixed-point kernels | |
| if: needs.changes.outputs.code == 'true' | |
| run: make -C formal prove | |
| misra: | |
| name: MISRA C:2012 (advisory) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Advisory: cppcheck's MISRA C ruleset run against C++17 template code. | |
| # Most findings are expected; this surfaces them for review, never blocks. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install cppcheck | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y cppcheck | |
| - name: MISRA C:2012 addon | |
| if: needs.changes.outputs.code == 'true' | |
| run: make misra | |
| - name: Upload report | |
| if: always() && needs.changes.outputs.code == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: misra-report | |
| path: misra-report.txt | |
| if-no-files-found: ignore | |
| tidy: | |
| name: clang-tidy (advisory) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Advisory: surfaces clang-tidy findings (incl. clang static analyzer) but | |
| # never blocks the merge. The hard gates are sanitize + cppcheck. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install toolchain | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y libboost-dev libboost-test-dev clang-tidy bear | |
| - name: clang-tidy | |
| if: needs.changes.outputs.code == 'true' | |
| env: | |
| BOOST_HOME: /usr | |
| run: make tidy | |
| - name: Upload report | |
| if: always() && needs.changes.outputs.code == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: tidy-report | |
| path: tidy-report.txt | |
| if-no-files-found: ignore | |
| arm-crosscompile: | |
| # HARD GATE. The embedded matrix builds every gate corner with host g++ and | |
| # freestanding -D macros -- it never proves the headers assemble for a real | |
| # Cortex-M. This compiles (no link) the smoke source across M0+/M4/M33 and | |
| # soft/hard float ABIs with the arm-none-eabi toolchain, catching ABI/size/ | |
| # intrinsic breakage the host build hides. Build-only; no hardware needed. | |
| name: ARM cross-compile (Cortex-M) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install arm-none-eabi toolchain | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi | |
| - name: Cross-compile smoke corners for Cortex-M | |
| if: needs.changes.outputs.code == 'true' | |
| run: make -C unit_test/embedded arm_crosscompile | |
| heap-free: | |
| # HARD GATE. TinyMind targets no-heap MCUs; the library must never reference | |
| # the dynamic allocator. Compiles the freestanding + quant corners and | |
| # asserts no malloc / operator new symbols. A std::vector / new sneaking into | |
| # an embedded header re-triggers this. | |
| name: Heap-free guarantee | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Assert no dynamic-allocation symbols | |
| if: needs.changes.outputs.code == 'true' | |
| run: make -C unit_test/embedded heapfree | |
| warnings-strict: | |
| name: Conversion warnings (advisory) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Advisory: -Wconversion/-Wsign-conversion/-Wdouble-promotion over the | |
| # fixed-point paths. Expected narrowings in QValue internals mean this is a | |
| # review signal + ratchet baseline, never a merge gate. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Report conversion / promotion warnings | |
| if: needs.changes.outputs.code == 'true' | |
| run: make warnings-strict | |
| - name: Upload report | |
| if: always() && needs.changes.outputs.code == 'true' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: strict-warnings | |
| path: strict-warnings.txt | |
| if-no-files-found: ignore | |
| header-selfcheck: | |
| name: Header self-containment (advisory) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Advisory: compiles each cpp/**/*.hpp standalone (platform.hpp prelude). | |
| # Reports headers that only build via bundled include order so the list can | |
| # be ratcheted down; never blocks. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Compile each header standalone | |
| if: needs.changes.outputs.code == 'true' | |
| run: make header-selfcheck | |
| stack-usage: | |
| name: Stack-usage report (advisory) | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| # Advisory: -fstack-usage over the deployable Cortex-M4 int8 shape. Reports | |
| # deepest per-frame stack for review of unbounded growth; never blocks. | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| if: needs.changes.outputs.code == 'true' | |
| - name: Install arm-none-eabi toolchain | |
| if: needs.changes.outputs.code == 'true' | |
| run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi | |
| - name: Report deepest stack frames | |
| if: needs.changes.outputs.code == 'true' | |
| run: make -C unit_test/embedded stack_usage |