Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ before changing anything here.
its own tag, so base and advertised version cannot drift apart. It reaches the
Dockerfile through a single `BASE_IMAGE` build-arg — one argument rather than
repo + tag, because a digest needs `@` where a tag needs `:`, and because it makes
the base repository overridable for a fork or a local build.
the base repository overridable for a fork or a local build. A digest names no
version, so the ESP-IDF version travels separately as `IDF_VERSION` in `args`,
taken from the same `base_tag`; both ESP images label it as
`dev.jethome.idf.version` and both assert it against `idf.py --version`, which is
what keeps a label a consumer reads from the registry from outliving the base it
describes.
- **`images/versions.json` is the single source of truth for what CI builds.** A
`prepare` job runs `scripts/check-versions.sh`, then `scripts/versions-matrix.sh
<image> build|manifest`, and every other job takes its matrix from
Expand Down Expand Up @@ -360,7 +365,11 @@ Four places, none of them checked automatically:
the image afterwards and only applies to a single named image. Building
`esp-matter` this way pulls its base from GHCR — to build it on an esp-idf you
just built, pass the base explicitly:
`docker build --build-arg BASE_IMAGE=jethome-dev-esp-idf:local images/esp-matter`.
`docker build --build-arg BASE_IMAGE=jethome-dev-esp-idf:local --build-arg
IDF_VERSION=v<idf-version> images/esp-matter` (the leading `v`, as the SDK
reports it — the assertion compares the two literally) — the second arg because the version is
asserted against the base, and its default belongs to the *published* base
rather than to whatever you just built.
Note that a version-bump branch cannot use the Dockerfile default at all: it
names a tag that only exists once the branch lands.
- There is no local workflow runner. Workflow changes are validated by pushing the
Expand Down
30 changes: 29 additions & 1 deletion images/esp-idf/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ ARG IDF_BASE_TAG=v5.5.5

FROM espressif/idf:${IDF_BASE_TAG}

# Re-declared after FROM: an ARG stated before it is out of scope below, so the
# LABEL would otherwise expand to the empty string - silently, since Docker does
# not object to an undefined variable.
ARG IDF_BASE_TAG

# The ESP-IDF version this image carries, readable from the registry without
# pulling several gigabytes to find out. The tag says it too, but a consumer that
# pins by digest - the only pin that cannot be moved under it - deliberately does
# not keep the tag, so the tag is exactly what it cannot read. The verification
# layer at the bottom holds this to what the SDK itself reports, so it is a fact
# about the image rather than a claim it makes.
LABEL dev.jethome.idf.version="${IDF_BASE_TAG}"

# Use bash for all RUN commands: the layers below call the bash builtin `source`,
# which /bin/sh does not have. (This once said "needed for QEMU emulation
# compatibility" - CI no longer emulates anything, but the reason above always was
Expand Down Expand Up @@ -68,6 +81,13 @@ WORKDIR /workspace
# The freeze is written to a file rather than piped, so the image carries its own
# version snapshot and no pipe can swallow a failure.
#
# The idf.py line is an assertion too, and for a reason of its own: the
# dev.jethome.idf.version label near the top of this file is what a consumer reads
# instead of pulling several gigabytes, and a label is only worth reading if the
# image cannot publish one that contradicts itself. Compared for equality against
# what the SDK reports (`ESP-IDF v5.5.5`), so a tag naming one version while the
# base carries another fails here rather than downstream.
#
# The esptool line is an assertion, not a listing: `grep -E '^esptool'` would pass
# on any version, and "any version" is exactly the bug - ESP-IDF's own environment
# ships 4.12.0 and anything reinstalling it would go unnoticed. It repeats the
Expand All @@ -82,7 +102,15 @@ RUN export IDF_PATH_FORCE=1 && \
grep -qx 'esptool==5.3.1' /opt/esp/python-packages.txt && \
grep -E '^(pytest|esptool)' /opt/esp/python-packages.txt && \
jq --version && \
idf.py --version && \
idf_reported="$(idf.py --version)" && \
echo "${idf_reported}" && \
{ if [[ "${IDF_BASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then \
[ "${idf_reported}" = "ESP-IDF ${IDF_BASE_TAG}" ] \
|| { echo "expected 'ESP-IDF ${IDF_BASE_TAG}', got '${idf_reported}'" >&2; \
exit 1; }; \
else \
echo "IDF_BASE_TAG '${IDF_BASE_TAG}' is not an exact version - label not asserted"; \
fi; } && \
echo "ESP-IDF installation verified successfully"

CMD ["/bin/bash"]
Expand Down
37 changes: 37 additions & 0 deletions images/esp-idf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,43 @@ tag itself is rewritten on every rebuild.
- **CI/CD**: Use version tags (`idf-v<version>`) for reproducibility
- **Debugging**: Use commit tags (`sha-<short-commit>`) to reproduce exact build

### Reading the ESP-IDF version without pulling the image

A consumer that pins by digest — the only pin nothing can move underneath it —
does not keep the tag, so the tag is the one place it cannot read the version
from. The image carries it as a label instead, readable straight from the
registry:

```bash
docker buildx imagetools inspect \
ghcr.io/jethome-iot/jethome-dev-esp-idf@sha256:<digest> \
--format '{{ index (index .Image "linux/amd64").Config.Labels "dev.jethome.idf.version" }}'
```

The platform key is not decoration: these tags are multi-arch indexes, so
`imagetools` hands the template a map of one image per platform rather than a
single image, and `.Image.Config` on an index is a template error rather than an
answer. Both platforms carry the same label; pick either.

Locally, on an image **already pulled** (this one reads the local daemon, not the
registry):

```bash
docker image inspect \
--format '{{index .Config.Labels "dev.jethome.idf.version"}}' <image>
```

The label is not a claim the image makes about itself: its build fails unless
`idf.py --version` inside the image reports exactly that version, so the cheap
answer above and the expensive one (`docker run … idf.py --version`) cannot
disagree.

That holds for every published image, since CI builds each one from an exact
version. A local build given an alias — `IDF_BASE_TAG=latest`, or a minor-line
tag like `v5.3` — cannot be checked that way (an alias never equals the release
it resolves to), so the build says the assertion was skipped and the label
repeats the alias. Publish exact versions; aliases are a local convenience.

### Pull Image

```bash
Expand Down
23 changes: 21 additions & 2 deletions images/esp-matter/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@ ARG ESP_MATTER_VERSION=v1.6
# rebuild reproduce the same tree instead of picking up whatever landed upstream.
# Update with ./scripts/update-matter-ref.sh.
ARG ESP_MATTER_REF=2c125619610b9a16bb8e92f5ebbf55111638615e
# The ESP-IDF version underneath, for the label and - the actual reason it is an
# arg - for the assertion. The label alone would need nothing: Docker inherits a
# base image's LABELs, so building on this repository's esp-idf already carries
# `dev.jethome.idf.version` down. What inheritance cannot do is *check* it, and it
# carries nothing at all when BASE_IMAGE points at some other image. Since CI
# passes that base as a digest, and a digest names no version, the value comes
# from images/versions.json instead - taken there from the same `base_tag` the
# digest was resolved from, with the checker refusing a value the variant's own
# tag does not name.
ARG IDF_VERSION=v5.5.5

FROM ${BASE_IMAGE}

# Re-declare build arguments after FROM
ARG ESP_MATTER_VERSION
ARG ESP_MATTER_REF
ARG IDF_VERSION

# Use bash for all RUN commands: the layers below call the bash builtin `source`,
# which /bin/sh does not have. (This once said "needed for QEMU emulation
Expand Down Expand Up @@ -63,7 +74,8 @@ RUN git init esp-matter && \

# Answers "what is actually in here" without starting the container.
LABEL org.opencontainers.image.revision="${ESP_MATTER_REF}" \
org.opencontainers.image.version="${ESP_MATTER_VERSION}"
org.opencontainers.image.version="${ESP_MATTER_VERSION}" \
dev.jethome.idf.version="${IDF_VERSION}"

# Install ESP-Matter (without host tools to reduce image size)
# Source ESP-IDF environment first to activate Python venv with pip
Expand Down Expand Up @@ -104,7 +116,14 @@ RUN source ${IDF_PATH}/export.sh && \
{ [ -z "${base_esptool}" ] && echo "no base snapshot - esptool not asserted" \
|| grep -qx "${base_esptool}" /opt/esp/python-packages.txt; } && \
grep -E '^(pytest|esptool)' /opt/esp/python-packages.txt && \
idf.py --version && \
idf_reported="$(idf.py --version)" && \
echo "${idf_reported}" && \
# The inherited SDK, held to the version this image labels. IDF_VERSION arrives as
# a build arg because BASE_IMAGE is a digest and says nothing about the version -
# so without this the label would be the one thing here nobody checks.
{ [ "${idf_reported}" = "ESP-IDF ${IDF_VERSION}" ] \
|| { echo "expected 'ESP-IDF ${IDF_VERSION}', got '${idf_reported}'" >&2; \
exit 1; }; } && \
Comment thread
hacker-cb marked this conversation as resolved.
echo "ESP-Matter ${ESP_MATTER_VERSION} installation verified successfully"

# Use custom entrypoint to activate environments (like entrypoint.sh in esp-idf image)
Expand Down
35 changes: 33 additions & 2 deletions images/esp-matter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ Two things worth knowing before pinning:
docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' <image>
```

The ESP-IDF underneath is readable the same way, on an image already pulled:

```bash
docker image inspect --format '{{index .Config.Labels "dev.jethome.idf.version"}}' <image>
```

And from the registry, with nothing pulled at all — the platform key is
required, because these tags are multi-arch indexes and `imagetools` then hands
the template one image per platform rather than a single one:

```bash
docker buildx imagetools inspect ghcr.io/jethome-iot/jethome-dev-esp-matter@sha256:<digest> \
--format '{{ index (index .Image "linux/amd64").Config.Labels "dev.jethome.idf.version" }}'
```

That one is checked rather than declared: the build fails unless `idf.py
--version` in the finished image reports exactly the labelled version, so it
cannot drift from the base the image was actually built on.

### Pull Image

```bash
Expand Down Expand Up @@ -276,8 +295,9 @@ docker build -t jethome-dev-esp-matter:local .

```bash
docker build \
--build-arg BASE_IMAGE=ghcr.io/jethome-iot/jethome-dev-esp-idf:idf-v<version> \
--build-arg ESP_MATTER_VERSION=v<version> \
--build-arg BASE_IMAGE=ghcr.io/jethome-iot/jethome-dev-esp-idf:idf-v<idf-version> \
--build-arg IDF_VERSION=v<idf-version> \
--build-arg ESP_MATTER_VERSION=v<matter-version> \
-t jethome-dev-esp-matter:local .
Comment thread
Copilot marked this conversation as resolved.
```

Expand All @@ -289,6 +309,16 @@ Available build arguments:
digest fits: `…/jethome-dev-esp-idf@sha256:…`, which is what CI passes to pin the
exact base its own run produced. It also lets a fork build against its own base
instead of this repository's.
- `IDF_VERSION` - the ESP-IDF version that base carries, for the
`dev.jethome.idf.version` label. Same spelling the SDK itself reports, leading
`v` included (`v5.5.5`), and distinct from `ESP_MATTER_VERSION` — the two are
different versions and the placeholders above keep them apart. **Pass it whenever `BASE_IMAGE` is not the
default**: it does not follow the base, so a build on `idf-v<other>` would
otherwise assert the default version and fail — after the SDK install, which is
the expensive half of the build. It is passed rather than derived because
`BASE_IMAGE` is a digest and a digest names no version; the build asserts the
value against what `idf.py --version` reports, so passing the wrong one fails
the build instead of publishing a label that lies.
- `ESP_MATTER_VERSION` - the Matter specification this image carries, used for the
image label and the verification line (default: see Dockerfile)

Expand All @@ -298,6 +328,7 @@ To build on an ESP-IDF image you built yourself:
./scripts/build.sh esp-idf
docker build \
--build-arg BASE_IMAGE=jethome-dev-esp-idf:local \
--build-arg IDF_VERSION=v<idf-version> \
-t jethome-dev-esp-matter:local images/esp-matter
```

Expand Down
9 changes: 6 additions & 3 deletions images/versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@
"base_tag": "idf-v5.5.5",
"upstream_branch": "release/v1.6",
"args": {
"ESP_MATTER_VERSION": "v1.6"
"ESP_MATTER_VERSION": "v1.6",
"IDF_VERSION": "v5.5.5"
},
"pin": {
"ESP_MATTER_REF": "2c125619610b9a16bb8e92f5ebbf55111638615e"
Expand All @@ -93,7 +94,8 @@
"base_tag": "idf-v5.5.5",
"upstream_branch": "release/v1.5",
"args": {
"ESP_MATTER_VERSION": "v1.5.1"
"ESP_MATTER_VERSION": "v1.5.1",
"IDF_VERSION": "v5.5.5"
},
"pin": {
"ESP_MATTER_REF": "44dfffd046f04552a87fa5ff7cf9695be486f177"
Expand All @@ -105,7 +107,8 @@
"base_tag": "idf-v5.4.1",
"upstream_branch": "release/v1.4.2",
"args": {
"ESP_MATTER_VERSION": "v1.4.2"
"ESP_MATTER_VERSION": "v1.4.2",
"IDF_VERSION": "v5.4.1"
},
"pin": {
"ESP_MATTER_REF": "2e4e0050c0bf6167f6bfc3351086c4e1126a6893"
Expand Down