Hand images between jobs by digest, not by tag - #12
Merged
Conversation
The manifest jobs assembled a multi-arch image out of tag *names*, which they looked up when they ran rather than receiving from the build legs that produced them. Two pushes to master overlapping - now a routine shape, since esp-matter runs for minutes after esp-idf finishes - and the manifest of run A publishes what run B overwrote, under `sha-<commit-of-A>`. The one tag that is supposed to mean "this exact commit" was the one that could lie. Build legs now push by digest and carry no tag at all. Digests reach the manifest job as one empty file per leg, named after the digest, through an artifact - matrix legs cannot each set a job output, they overwrite one another - and the manifest is assembled from `<image>@sha256:<digest>` references. Nothing in between is a name that something else could rebind. The `-linux-<arch>` tags disappear from GHCR with this; they were documented as "not intended for direct use" and existed only as this handoff. esp-idf-manifest now exports its own digest, and esp-matter-build builds FROM that digest instead of `idf-<version>`. Before this, a long esp-matter build could be sitting on a base that a later run had already replaced, and the two halves of one multi-arch `latest` could come from different bases. That moves the Dockerfile from `ARG BASE_IMAGE_TAG` to a single `ARG BASE_IMAGE` carrying the whole reference, because a digest needs `@` where a tag needs `:` - and it makes the base repository overridable, so a fork or a local build can point at its own esp-idf rather than this one. Verified both ways locally: the default resolves, and --build-arg overrides what FROM sees. Both workflows also gain a concurrency group, queuing on master rather than cancelling: a cancelled run leaves images pushed by digest with no manifest pointing at them. Pull requests do cancel - there is nothing to leave behind, and superseded runs are not worth paying for.
…ture The code-review workflow found ten defects, six confirmed. Three were mine to fix in the mechanism, three in documentation this same branch had made false. concurrency did not do what its comment said. `cancel-in-progress: false` is not a queue: GitHub cancels a *pending* run in a group whenever a newer one arrives, regardless of that flag - confirmed against the workflow-syntax reference. Grouping master pushes by ref would silently drop the middle commit of any three landing inside one build window, and its sha-<commit> image, which the READMEs document as the way to pin an exact commit, would never exist. The key now carries github.sha on a push, so every commit has its own group and cancels nothing; pull requests still group by ref and cancel, where there is nothing published to lose. The digest capture ran `imagetools inspect | jq` with no pipefail. jq exits 0 on empty input, so a transient GHCR failure would have produced an empty digest, a green manifest job, and a failure surfacing 180 minutes later inside esp-matter-build as `invalid reference format` - pointing nowhere near the job that actually broke. Now under `set -euo pipefail`, with `jq -e` and an explicit sha256: check. The manifest jobs globbed whatever digest files were present. With fail-fast: false, one failed leg would leave a single file and publish a single-platform image under latest and the version tag. Each manifest job now asserts it got one digest per platform first. Two latent traps that a second version in the matrix would have sprung, which is exactly what PR-5 adds: the esp-idf manifest digest travelled as a scalar job output from a matrix job - the overwrite hazard the build job's own comment warns about - and the digest artifact names omitted the version, so two versions would collide under a version-agnostic download pattern. The digest is now an artifact named after its version, and esp-matter-build downloads the one matching the ESP-IDF version in its own tag. That also binds base to tag: a half-done bump now fails on a missing artifact instead of publishing an idf-v<old> tag built on v<new>. Documentation: CLAUDE.md still described esp-matter as FROM a hardcoded jethome-iot tag, still explained a half-bumped version through tags that no longer exist, still named `push:` as a top-level key after it moved into the outputs CSV, and both the workflow header and the esp-matter timeout rationale still reasoned about platform tags this branch deleted.
There was a problem hiding this comment.
Pull request overview
This PR hardens the Docker image publishing workflows by handing off build outputs between matrix legs and manifest jobs via immutable image digests (artifacts), instead of mutable tag names that can be overwritten by overlapping runs.
Changes:
- Update
esp-idf.ymlandplatformio.ymlto push per-platform images by digest (no intermediate tags), upload those digests as artifacts, and assemble multi-arch manifests from@sha256:references. - Pin
esp-matterbuilds to the exact ESP-IDF base image produced in the same run by passing a fullBASE_IMAGEreference (including digest) intoFROM. - Remove documentation of
*-linux-<arch>tags from image READMEs and update repo guidance to reflect digest-based handoff and revised build args.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
.github/workflows/esp-idf.yml |
Switch ESP-IDF + ESP-Matter workflows to digest artifact handoff, manifest assembly by digest, and base digest propagation to ESP-Matter. |
.github/workflows/platformio.yml |
Switch PlatformIO workflow to digest artifact handoff and manifest assembly by digest. |
images/esp-matter/Dockerfile |
Replace tag-shaped base arg with full BASE_IMAGE reference used directly in FROM. |
images/esp-matter/README.md |
Document BASE_IMAGE build arg and add local-build-on-local-base example; drop platform-specific tag docs. |
images/esp-idf/README.md |
Drop platform-specific tag docs now that per-arch tags are no longer published. |
images/platformio/README.md |
Drop platform-specific tag docs now that per-arch tags are no longer published. |
CLAUDE.md |
Update repo CI guidance to match digest-based workflow behavior and new base pinning approach. |
💡 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 manifest jobs assembled a multi-arch image out of tag names, looked up when
they ran rather than received from the build legs that produced them:
Two pushes to master overlapping — a routine shape, since esp-matter runs for
minutes after esp-idf finishes — and run A's manifest publishes what run B
overwrote, under
sha-<commit-of-A>. The one tag that is supposed to mean thisexact commit was the one that could lie.
Digest handoff
Build legs push by digest and carry no tag at all:
The digests travel to the manifest job as one empty file per leg, named after the
digest, through an artifact — matrix legs cannot each set a job output, they
overwrite one another. The manifest is then built from
@sha256:references, sonothing in between is a name something else could rebind.
The
-linux-<arch>tags disappear from GHCR with this. They existed only as thishandoff and were already documented as "not intended for direct use"; the tag
tables in the three image READMEs drop that row.
esp-matter builds on a pinned base
esp-idf-manifestexports its own digest as a job output, andesp-matter-buildconsumes it:
Before this, an esp-matter build could be sitting on a base that a later run had
already replaced, and the two halves of one multi-arch
latestcould come fromdifferent bases.
That turns
ARG BASE_IMAGE_TAGinto a singleARG BASE_IMAGEcarrying the wholereference, because a digest needs
@where a tag needs:. It also stopshardcoding the owner in
FROM, so a fork — or a local build — can point at its ownesp-idf:
Concurrency
Queue on master, cancel on pull requests. Cancelling a master run would leave
images pushed by digest with no manifest pointing at them.
Verification
./scripts/lint.shgreen — and it caughtSC2046in the first draft of themanifest step, where the digest list relied on word splitting; it is an explicit
array now
ARG BASE_IMAGEproven both ways locally: the Dockerfile default resolves, and--build-arg BASE_IMAGE=jethome-nonexistent-base:proofmakes Docker resolvemetadata for exactly that reference, so the argument does reach
FROMthe PR checks exercise the build legs with
push: falseReviewed by
codex-review(xhigh) — no findings; it confirms the handoff follows the standardBuildx multi-platform pattern.
The
code-reviewworkflow (high) — ten defects, six confirmed, all applied:concurrencydid not queue.cancel-in-progress: falseis not a queue —GitHub cancels a pending run in a group whenever a newer one arrives, whatever
that flag says. Grouping master pushes by ref would have dropped the middle
commit of any three landing inside one build window, and its
sha-<commit>image. The key now carries
github.shaon a push, so every commit has its owngroup; pull requests still group by ref and cancel.
imagetools inspect | jqhad nopipefail.jqexits 0 on empty input, soa transient GHCR failure would have produced an empty digest, a green manifest
job, and a failure surfacing much later inside
esp-matter-buildasinvalid reference format, pointing nowhere near the job that broke.fail-fast: false, one failed leg leaves a single file — and would publish asingle-platform image under
latest. Each manifest job now asserts one digestper platform first.
The base digest travelled as a scalar output from a matrix job (the overwrite
hazard the build job's own comment warns about), and artifact names omitted the
version, so two versions would collide under a version-agnostic download
pattern. Both fixed, and the fix binds base to tag: a half-done bump now fails on
a missing artifact instead of publishing
idf-v<old>built onv<new>.CLAUDE.mdstill describedesp-matter as
FROMa hardcoded tag, still explained a half-bumped versionthrough tags that no longer exist, and still named
push:as a top-level key.