Skip to content

Publish flashable release artifacts - #13

Merged
PSi86 merged 2 commits into
mainfrom
publish-flashable-artifacts
Jul 30, 2026
Merged

Publish flashable release artifacts#13
PSi86 merged 2 commits into
mainfrom
publish-flashable-artifacts

Conversation

@PSi86

@PSi86 PSi86 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Work packages B1 and B2 from RaceLink_Docs/_private/plans/2026-07-30-release-factory-images-and-ota-preflight.md, matching what just landed in RaceLink_Gateway (PR #11 there).

Problem

Releases published PlatformIO's application image only. Commissioning a blank node therefore meant flashing stock WLED from install.wled.me and then pushing the RaceLink image through /update with Ignore firmware validation ticked — a detour that routes the least experienced users through the most dangerous control in WLED's UI, because that checkbox disables all payload validation.

What ships now

Per environment: the application image, the three pre-application images (bootloader, partition table, boot_app0), and a merged factory image covering all four — plus a SHA-256 manifest and an …-assets.json sidecar giving environment, chip, device type, kind, flash offset, size and digest for every file.

RaceLink_WLED-<version>-<env>-TYPE<n>-wled_<ref>-app.bin
RaceLink_WLED-<version>-<env>-TYPE<n>-wled_<ref>-factory-usb-serial-only.bin
RaceLink_WLED-<version>-<env>-bootloader.bin
RaceLink_WLED-<version>-<env>-partitions.bin
RaceLink_WLED-<version>-<env>-boot_app0.bin
RaceLink_WLED-<version>-sha256.txt
RaceLink_WLED-<version>-assets.json

Each asset now states both versions — its own RaceLink release and the WLED release it wraps — and the DEV_TYPE the master looks up rather than leaving it to be inferred.

This also retires the old suffix handling. "".join(Path.suffixes) was meant to preserve .bin versus .bin.gz, but Path.suffixes returns every dot-separated segment after the first dot, and the upstream version contains dots — so WLED_0.15.3_RaceLink_Node_V4_TYPE_12.bin folded the whole release name into the "extension". (.gz only ever applied to ESP8266, which this repo does not build.) Nothing consumes these names: ota_service.py uploads whatever file the operator picks, and WLED's /update ignores filenames.

Shared with RaceLink_Gateway

scripts/release_artifacts.py and scripts/release_staging.py are byte-identical to their gateway counterparts. Both repositories publish the same asset shapes, and this is where a mistake produces an image that flashes cleanly and never boots — so there is one implementation rather than two.

  • Offsets come from PlatformIO's project metadata, not a per-SoC table.
  • The chip handed to esptool is read out of the ESP image header of the binary being merged.
  • The application offset comes from the generated partition table that ends up inside the same factory image.
  • A per-chip table remains as a cross-check: staging aborts if the two sources disagree, and refuses to guess for an unmapped SoC. That guard earns its keep here more than in the gateway — the shipping set spans C3, S2 and S3, and the S2 bootloader sits at 0x1000 rather than 0x0.

Metadata is collected inside the per-profile loop, while that profile's platformio_override.ini is still staged; the next iteration overwrites it.

build.yml — compile-only CI

release.yml was the only thing that ever built this repo, and it tags and publishes in the same run. The build-and-stage loop moved into scripts/build_and_stage_profiles.sh, which both workflows call — so the pull-request rehearsal and the real release cannot drift, and release day differs only in the version string.

The new workflow builds all six shipping profiles against the latest published WLED release, verifies each factory image structurally, uploads the staged assets so a PR can be flashed on hardware, and publishes nothing.

B2 — v7 omission is now self-documenting

RaceLink_Node_v7_classic_esp32_emac stays out of SHIPPING_PROFILE_FILENAMES until its Ethernet support ships. The comment records that, and notes it will be the first classic ESP32 in the set — the 0x1000 case the cross-check exists to catch.

Verification

  • 45 static tests pass.
  • The staging code is the same code, byte for byte, that produced verified factory images in RaceLink_Gateway — where one was flashed onto a fully erased Heltec Wireless Stick V3 and booted (see PR Release workflow: build the full shipping profile list (incl. v5/v6) #11 there).
  • This PR's CI run is the first exercise of the C3 and S2 paths, including the 0x1000 bootloader offset. That is the thing to check before merging.

Not covered here

No hardware flash test on a node yet. The build artifact contains factory images for all six profiles, so a node can be commissioned from this PR directly.

PSi86 added 2 commits July 30, 2026 18:59
Releases published the application image only; the second-stage bootloader and
the partition table stayed in the build directory. Commissioning a blank node
therefore meant flashing stock WLED from install.wled.me and then pushing the
RaceLink image through /update with "Ignore firmware validation" ticked -- a
detour that routes the least experienced users through the most dangerous
control in WLED's UI, because that checkbox disables all payload validation.

Stage, per environment: the application image, the three pre-application
images, and a merged factory image covering all four, plus a SHA-256 manifest
and an assets.json sidecar giving environment, chip, device type, kind, flash
offset, size and digest for every file.

release_artifacts.py and release_staging.py are byte-identical to their
RaceLink_Gateway counterparts. Both repositories publish the same asset shapes,
and this is where a mistake produces an image that flashes cleanly and never
boots, so there is one implementation of it rather than two. Offsets come from
PlatformIO's metadata, the chip is read out of the image being merged, and the
application offset comes from the generated partition table -- with a per-chip
cross-check that aborts if the two sources disagree. That guard matters more
here than in the gateway: the shipping set spans C3, S2 and S3, and the S2
bootloader sits at 0x1000 rather than 0x0.

Metadata has to be collected inside the per-profile loop, while that profile's
platformio_override.ini is still staged -- the next iteration overwrites it.

Naming now states both versions, the RaceLink release and the WLED release it
wraps, and the device type the master looks up:

  RaceLink_WLED-0.1.8-RaceLink_Node_v4_s3_llcc68-TYPE12-wled_v0.15.3-app.bin

This also retires the old suffix handling, which joined Path.suffixes to
preserve ".bin" but folded the whole WLED release name into the extension,
because the upstream version contains dots. Nothing consumes these names: the
host uploads whatever file the operator picks and WLED's /update ignores
filenames.

The build-and-stage loop moved into scripts/build_and_stage_profiles.sh so the
new build.yml can rehearse the entire release on every pull request -- six
profiles, staged, merged and verified, publishing nothing.

Also records why RaceLink_Node_v7_classic_esp32_emac stays out of the shipping
set: the omission was indistinguishable from an oversight.
`pio project metadata` cleans the build directory, so collecting it after
`pio run` deleted the firmware.bin the staging step was about to read. The
trigger is visible in the failing run: the metadata pass resolved and installed
a dependency the build had not, which changes PlatformIO's project checksum and
makes it wipe the build directory.

Everything the metadata reports is derived from the configuration -- the flash
offsets, and paths like $BUILD_DIR/bootloader.bin -- so the files it names do
not have to exist yet, and collecting it first also means any dependency
resolution happens before the build rather than invalidating it afterwards.
@PSi86

PSi86 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

CI verification

The first run failed and the failure was worth having: pio project metadata cleans the build directory, so collecting it after pio run deleted the firmware.bin the staging step was about to read. The trigger is visible in that run — the metadata pass resolved and installed a dependency the build had not (Library Manager: Installing SPI), which changes PlatformIO's project checksum and makes it wipe the build directory.

Fixed by collecting metadata before the build. Everything it reports is derived from the configuration — the offsets, and paths like $BUILD_DIR/bootloader.bin — so the files it names need not exist yet, and any dependency resolution now happens before the build rather than invalidating it afterwards. A test pins the ordering.

Note for RaceLink_Gateway: its workflow has the same ordering and passes only because every dependency was already installed when the metadata pass ran. That is luck, not design, and should be reordered there too.

Result — all six profiles, built against WLED v16.0.1

Environment Chip DEV_TYPE Factory image
RaceLink_Node_v1_c3_ct62 esp32c3 10 1188576 B
RaceLink_Node_v3_s2_llcc68 esp32s2 11 1233600 B
RaceLink_Node_v3_s2_llcc68_epaper esp32s2 50 1266448 B
RaceLink_Node_v4_s3_llcc68 esp32s3 12 1246192 B
RaceLink_Node_v5_s3_eth esp32s3 13 1221504 B
RaceLink_Node_v6_s3_heltec_wpaper esp32s3 51 1276896 B

Each verified structurally: bootloader, partition table and application present at their declared offsets, and the embedded application byte-identical to the published application image. 32 assets staged in total; 12m22s.

The offset cross-check earns its keep

Read back from the staged manifest:

RaceLink_Node_v1_c3_ct62           esp32c3  bootloader=0x0000  partitions=0x8000  app=0x10000
RaceLink_Node_v3_s2_llcc68         esp32s2  bootloader=0x1000  partitions=0x8000  app=0x10000
RaceLink_Node_v3_s2_llcc68_epaper  esp32s2  bootloader=0x1000  partitions=0x8000  app=0x10000
RaceLink_Node_v4_s3_llcc68         esp32s3  bootloader=0x0000  partitions=0x8000  app=0x10000
RaceLink_Node_v5_s3_eth            esp32s3  bootloader=0x0000  partitions=0x8000  app=0x10000
RaceLink_Node_v6_s3_heltec_wpaper  esp32s3  bootloader=0x0000  partitions=0x8000  app=0x10000

PlatformIO reports 0x1000 for the S2 targets and 0x0 for C3 and S3, and the per-chip table agrees in every case — so all three SoC families are now exercised rather than assumed. Hard-coding 0x0, which is what the gateway-only view would have suggested, would have produced two unbootable factory images.

Names in practice

RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-TYPE10-wled_v16.0.1-app.bin
RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-TYPE10-wled_v16.0.1-factory-usb-serial-only.bin
RaceLink_WLED-0.0.0-ci-RaceLink_Node_v1_c3_ct62-bootloader.bin

(0.0.0-ci is the rehearsal's placeholder version; a release uses the real one.)

Still not covered

No hardware flash test on a node. The build artifact holds factory images for all six profiles, so one can be commissioned from this PR directly.

@PSi86
PSi86 merged commit 4ab4b8a into main Jul 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant