feat: cross-validation harness and performance regression tracking #55
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] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| python: | |
| name: Python Tests & Lint | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12"] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Lint with ruff | |
| run: ruff check python/ | |
| - name: Type check with mypy | |
| run: mypy python/arbiter/ | |
| - name: Run tests | |
| run: pytest tests/python -v | |
| - name: Validate sample model | |
| run: arbiterc validate samples/battery_policy/models/battery.arb.yaml --strict | |
| - name: Profile validation — compile all samples on standard profile | |
| run: | | |
| for model in samples/*/models/*.arb.yaml; do | |
| echo "--- $model ---" | |
| arbiterc compile "$model" --profile standard --out-c /dev/null --out-h /dev/null | |
| done | |
| clang-tidy: | |
| name: clang-tidy Static Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install clang-tidy | |
| run: sudo apt-get install -y --no-install-recommends clang-tidy | |
| - name: Run clang-tidy on engine sources | |
| run: | | |
| clang-tidy \ | |
| lib/arbiter_engine.c \ | |
| lib/arbiter_eval.c \ | |
| lib/arbiter_fact_store.c \ | |
| lib/arbiter_trace.c \ | |
| lib/arbiter_blob.c \ | |
| lib/arbiter_action.c \ | |
| -- \ | |
| -I include \ | |
| -isystem tools/clang-tidy-stubs \ | |
| -std=c11 \ | |
| -DCONFIG_ARBITER_LOG_LEVEL=3 \ | |
| -DCONFIG_ARBITER_MAX_FACTS=64 \ | |
| -DCONFIG_ARBITER_MAX_ACTIONS_PER_EVAL=16 \ | |
| -DCONFIG_ARBITER_MAX_TRACE_ENTRIES=64 \ | |
| -DCONFIG_ARBITER_MAX_TRACE_INPUTS=8 | |
| spdx: | |
| name: SPDX Header Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Check SPDX headers | |
| run: | | |
| missing=0 | |
| for f in $(find lib/ include/ subsys/ python/ -name '*.c' -o -name '*.h' -o -name '*.py' | grep -v __pycache__); do | |
| if ! head -5 "$f" | grep -q 'SPDX-License-Identifier'; then | |
| echo "Missing SPDX header: $f" | |
| missing=1 | |
| fi | |
| done | |
| exit $missing | |
| misra: | |
| name: MISRA-C Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install cppcheck | |
| run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cppcheck | |
| - name: Run cppcheck with MISRA C 2012 rules | |
| run: | | |
| # cppcheck's MISRA addon may not be available on ubuntu-latest; | |
| # fall back to built-in checks if the addon fails. | |
| if cppcheck --addon=misra --version 2>/dev/null; then | |
| cppcheck --addon=misra \ | |
| --suppress=misra-c2012-1.1 \ | |
| --suppress=misra-c2012-20.9 \ | |
| --suppress=misra-c2012-21.6 \ | |
| -I include/ \ | |
| -isystem tools/clang-tidy-stubs \ | |
| --error-exitcode=1 \ | |
| --inline-suppr \ | |
| lib/*.c | |
| else | |
| echo "MISRA addon not available — falling back to built-in checks" | |
| cppcheck \ | |
| --enable=all \ | |
| --suppress=missingIncludeSystem \ | |
| -I include/ \ | |
| -isystem tools/clang-tidy-stubs \ | |
| --error-exitcode=1 \ | |
| --inline-suppr \ | |
| lib/*.c | |
| fi | |
| zephyr: | |
| name: Zephyr Twister Tests | |
| runs-on: ubuntu-latest | |
| env: | |
| # native_sim uses host GCC; skip cross-compiler toolchain lookup. | |
| ZEPHYR_TOOLCHAIN_VARIANT: host | |
| # Tell Zephyr's module system where arbiter lives. west skips | |
| # cloning the manifest project to its declared module path, so | |
| # ZEPHYR_EXTRA_MODULES is the clean solution. Now that the | |
| # Kconfig symbol (ARBITER) and CMake guard (CONFIG_ARBITER) match, | |
| # and sources use CMAKE_CURRENT_LIST_DIR, this resolves fully. | |
| ZEPHYR_EXTRA_MODULES: ${{ github.workspace }}/app | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # west.yml declares self.path: app — match it so west module | |
| # resolution works correctly alongside modules/lib/arbiter/ | |
| path: app | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install west | |
| run: pip install west | |
| # Cache the Zephyr tree and west state keyed on west.yml. | |
| # All three paths are saved/restored together so a partial | |
| # hit cannot leave the workspace in an inconsistent state. | |
| - name: Cache west workspace | |
| id: cache-west | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| zephyr | |
| modules | |
| .west | |
| key: west-${{ hashFiles('app/west.yml') }} | |
| restore-keys: west- | |
| # Only run init+update on a cache miss; .west/config is | |
| # restored from cache and is sufficient for subsequent steps. | |
| - name: West init | |
| if: steps.cache-west.outputs.cache-hit != 'true' | |
| run: west init -l app | |
| # --narrow: only fetch projects in our top-level manifest | |
| # (zephyr + arbiter), skipping Zephyr's full HAL set. | |
| # -o=--depth=1: shallow clone to keep the runner fast. | |
| # native_sim needs only the Zephyr core tree, no HALs. | |
| - name: West update | |
| if: steps.cache-west.outputs.cache-hit != 'true' | |
| run: west update --narrow -o=--depth=1 | |
| # native_sim/native targets 32-bit by default; gcc-multilib provides | |
| # the 32-bit glibc headers (bits/libc-header-start.h) on ubuntu-latest. | |
| - name: Install native_sim build dependencies | |
| run: sudo apt-get install -y --no-install-recommends gcc-multilib g++-multilib | |
| - name: Install Zephyr Python requirements | |
| run: pip install -r zephyr/scripts/requirements.txt | |
| # Zephyr CMake requires find_package(Zephyr-sdk) even for native_sim | |
| # (host tools: DTC, cmake config files). The minimal bundle is ~10 MB | |
| # and contains no cross-compiler — only the cmake registration files. | |
| # setup.sh -c writes ~/.cmake/packages/Zephyr-sdk/ so CMake finds it. | |
| # SDK 1.0.1 matches pinned Zephyr v4.4.0 (Zephyr v4.4.0 requires >= 1.0.0). | |
| # sdk-version file does not exist in v4.4.0; version is hardcoded here. | |
| - name: Install Zephyr SDK (minimal, cmake files only) | |
| env: | |
| SDK_VER: "1.0.1" | |
| run: | | |
| wget -q \ | |
| "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${SDK_VER}/zephyr-sdk-${SDK_VER}_linux-x86_64_minimal.tar.xz" \ | |
| -O /tmp/zephyr-sdk-minimal.tar.xz | |
| mkdir -p ~/zephyr-sdk | |
| tar -xf /tmp/zephyr-sdk-minimal.tar.xz --strip-components=1 -C ~/zephyr-sdk | |
| ~/zephyr-sdk/setup.sh -c | |
| # Unit tests run as native executables on native_sim. | |
| # -fstack-usage emits .su files for stack analysis. | |
| - name: Twister — unit tests | |
| run: | | |
| west twister \ | |
| -T app/tests/unit \ | |
| -p native_sim \ | |
| --inline-logs -v \ | |
| -O twister-out/unit \ | |
| -- -DEXTRA_CFLAGS=-fstack-usage | |
| - name: Check stack usage (max 512 bytes per function) | |
| run: | | |
| echo "--- Stack usage analysis ---" | |
| err=0 | |
| while IFS= read -r sufile; do | |
| while IFS=$'\t' read -r func bytes type; do | |
| if [ "${bytes:-0}" -gt 512 ] 2>/dev/null; then | |
| echo "STACK EXCEEDED: $sufile: $func $bytes $type" | |
| err=1 | |
| fi | |
| done < "$sufile" | |
| done < <(find twister-out/unit -name '*.su' 2>/dev/null) | |
| if [ $err -ne 0 ]; then | |
| echo "::error::One or more functions exceed 512-byte stack limit" | |
| exit 1 | |
| fi | |
| echo "All functions within 512-byte stack limit" | |
| # Benchmarks execute on native_sim; Twister captures timing output | |
| # as an artifact. pass/fail is gated on the final log line regex. | |
| - name: Twister — benchmarks | |
| run: | | |
| west twister \ | |
| -T app/tests/benchmarks \ | |
| -p native_sim \ | |
| --inline-logs -v \ | |
| -O twister-out/benchmarks | |
| # Parse benchmark timing and print summary table. | |
| # No fail threshold yet — we need baseline data first. | |
| - name: Benchmark timing summary | |
| if: always() | |
| run: python app/tools/parse_benchmark.py twister-out/benchmarks/ | |
| # All 17 samples are build_only; CI proves they compile clean. | |
| - name: Twister — samples | |
| run: | | |
| west twister \ | |
| -T app/samples \ | |
| -p native_sim \ | |
| --inline-logs -v \ | |
| -O twister-out/samples | |
| - name: Upload Twister results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: twister-results | |
| path: twister-out/ | |
| - name: Upload stack usage reports | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: stack-usage-reports | |
| path: twister-out/**/*.su | |
| if-no-files-found: ignore |