From f002a13b359de97fc250bcdf718d9f11f78fea20 Mon Sep 17 00:00:00 2001 From: Justin Bunton <130115204+Bunton33@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:04:21 -0400 Subject: [PATCH 1/4] apollo_m1: pin libraries and repo identity to what bundle b8 built QuickESPNow tracked the optional-debug branch, which moved on 2026-09-27, so a clean checkout no longer compiled the ESP-NOW code b8 shipped. apollo_m1 now lists its libraries explicitly, in the inherited order, with QuickESPNow pinned to 193e9916 and AnimatedGIF to 1.4.7. WLED_REPO is set so the reported repo no longer depends on which remote the branch tracks. Co-Authored-By: Claude Opus 5.5 --- platformio.ini | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/platformio.ini b/platformio.ini index aee46449e6..5ad2a4ee39 100644 --- a/platformio.ini +++ b/platformio.ini @@ -924,3 +924,19 @@ build_flags = ${env:esp32s3dev_16MB_opi_hub75.build_flags} -D ABL_MILLIAMPS_DEFAULT=0 ;; dedicated 5V supply; see FACT 4 for the upstream ABL question -D UM_AUDIOREACTIVE_ENABLE ;; AudioReactive on by default (Justin, closes D3); rev4 no-mic impact tracked in QA D9 -D WLED_MM_HUB75_MIGRATION ;; read HUB75 bus entries written by WLED-MM cfg.json (OTA upgrade path) + -D WLED_REPO=\"ApolloAutomation/WLED-M1\" ;; fixed identity; set_metadata.py would otherwise read it from whatever remote the branch tracks +;; Pinned libraries for reproducible M-1 builds: the same list and order as the inherited +;; ${esp32s3.lib_deps} + ${hub75.lib_deps}, with the two floating entries fixed to exactly what +;; bundle b8 compiled. QuickESPNow #optional-debug is a branch (it moved on 2026-09-27) and +;; AnimatedGIF was ^1.4.7. Re-check this list against the inherited one on every upstream merge. +lib_deps = + esp32async/AsyncTCP @ 3.4.7 + bitbank2/AnimatedGIF @ 1.4.7 + https://github.com/Aircoookie/GifDecoder.git#bc3af189b6b1e06946569f6b4287f0b79a860f8e + https://github.com/someweisguy/esp_dmx.git#47db25d8c515e76fabcf5fc5ab0b786f98eeade0 + IRremoteESP8266 @ 2.8.2 + https://github.com/Makuna/NeoPixelBus.git#1d7ff38f14d04a976f9c6e365c83a232bcba04fc + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.2 + marvinroger/AsyncMqttClient @ 0.9.0 + https://github.com/blazoncek/QuickESPNow.git#193e991642cbffe1d44e7445a0a34445f69a67d8 + https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#f17fb7fe9d487e9643f919eb5aeedea8d9d1f8d7 ;; 3.0.14 From 236a1b6c2b6246b0127922e606852689719bcffb Mon Sep 17 00:00:00 2001 From: Justin Bunton <130115204+Bunton33@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:04:21 -0400 Subject: [PATCH 2/4] build_artifacts.sh: keep debug symbols, use PlatformIO's esptool, guard stale builds Keeps firmware.elf/map, bootloader, partitions, littlefs, BUILD_INFO.txt and SHA256SUMS.txt with every build. Uses PlatformIO's esptool 4.7.4 instead of whatever is on PATH. Accepts a prebuilt LittleFS image (--littlefs) because PlatformIO's mklittlefs is x86-only on macOS. Refuses a dirty tree and, with --skip-pio, a build older than the current commit. Co-Authored-By: Claude Opus 5.5 --- apollo/build_artifacts.sh | 100 ++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 15 deletions(-) diff --git a/apollo/build_artifacts.sh b/apollo/build_artifacts.sh index 25f01f8fca..0d615b4005 100755 --- a/apollo/build_artifacts.sh +++ b/apollo/build_artifacts.sh @@ -5,8 +5,18 @@ # LittleFS with factory cfg.json/presets.json) # out/M-1_ota.bin - app only, for the WLED OTA update page; does not # touch the filesystem +# plus what is needed to support the release later: firmware.elf and firmware.map +# (decode crash backtraces), bootloader.bin, partitions.bin, littlefs.bin, +# BUILD_INFO.txt (commit and tool versions) and SHA256SUMS.txt. # -# Usage: apollo/build_artifacts.sh [--skip-pio] +# Usage: apollo/build_artifacts.sh [--skip-pio] [--littlefs ] [--allow-dirty] +# --skip-pio package the existing .pio build instead of running pio first +# (refused if that build is older than the current commit) +# --littlefs use a prebuilt LittleFS image instead of running mklittlefs, +# e.g. the previous release's littlefs.bin when apollo/fs is +# unchanged (mklittlefs from PlatformIO is x86-only on macOS and +# needs Rosetta on Apple silicon) +# --allow-dirty allow uncommitted changes to tracked files (never for releases) set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -22,34 +32,79 @@ APP_SLOT_SIZE=$((0x300000)) FS_OFF=0x610000 FS_SIZE=$((0x9E0000)) -MKLITTLEFS="$HOME/.platformio/packages/tool-mklittlefs/mklittlefs" -BOOT_APP0="$(ls "$HOME"/.platformio/packages/framework-arduinoespressif32*/tools/partitions/boot_app0.bin 2>/dev/null | head -1)" -ESPTOOL="esptool.py" -command -v "$ESPTOOL" >/dev/null || ESPTOOL="python3 -m esptool" +SKIP_PIO=0 +LITTLEFS_IN="" +ALLOW_DIRTY=0 +while [ $# -gt 0 ]; do + case "$1" in + --skip-pio) SKIP_PIO=1 ;; + --littlefs) LITTLEFS_IN="${2:?--littlefs needs a file}"; shift ;; + --allow-dirty) ALLOW_DIRTY=1 ;; + *) echo "unknown option: $1"; exit 1 ;; + esac + shift +done + +# Use PlatformIO's own tools (the versions the platform pins), not whatever is on PATH. +PIO_HOME="${PLATFORMIO_CORE_DIR:-$HOME/.platformio}" +PIO_PY="$PIO_HOME/penv/bin/python" +[ -x "$PIO_PY" ] || PIO_PY="python3" +ESPTOOL_PY="$PIO_HOME/packages/tool-esptoolpy/esptool.py" +MKLITTLEFS="$PIO_HOME/packages/tool-mklittlefs/mklittlefs" +BOOT_APP0="$PIO_HOME/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin" + +[ -f "$ESPTOOL_PY" ] || { echo "esptool not found at $ESPTOOL_PY - build the $ENV_NAME env once first"; exit 1; } +[ -f "$BOOT_APP0" ] || { echo "boot_app0.bin not found at $BOOT_APP0"; exit 1; } +esptool() { "$PIO_PY" "$ESPTOOL_PY" "$@"; } -[ -x "$MKLITTLEFS" ] || { echo "mklittlefs not found at $MKLITTLEFS"; exit 1; } -[ -n "$BOOT_APP0" ] || { echo "boot_app0.bin not found in PlatformIO packages"; exit 1; } +if [ "$ALLOW_DIRTY" = 0 ] && [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no)" ]; then + echo "tracked files have uncommitted changes; commit them or pass --allow-dirty (not for releases)"; exit 1 +fi -if [ "${1:-}" != "--skip-pio" ]; then +if [ "$SKIP_PIO" = 0 ]; then (cd "$ROOT" && pio run -e "$ENV_NAME") fi -for f in bootloader.bin partitions.bin firmware.bin; do +for f in bootloader.bin partitions.bin firmware.bin firmware.elf firmware.map; do [ -f "$BUILD/$f" ] || { echo "missing $BUILD/$f - build the $ENV_NAME env first"; exit 1; } done -APP_SIZE=$(stat -f%z "$BUILD/firmware.bin" 2>/dev/null || stat -c%s "$BUILD/firmware.bin") +mtime() { stat -f%m "$1" 2>/dev/null || stat -c%Y "$1"; } +fsize() { stat -f%z "$1" 2>/dev/null || stat -c%s "$1"; } + +# A leftover bench build must never be packaged as a release. +if [ "$SKIP_PIO" = 1 ] && [ "$(mtime "$BUILD/firmware.bin")" -lt "$(git -C "$ROOT" log -1 --format=%ct)" ]; then + echo "$BUILD/firmware.bin is older than the current commit; rebuild (run without --skip-pio)"; exit 1 +fi + +APP_SIZE=$(fsize "$BUILD/firmware.bin") if [ "$APP_SIZE" -gt "$APP_SLOT_SIZE" ]; then echo "firmware.bin ($APP_SIZE bytes) exceeds the $APP_SLOT_SIZE byte OTA slot"; exit 1 fi mkdir -p "$OUT" +# Clear only this script's own outputs so a failed run can't leave a stale mix. +(cd "$OUT" && rm -f M-1_full_install.bin M-1_ota.bin littlefs.bin firmware.elf firmware.map \ + bootloader.bin partitions.bin BUILD_INFO.txt SHA256SUMS.txt) -echo "== LittleFS image from apollo/fs (offset $FS_OFF, size $FS_SIZE)" -"$MKLITTLEFS" -c "$ROOT/apollo/fs" -b 4096 -p 256 -s "$FS_SIZE" "$OUT/littlefs.bin" >/dev/null +if [ -n "$LITTLEFS_IN" ]; then + echo "== LittleFS image: prebuilt $LITTLEFS_IN" + [ "$(fsize "$LITTLEFS_IN")" -eq "$FS_SIZE" ] || { echo "$LITTLEFS_IN is not $FS_SIZE bytes"; exit 1; } + cp "$LITTLEFS_IN" "$OUT/littlefs.bin" + FS_SOURCE="prebuilt $(basename "$LITTLEFS_IN") (apollo/fs last changed in $(git -C "$ROOT" log -1 --format=%h -- apollo/fs))" +else + "$MKLITTLEFS" --version >/dev/null 2>&1 || { + echo "mklittlefs at $MKLITTLEFS does not run here (x86-only on macOS: needs Rosetta)." + echo "If apollo/fs is unchanged since the last release, pass --littlefs ." + exit 1 + } + echo "== LittleFS image from apollo/fs (offset $FS_OFF, size $FS_SIZE)" + "$MKLITTLEFS" -c "$ROOT/apollo/fs" -b 4096 -p 256 -s "$FS_SIZE" "$OUT/littlefs.bin" >/dev/null + FS_SOURCE="mklittlefs $("$MKLITTLEFS" --version 2>&1 | head -1) from apollo/fs" +fi echo "== M-1_full_install.bin (merged at 0x0, ESP32-S3)" -$ESPTOOL --chip esp32s3 merge_bin -o "$OUT/M-1_full_install.bin" \ +esptool --chip esp32s3 merge_bin -o "$OUT/M-1_full_install.bin" \ --flash_mode keep --flash_freq keep --flash_size 16MB \ 0x0 "$BUILD/bootloader.bin" \ 0x8000 "$BUILD/partitions.bin" \ @@ -57,11 +112,26 @@ $ESPTOOL --chip esp32s3 merge_bin -o "$OUT/M-1_full_install.bin" \ "$APP0_OFF" "$BUILD/firmware.bin" \ "$FS_OFF" "$OUT/littlefs.bin" -echo "== M-1_ota.bin (app only)" +echo "== M-1_ota.bin (app only) and support files" cp "$BUILD/firmware.bin" "$OUT/M-1_ota.bin" +cp "$BUILD/firmware.elf" "$BUILD/firmware.map" "$BUILD/bootloader.bin" "$BUILD/partitions.bin" "$OUT/" + +{ + echo "commit: $(git -C "$ROOT" rev-parse HEAD) ($(git -C "$ROOT" describe --tags --always --dirty))" + echo "built: $(date -u '+%Y-%m-%dT%H:%M:%SZ') (firmware.bin from $(date -u -r "$(mtime "$BUILD/firmware.bin")" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -d "@$(mtime "$BUILD/firmware.bin")" '+%Y-%m-%dT%H:%M:%SZ'))" + echo "env: $ENV_NAME" + echo "platformio: $(pio --version 2>/dev/null || echo unknown)" + echo "esptool: $(esptool version 2>/dev/null | tail -1)" + echo "littlefs: $FS_SOURCE" + echo "web ui: $(grep -h 'define WEB_BUILD_TIME' "$ROOT"/wled00/html_ui.h 2>/dev/null | head -1)" +} > "$OUT/BUILD_INFO.txt" + +(cd "$OUT" && shasum -a 256 M-1_full_install.bin M-1_ota.bin littlefs.bin firmware.elf firmware.map bootloader.bin partitions.bin > SHA256SUMS.txt) echo echo "Artifacts in $OUT:" -ls -l "$OUT/M-1_full_install.bin" "$OUT/M-1_ota.bin" +ls -l "$OUT" +echo +cat "$OUT/BUILD_INFO.txt" echo echo "App size: $APP_SIZE / $APP_SLOT_SIZE bytes ($((APP_SIZE * 100 / APP_SLOT_SIZE))% of OTA slot)" From 6436bb98b1f29c8561f8c1950165483a800e2cd6 Mon Sep 17 00:00:00 2001 From: Justin Bunton <130115204+Bunton33@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:04:21 -0400 Subject: [PATCH 3/4] CI: build the apollo_m1 env Upstream's WLED CI never builds the env Apollo ships. This workflow builds apollo_m1 on pushes and PRs to the product branch, with no secrets and a read-only token, and keeps the ELF and map for 90 days. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/apollo-m1-build.yml | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/apollo-m1-build.yml diff --git a/.github/workflows/apollo-m1-build.yml b/.github/workflows/apollo-m1-build.yml new file mode 100644 index 0000000000..47ddbd618c --- /dev/null +++ b/.github/workflows/apollo-m1-build.yml @@ -0,0 +1,48 @@ +name: Apollo M-1 build + +# Builds the apollo_m1 env (the firmware Apollo ships) for every change to the product +# branch and every pull request into it. Upstream's WLED CI does not build this env. +# No secrets are used and the token is read-only. The ELF and map are kept for 90 days. +on: + push: + branches: [m1-wled-update, main] + pull_request: + branches: [m1-wled-update, main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: apollo-m1-build-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'npm' + - run: npm ci + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + - run: pip install -r requirements.txt + - name: Build apollo_m1 + run: pio run -e apollo_m1 + - name: Resolved libraries + run: pio pkg list -e apollo_m1 + - uses: actions/upload-artifact@v4 + with: + name: apollo_m1-${{ github.sha }} + retention-days: 90 + path: | + .pio/build/apollo_m1/firmware.bin + .pio/build/apollo_m1/firmware.elf + .pio/build/apollo_m1/firmware.map + .pio/build/apollo_m1/bootloader.bin + .pio/build/apollo_m1/partitions.bin From 4f701c6ee89dbd1d472b1721bd118249a2362263 Mon Sep 17 00:00:00 2001 From: Justin Bunton <130115204+Bunton33@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:04:21 -0400 Subject: [PATCH 4/4] Add the M-1 release checklist Step-by-step release process: gates (including OTA), artifacts, tag before release with --verify-tag, immutable release, install page redeploy, FlashScripts pin bump, firmware vault, final hash checks. CLAUDE.md points to it. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 8 +++- apollo/RELEASE_CHECKLIST.md | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 apollo/RELEASE_CHECKLIST.md diff --git a/CLAUDE.md b/CLAUDE.md index 84633825d7..e6d2923b14 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,5 +55,9 @@ The Apollo rules below take precedence over anything inherited from upstream. consumers first. - Each shipped release is archived (binaries, source bundle, libraries, toolchain, debug symbols) in the company Google Drive under Apollo Firmware Vault/M-1/. -- Build with env `apollo_m1`. Firmware changes need the virgin-boot gate and the - QA_CHECKLIST.md D13 AP walkthrough before release. +- Build with env `apollo_m1` (its libraries are pinned in platformio.ini; keep them pinned). + Firmware changes need the virgin-boot gate, the QA_CHECKLIST.md D13 AP walkthrough and an + OTA gate before release. +- Releases follow apollo/RELEASE_CHECKLIST.md: push the m1-bN tag first, then + `gh release create --verify-tag` (without it gh tags the default branch). Release + immutability is on, and m1-* tags cannot be moved or deleted. diff --git a/apollo/RELEASE_CHECKLIST.md b/apollo/RELEASE_CHECKLIST.md new file mode 100644 index 0000000000..e007d6344b --- /dev/null +++ b/apollo/RELEASE_CHECKLIST.md @@ -0,0 +1,77 @@ +# M-1 firmware release checklist (bundle m1-bN) + +Every step, in order. The install page and the FlashScripts flash script both serve +`M-1_full_install.bin` from the release, so a release is not done until both are +re-pointed and hash-checked. Nothing in this process touches upstream WLED. + +## 1. Scope and branch +- Work on a branch off the product branch (see CLAUDE.md, Branches) and merge by PR. +- Upstream changes come only from an upstream release tag. A cherry-pick from upstream + (for example a security fix before the next tag exists) is an explicit, recorded + exception: use `git cherry-pick -x` so the upstream commit is named. +- If apollo/fs changes, the first-boot look changes too: update FlashScripts LED_PASS text + (step 7) and the customer docs. + +## 2. Gates (on hardware, with the exact build you will ship) +- Virgin-boot gate (factory-erased unit, full install). +- QA_CHECKLIST.md D13 AP walkthrough. +- OTA gate: previous bundle (m1-bN-1) to this build through the WLED update page, with + settings and presets surviving. When WLED-MM units are still in the field, also repeat + QA_CHECKLIST.md D2 (WLED-MM 14.5.1 to this build). + +## 3. Build the artifacts +- Clean tree at the commit you gated: `git status` shows nothing to commit. +- `apollo/build_artifacts.sh`. On Apple silicon without Rosetta, mklittlefs cannot run: + if apollo/fs is unchanged since the last release, pass + `--littlefs ` (in the firmware vault). +- Keep everything in apollo/out: both binaries, littlefs.bin, firmware.elf, firmware.map, + bootloader.bin, partitions.bin, BUILD_INFO.txt, SHA256SUMS.txt. + +## 4. Tag (push the tag BEFORE creating the release) +``` +git tag -a m1-bN -m "Apollo M-1 firmware bundle bN (gated YYYY-MM-DD)" +git push origin refs/tags/m1-bN # never `git push --tags` +git ls-remote origin 'refs/tags/m1-bN^{}' # must print +``` +`m1-*` tags cannot be moved or deleted once pushed (repo ruleset), so check the commit first. + +## 5. GitHub release (immutable once published) +``` +gh release create m1-bN -R ApolloAutomation/WLED-M1 --verify-tag \ + --title "Apollo M-1 firmware, bundle bN" --notes-file \ + apollo/out/M-1_full_install.bin apollo/out/M-1_ota.bin +gh release view m1-bN -R ApolloAutomation/WLED-M1 --json assets --jq '.assets[] | .name + " " + .digest' +``` +- `--verify-tag` stops gh from tagging the default branch when the tag is missing. +- The asset digests must equal the two lines in apollo/out/SHA256SUMS.txt. +- Release immutability is on: assets cannot be replaced after publishing. A bad release + gets a new tag (m1-bN+1); the tag name cannot be reused. +- Publishing starts an "apollo-pages" run that fails on the github-pages branch policy. + That red run is expected; deploy in step 6. + +## 6. Install page (install.apolloautomation.com/#/m-1) +``` +gh workflow run apollo-pages.yml -R ApolloAutomation/WLED-M1 --ref main +``` +- It copies apollo/installer/manifest.json from `main` and `M-1_full_install.bin` from the + release marked Latest. If the WLED base version changed, update the manifest "version" + and the installer's devices.json label first. +- After the run succeeds (allow up to 10 minutes of CDN cache): + `curl -sL https://apolloautomation.github.io/WLED-M1/M-1_full_install.bin | shasum -a 256` + must equal the new full-install hash. + +## 7. FlashScripts (ApolloAutomation/FlashScripts, M-1/flash-prod.sh) +- In one PR: PINNED_URL (new tag), PINNED_SHA256, PINNED_VERSION, and LED_PASS / + LED_PASS_SHORT if the first-boot look changed. +- Verify: `curl -sL | shasum -a 256` equals PINNED_SHA256. + +## 8. Firmware vault +- Company Google Drive, Apollo Firmware Vault/M-1/m1-bN: everything from apollo/out, the + release notes, and `git bundle create WLED-M1-all.bundle --all`. +- `shasum -a 256 -c SHA256SUMS.txt` inside the Drive folder. + +## 9. Final check +- Install page: manifest returns 200 and the bin hash is the new one. +- FlashScripts PINNED_URL hash is the new one. +- WLED-MM-M1 manifests still return 200 (the installer still offers them). +- Customer docs and wiki match what the unit now does.