A pull request's firmware step took 358 s: 260 of them rebuilding the framework that was already in the volume, because the platform decides by sdkconfig.defaults in the project folder, which is generated and not in git, so no fresh checkout had it. It is now kept in the volume, inside the libraries it describes, and copied into the checkout; the platform still checks its hash against platformio.ini. The version was a -D on every command line: each commit recompiled everything, and no cache could help. scripts/version.py now writes one generated header, read by one file. PlatformIO's build cache in the volume for pull requests' firmware, ccache for the host tests (built for coverage, which the build cache can't keep), the tools in a venv in the volume, and a tag builds its firmware once. A release still compiles its own sources from nothing. Measured on fresh copies of the tree: the firmware step 358 s to 27 s (a new version and one changed file), tests and coverage 49 s to 33 s, a local rebuild with nothing changed 77 s to 13 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
2.5 KiB
+++ title = "Build, test and release" description = "Docker is the only tool you need. How the firmware is built, how the host tests run, and what CI does on a pull request and on a tag." weight = 1
[extra] docs = true source = "README.md" tag = "Build" +++
Requirements
Only Docker is needed. PlatformIO and the ESP32 toolchain run inside a container, and are cached in the roro9stack-pio Docker volume. The first build downloads about 1 GB and takes a few minutes.
Build and test (local CI)
scripts/ci.sh
This runs the host-side unit tests (test/, native environment), then builds the firmware. The output is .pio/build/cardputer-adv/firmware.factory.bin.
scripts/coverage.sh runs the same tests with coverage counters and writes a line-by-line report to .pio/coverage/index.html. The badge above is its figure for main: the share of the lines of lib/ that the host tests run. lib/ is the logic that compiles on a PC; lib/SD (the card's driver) and src/ (the Apps, the Services, everything that needs the device) have no host tests and aren't in that figure.
The framework is rebuilt with the TLS settings in platformio.ini (custom_sdkconfig, ADR 0006), so the first build after a fresh checkout takes about 6 minutes; later builds take about 15 seconds when little has changed. The platform knows the framework is already rebuilt by sdkconfig.defaults in the project folder, which it writes and git ignores: delete it and the next build rebuilds the framework. CI keeps that file, a build cache and ccache in its volume (docs/milestones/R1.md).
CI and releases
Gitea Actions (.gitea/workflows/ci.yml, docs/milestones/R1.md) runs the host tests on every push to main, and on a pull request also builds the firmware: changes reach main through pull requests. Pushing a tag v* runs all of it and publishes a release on Gitea with:
roro9stack-<version>.ota, the signed Update File;roro9stack-<version>-factory.bin, the whole flash image for a first install over USB;roro9stack-<version>.elf.gz, to decode crash reports from that build;SHA256SUMS.
CI signs with the project's key, held as a repository secret (ADR 0008). There is one firmware: the Debug Console is in every build, switched off until its owner switches it on (ADR 0010).
scripts/ota_verify.py <file.ota> checks an Update File on a PC the way a device does. scripts/release_build.sh and scripts/release_publish.py are what the workflow runs; they work the same by hand.