name: CI # Stream 11 verification pipeline. # # Three parallel jobs: # * cpp-tests — builds nisps host C++ tests, builds nisps.wasm, runs # the parity check, runs the lint script, and reports # per-engine throughput on both host targets. # * manifold-tests — typechecks the React manifold app, runs bun unit # tests, builds the production bundle, runs Playwright # e2e tests. # # * firmware-build — compiles three representative PlatformIO envs for the # RP2350 target. # # Firmware entered CI for the first time with the Phase 4 PlatformIO migration # (plan §5, S9). It was previously excluded because arduino-cli + the rp2040 # board package cost ~2 minutes per run for "low verification value" — an # assessment that did not survive contact: the SelfTest variant sat broken for # an unknown period, calling a DisplayDriver method that did not exist at the # pinned memllib commit, and nothing noticed because nothing built it. PIO's # toolchain caches cleanly, so the cost is now a cache restore. on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: cpp-tests: name: C++ tests + WASM + parity + lint runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install build deps run: | sudo apt-get update sudo apt-get install -y --no-install-recommends \ cmake ninja-build build-essential perl - name: Setup Emscripten # mymindstorm/setup-emsdk caches the SDK between runs. Pin to a # known-working version; bump deliberately. uses: mymindstorm/setup-emsdk@v14 with: version: '3.1.69' actions-cache-folder: 'emsdk-cache' - name: Verify emcc run: emcc --version - name: Setup Node (for parity_wasm.mjs) uses: actions/setup-node@v4 with: node-version: '20' - name: Build C++ tests + run ctest env: # CI is non-interactive — turn off colour for log readability. CMAKE_BUILD_PARALLEL_LEVEL: '4' run: bash scripts/build-cpp-tests.sh # Freshness gate. manifold/public/nisps.{js,wasm} are TRACKED artifacts: # the VPS webhook builds only manifold/, so vite copies whatever is # committed into the production bundle. Prove the committed artifact # still agrees with the C++ we just built, BEFORE the rebuild below # overwrites it. A stale commit fails here instead of silently shipping. - name: WASM freshness gate (committed artifact vs native) env: NISPS_PARITY_NO_BUILD: '1' run: bash scripts/parity-check.sh - name: Build WASM # setup-emsdk puts emcc on PATH; build-wasm.sh picks it up via # `command -v emcc`. No EMCC override needed. run: bash scripts/build-wasm.sh - name: Parity check (native vs WASM) env: NISPS_PARITY_NO_BUILD: '1' # we just built; don't re-build run: bash scripts/parity-check.sh - name: Lint run: bash scripts/lint-cpp.sh # Throughput is reported, not asserted — the same call the firmware # flash/RAM step makes below, for a stronger reason: a wall-clock # threshold on a shared runner is either slack enough to be meaningless # or tight enough to fail on someone else's noisy neighbour. What this # buys is (a) a per-commit ns/sample + realtime-factor record in the log, # where a 2-3x regression — the failure mode ALIGNMENT defect 5 names — # is unmissable even at runner noise levels, (b) proof the bench still # builds and runs on BOTH targets, and (c) the wasm/native ratio. # Runner noise makes small moves unreadable; for a real comparison run # scripts/bench-engines.sh --compare locally. - name: Engine throughput report (native + WASM, not asserted) run: bash scripts/bench-engines.sh --target-ms 60 - name: Upload parity blobs on failure if: failure() uses: actions/upload-artifact@v4 with: name: parity-blobs path: | tests/cpp/parity_native.bin tests/cpp/parity_wasm.bin if-no-files-found: ignore retention-days: 7 manifold-tests: name: Manifold typecheck + unit + e2e runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest # Enforces the "schema changes ship with regenerated C++ AND TypeScript # in the same change" rule. Regenerate from schemas/ and fail if the # committed outputs differ; then run the codegen golden test. - name: Codegen is committed and idempotent run: | cd codegen bun install --frozen-lockfile bun run generate.ts bun run generate-midi-devices.ts bun run tests/golden_test.ts # The schemas' per-voice-space `curve` declarations are DESCRIPTIVE # of nisps/engines/*.hpp. Prove they still describe it. bun run tests/curve_drift_test.ts cd .. GEN_DIRS="nisps/modes/generated nisps/midi/generated nisps/ml/generated \ manifold/src/modes/generated manifold/src/midi-devices/generated" git add -N $GEN_DIRS if ! git diff --exit-code -- $GEN_DIRS; then echo "::error::Generated outputs are stale. Run codegen and commit the result." >&2 exit 1 fi - name: Install manifold deps working-directory: manifold run: bun install --frozen-lockfile - name: Typecheck working-directory: manifold run: bun run typecheck - name: Unit tests working-directory: manifold run: bun run test - name: Build manifold bundle working-directory: manifold run: bun run build - name: Install Playwright browsers working-directory: manifold run: bunx playwright install --with-deps chromium - name: Run Playwright tests working-directory: manifold run: bunx playwright test - name: Upload Playwright report on failure if: failure() uses: actions/upload-artifact@v4 with: name: playwright-report path: manifold/playwright-report/ retention-days: 7 firmware-build: name: Firmware (RP2350, PlatformIO) runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' # The platform wrapper and the arduino-pico framework are fetched from # git and total ~1-2 GB. Key on platformio.ini because that file pins # both versions — change a pin, get a fresh toolchain. - name: Cache PlatformIO toolchain uses: actions/cache@v4 with: path: | ~/.platformio ~/.cache/pip key: pio-${{ runner.os }}-${{ hashFiles('firmware/MEMLNaut-NISPS/platformio.ini') }} restore-keys: pio-${{ runner.os }}- - name: Install PlatformIO run: pip install --upgrade platformio # Four representative envs rather than all sixteen: they differ mostly in # which mode type is instantiated, so a compile break is usually common to # all. slpworkshop is the shipped default, pafsynth is the heaviest RAM # user, selftest takes the separate NISPS_SELFTEST fork through main.cpp # (the one that broke unnoticed before), and soundanalysismidi is the ONLY # microphone variant — the sole mode whose DriverConfig differs from its # engine's, via an on_driver_config() override. Drop it and the mic path # stops being compiled by anything. - name: Build representative firmware variants working-directory: firmware/MEMLNaut-NISPS run: pio run -e slpworkshop -e pafsynth -e selftest -e soundanalysismidi # Sizes are reported, not asserted. A threshold would either be slack # enough to be meaningless or tight enough to fail on unrelated work; # this puts the numbers in the log so a jump is visible in review. # Flash = .text + .rodata, RAM = .data + .bss + vector table + # uninitialized — matching arduino-cli's convention, NOT PlatformIO's # own console line, which double-counts .data on this board. - name: Report flash/RAM per variant working-directory: firmware/MEMLNaut-NISPS run: | SIZE=$(find ~/.platformio/packages -name 'arm-none-eabi-size' | head -1) for e in slpworkshop pafsynth selftest soundanalysismidi; do "$SIZE" -A ".pio/build/$e/firmware.elf" | awk -v e="$e" ' /^\.text/{t=$2} /^\.rodata/{r=$2} /^\.data/{d=$2} /^\.bss/{b=$2} /^\.ram_vector_table/{v=$2} /^\.uninitialized_data/{u=$2} END{printf "%-14s flash=%-8d ram=%-8d\n", e, t+r, d+b+v+u}' done