CI and Releases
Four workflows: three with one job in life each on github.com, and a fourth (hardware-in-the-loop) that stays dormant until a device runner appears.
build.yml: the merge gate
On every push and pull request (skipping docs-only changes):
- Firmware build against the pinned ESP-IDF v6.0.2 container (
IDF_VERSIONin the workflow), exactly the version developers run, because "builds on latest IDF" is not the claim releases make. The st7789 and production compile-checks run the repo's CMake presets, and the Unity suite builds directly with-C testbecause preset discovery does not reach the test subproject; only the BLE compile-check still passes explicitSDKCONFIG_DEFAULTS. - ST7789 variant build with the
sdkconfig.ci.st7789overlay, so the second panel option always compiles even though nobody's daily build selects it. - Test app build so the Unity suites cannot silently rot even between on-target runs.
- Managed-component cache keyed on the hash of all
idf_component.ymlmanifests plus the lock file; keying on anything less once served stale LVGL to CI while builds locally diverged, a category of failure worth designing away permanently. - Dashboard check (
node --checkon the JS) because the dashboard has no build step to catch syntax errors otherwise. - Size report on pull requests: flash and RAM deltas versus the base branch, surfacing the cost of a change while it is still negotiable. The budget that matters: the app must stay comfortably under the 3 MB slot size; at ~2.2 MB there is real headroom, and the report exists to keep the burn rate visible.
- Lint: clang-format over the firmware sources.
docs.yml: this site
Push to main touching safi-docs/** builds the Docusaurus site (both locales, broken links fatal) and deploys to GitHub Pages via the official Pages actions. Diagram SVGs are committed rather than rendered in CI, so the pipeline needs Node and nothing else; see the diagram style guide for the local export loop.
hil.yml: hardware-in-the-loop (dormant)
A fourth workflow runs the on-target Unity suite, the stdio integrity gate, and a dev-firmware restore on a self-hosted runner labeled safi-hil. It is dormant by design: no GitHub-hosted runner carries that label, so the job never matches a runner until someone attaches hardware and registers one. It triggers on push and manual dispatch only; a pull-request trigger would leave a permanently pending check on every PR while the runner does not exist, so this workflow must not be made a required check until it is activated. What it runs and how to activate it: hardware-in-the-loop testing.
release.yml: tags to artifacts
A v* tag produces a GitHub Release carrying:
| Artifact | Purpose |
|---|---|
Safi.bin | The app image, what OTA consumes |
bootloader.bin, partition-table.bin, storage.bin | The rest of a full USB flash |
Safi.elf | Symbol file for decoding panic backtraces in logs and developer debugging; production builds ship with coredumps disabled (coredump policy) |
SHA256SUMS | Integrity for all of the above |
Release notes lead with the partition-table-changed warning when applicable (the one change OTA cannot deliver) and the highlights since the previous tag.
Unsigned by design in the public pipeline: secure-boot signing happens in the operator's environment with keys CI never sees. A public repo's CI holding production signing keys would concentrate exactly the risk secure boot exists to remove.
Versioning
Firmware version comes from the git tag at build time and surfaces in GET /api/system, the dashboard, and the splash screen, so "what are you running" is always answerable from any of the three without a cable.