Skip to content

feat: cross-validation harness and performance regression tracking #55

feat: cross-validation harness and performance regression tracking

feat: cross-validation harness and performance regression tracking #55

Workflow file for this run

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