Build each platform natively and drop the layer cache - #9
Merged
Conversation
Two changes, both measured rather than assumed. Runners. Every build job moves to a larger-runner pool picked by platform through a `runner` key in the matrix include, so linux/arm64 builds on arm64 hardware instead of QEMU on an x86 host. The emulated esp-matter leg took 5 h 37 min against 24 min for the native amd64 one - a 14x penalty, and it had grown 36 minutes in the eight months since the previous measurement, against a `timeout-minutes: 360` ceiling it was 23 minutes short of hitting. Native builds land around 15-25 min, so that ceiling drops to 120: wide enough to be a cap on a wedged job rather than a target. The pool size is chosen for disk, not cores. connectedhomeip needs ~50 GB, and `runner-smoke.yml` measured 372 GB free under Docker on ubuntu-latest-8core and 393 GB on ubuntu-latest-8core-arm, against 88 GB on 4-core. Manifest jobs go to ubuntu-latest-4core - they only call GHCR. This also retires the self-hosted incus runners from the workflows. They are 4 cores and 7.3 GiB RAM, weaker than a free ubuntu-latest, and both of their legs ran on x86 - so they never provided native arm64, which is the entire point here. Cache. cache-from/cache-to are removed from all six build steps. Measured across every master run: zero cache hits, against 21.6 minutes per run spent writing the cache - for esp-idf, 390 s of export on a 72 s build. mode=max wrote ~3.9 GiB per leg into a repository-wide 10 GB quota, so LRU evicted entries during the run that created them; esp-idf-linux-arm64 was left with an index whose every blob was already gone, which imports successfully and yields nothing. Green 18-second PR legs were an artefact of push: false, where CACHED is decided on the manifest and no blob is ever materialised. The Dockerfile SHELL comments said bash was needed for QEMU compatibility. Nothing is emulated now, and the real reason - these layers call the bash builtin `source` - always was the one that mattered.
…docs codex-review and the code-review workflow found eight defects between them. Forks. The build jobs kept a `workflow_dispatch` escape hatch while moving to pools that exist only in jethome-iot, so the documented fork path would have queued for 24 hours instead of building - the exact failure this series is meant to prevent, and one we hit for real on the first probe run. Forks now run nothing: the job gate is the owner alone, and a fork builds with ./scripts/build.sh. That also collapses the publish gates from three overlapping expressions to two questions asked once each - who (the job `if`) and where (login and `push:`). esp-matter timeout. 120 was extrapolated from the amd64 measurement; the arm64 leg has never run outside QEMU, so its per-core speed on connectedhomeip is unknown. Set to 180 until two runs report real numbers, because getting it wrong downward fails quietly: the leg is killed, the manifest job is skipped with it, and the amd64 platform tag has already been overwritten - leaving GHCR with a platform tag newer than the manifest pointing at it. Manifest jobs stay on ubuntu-latest rather than moving to a pool. They are seconds of GHCR calls, so a pool buys nothing, and they are the only jobs that publish a multi-arch tag: pointing them at a pool makes tag publication hostage to that pool existing and still being granted to this repository. Documentation that this series had made false. CLAUDE.md still justified the mandatory verification RUN layer by emulation no-ops three lines after stating that nothing is emulated, and still quoted a Dockerfile comment this branch had already rewritten. The layer's real justification is broader than QEMU - a stale mirror or an installer exiting 0 on a partial install produce the same silently green build - and it is now written that way. Also recorded, because nothing checks it: a `platform:` value with no matching `include:` entry leaves matrix.runner empty, `runs-on` evaluates to the empty string, and the leg never starts. Verified that actionlint - the image lint.yml runs - passes that edit; it catches a wrong label, not a missing key. PR-3's generated matrices are what will make this checkable.
There was a problem hiding this comment.
Pull request overview
Updates the Docker image build workflows to run linux/amd64 and linux/arm64 builds on native runner pools (no QEMU) and removes the GitHub Actions layer cache configuration, with accompanying documentation updates.
Changes:
- Switch ESP-IDF/ESP-Matter and PlatformIO build jobs to
runs-on: ${{ matrix.runner }}with per-platform runner selection. - Remove
cache-from/cache-tofrom docker/build-push-action steps to stop exporting an ineffective GHA cache. - Update README/CLAUDE and Dockerfile comments to reflect the new non-emulated, owner-only workflow behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents that forks won’t run Actions jobs and directs fork users to ./scripts/build.sh. |
| images/esp-idf/Dockerfile | Clarifies why bash is required for RUN layers (source), not QEMU. |
| images/esp-matter/Dockerfile | Same clarification for bash SHELL rationale. |
| CLAUDE.md | Updates CI guidance to match new runner-pool + no-cache behavior and workflow gating. |
| .github/workflows/esp-idf.yml | Moves ESP-IDF + ESP-Matter builds to native runner pools and removes cache config (but matrix include structure needs correction). |
| .github/workflows/platformio.yml | Moves PlatformIO builds to native runner pools and removes cache config (but matrix include structure needs correction). |
Suppressed comments (1)
.github/workflows/esp-idf.yml:170
- Same matrix issue as esp-idf-build: the
includeentries don’t specify both axes (version+platform), so the build legs can end up with missingrunner/jethome_idf_base_tag/esp_matter_versionvalues. That can breakruns-on, tag generation, and the build args passed to the Dockerfile.
matrix:
version: ['idf-v5.4.1-matter-v1.4.2']
platform: ['linux/amd64', 'linux/arm64']
include:
- version: 'idf-v5.4.1-matter-v1.4.2'
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The measured problem was never runner power — it was emulation, and a cache that
cost 21.6 minutes per run to produce zero hits.
Native builds
Every build job now picks its runner by platform through a
runnerkey in thematrix
include:, solinux/arm64builds on arm64 hardware instead of QEMU on anx86 host.
linux/amd64linux/arm64The emulated leg was a 14× penalty and it was growing — 5 h 01 min in November
2025, 5 h 37 min in July 2026, on the same Matter 1.4.2 — against a
timeout-minutes: 360ceiling it was 23 minutes short of hitting.Pool size is chosen for disk, not cores. connectedhomeip needs ~50 GB, and
runner-smoke.ymlmeasured 372 GB free under Docker onubuntu-latest-8coreand 393 GB on
ubuntu-latest-8core-arm, against 88 GB on 4-core.esp-matter-build's ceiling goes to 180 rather than something tighter: the onlynative esp-matter figure that exists is the amd64 one, and killing the arm64 leg
fails quietly — the manifest job is skipped with it while the amd64 platform tag
has already been overwritten, leaving GHCR with a platform tag newer than the
manifest pointing at it. Lower it once two runs have reported real numbers.
Manifest jobs stay on
ubuntu-latest. They are seconds of GHCR calls, so a poolbuys nothing, and they are the only jobs that publish a multi-arch tag — pointing
them at a pool would make tag publication hostage to that pool still existing and
still being granted to this repository.
This also retires the self-hosted incus runners from the workflows. They are 4
cores and 7.3 GiB RAM — weaker than a free
ubuntu-latest— and both of theirlegs ran on x86, so they never provided native arm64 at all. Their registration is
left alone for now; an idle self-hosted runner costs nothing and is a fallback
until this has run on master a couple of times.
No layer cache
cache-from/cache-toare gone from all six build steps. Measured across everymaster run:
grep -c CACHED= 0 in six checked jobs)on a 72 s build
mode=maxwrote~3.9 GiB per leg, so LRU evicted entries during the run that created them —
esp-idf-linux-arm64was left with an index whose every blob was already gone,which imports successfully and yields nothing
Green 18-second cache hits on PR legs were an artefact of
push: false:CACHEDis decided on the manifest and no blob is ever materialised, so the PR said the
cache worked while master got nothing.
A registry-backed cache in GHCR is the sensible replacement, but it cannot be
measured yet: esp-matter builds
FROMesp-idf, and any esp-idf rebuildinvalidates every layer below it — so that goes after the workflows are split.
Forks run nothing
The build jobs kept a
workflow_dispatchescape hatch while moving to pools thatexist only in
jethome-iot. A fork following the documented path would have got a24-hour queue rather than a build — the exact failure mode this series exists to
prevent, and one we hit for real on the first probe run. Forks now build with
./scripts/build.sh.That also collapses the publish gates from three overlapping expressions into two
questions asked once each: who (the job
if, owner only) and where (loginand
push:, master only).Verification
./scripts/lint.shgreen throughout./scripts/build.sh esp-idfand./scripts/build.sh platformiobuilt locally,verification layers reporting
ESP-IDF v5.4.1andjq-1.7What a PR cannot check here:
esp-matter-buildneedsesp-idf-manifest, which ismaster-only, so the first native esp-matter build happens after this merges. That
gap is what PR-4 closes.
Reviewed by
codex-review(xhigh) and thecode-reviewworkflow (high) — eight defects, allapplied or recorded. The two that changed the design are above (forks, and the
manifest jobs staying on
ubuntu-latest). One is recorded rather than fixed: aplatform:value with no matchinginclude:entry leavesruns-onempty and theleg silently never starts, and actionlint passes it — verified against the image
lint.ymlruns. PR-3's generated matrices make that checkable.