memlnaut-nisps/.github/workflows/ci.yml
monkey-w1n5t0n 68d4cc4017 build(firmware): migrate to PlatformIO and vendor memllib (plan §5)
One cut, no dual path. Closes ALIGNMENT defect 3 ("Arduino-CLI build
machinery is actively hostile") and vision bullet 4.

platformio.ini carries 16 [env:], one per variant, each passing
-DMEMLNAUT_MODE_TYPE; selftest passes -DNISPS_SELFTEST=1 instead. The env list
IS the registry now — the .ino comment-registry and the NISPS_ST_* token-paste
table are deleted rather than migrated. L12 noted that table was already
silently missing the currently-shipped SLPWorkshop variant, which is the whole
argument against having a second list.

Also deleted: the Python/sed machinery that rewrote the COMMITTED .ino on every
build, the sketch symlink forest, the global TFT_eSPI User_Setup.h mutation
(now -D flags — TFT_eSPI's own documented PlatformIO recipe), the UF2
boot-mount detection stack (upload_protocol=picotool talks to the bootloader
directly), and build-firmware-arch.sh entirely. Scripts 683 -> 435 lines.

memllib is vendored at lib/memllib/ from upstream e291192; no submodules
remain. VENDORED.md records provenance and the re-sync procedure.

S9: a firmware-build CI job compiles three representative envs against a cached
toolchain and reports per-variant flash/RAM. Firmware is in an automated gate
for the FIRST time. The old ci.yml comment justified excluding it as "low
verification value" — an assessment that did not survive contact, since 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.

Verified: all 16 envs build from an empty cache, each within ~520 bytes of the
arduino-cli binary it replaces, flash and RAM. Measured as .text+.rodata /
.data+.bss+vector+uninitialized — NOT PlatformIO's console line, which
double-counts .data on this board. This does not prove the hardware boots; no
flash+smoke test was possible and that stays an operator chokepoint.

  slpworkshop 248232/145028   pafsynth 256880/149716   selftest 216228/17960
  (all 16 in the CI log format; none exceeds 2% of a 16 MB flash)

Two traps recorded so nobody rediscovers them: vendoring memllib's subdirs
without a src/ wrapper makes PlatformIO's library builder silently compile
NOTHING while still linking; and project build_flags land BEFORE the
framework's own -std=gnu++17 -Os, so build_unflags is required.

CORRECTION carried in this commit: the firmware sizes in c19d846's message and
the first version of the memllib recon doc were wrong — SLPWorkshop 145348,
PAFSynth 145300, SelfTest 141840. They came from building variants in sequence
through a SHARED incremental arduino-cli build directory, which reused stale
objects and under-reported by ~75 KB. Clean-cache rebuilds of the identical
commit give 216736/18492 for SelfTest. The real cost of the memllib upstream
bump is +216 bytes flash, not +316. Never measure firmware size through a
reused build dir.

HISTORY NOTE: this commit and the docs commit before it were rebuilt (force-push,
2026-07-21) so that each contains only what its message describes. The first
versions had the firmware deletions stranded in the docs commit by a shared-index
race between concurrent agents; content is byte-identical to the originals.

Gates: run-all-tests.sh ALL GREEN (nisps/ untouched by this change beyond
include paths); 16/16 pio envs build.
2026-07-21 20:17:58 +02:00

214 lines
7.6 KiB
YAML

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.
# * 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
- 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
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
# Three representative envs rather than all sixteen: they differ only in
# which mode type is instantiated, so a compile break is almost always
# common to all. slpworkshop is the shipped default, pafsynth is the
# heaviest RAM user, and selftest takes the separate NISPS_SELFTEST fork
# through main.cpp — the one that broke unnoticed before.
- name: Build representative firmware variants
working-directory: firmware/MEMLNaut-NISPS
run: pio run -e slpworkshop -e pafsynth -e selftest
# 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; 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