Step 1 of 2 toward removing apk add from the zigbee2mqtt seed-data init container, which currently installs jq on every pod start. Per owner directive: baking tools into a layered image is fine, adding them at runtime is not.
Why this is split in two
Runtime images in this repo are pinned by digest (as the secret-rotator CronJobs are), and the digest doesn't exist until the image has been built and pushed. So this PR adds only the image and its build step — nothing consumes it yet, so it is inert on merge.
To make step 2 cheap, the build writes the digest via kaniko's --image-name-with-digest-file and echoes it as PIN THIS:, so the follow-up can copy the reference straight out of the build log with no registry query. (--image-name-with-digest-file was verified against executor:v1.23.2-debug --help rather than assumed.)
Why an image for one jq call
I first implemented this with busybox awk to avoid a new image, and testing turned up a genuine hazard worth recording:
"key" occurs 12 times in the real backup — once per device with an APS link_key — and the first occurrence is a device key, because "devices" sorts before "network_key". An extraction that isn't correctly anchored returns a device link key; herdsman then forms a NEW network and every device drops off.
I hit exactly that during testing: a corruption fixture aimed at the network key silently modified a device key instead, which is what exposed the ordering. The awk version I built was anchored and did produce byte-identical output to the live value (sha256 27c5c77a945bcecc), with guards covering non-hex, wrong-length, and missing-key inputs — but the failure mode if anyone later edits that expression is losing the whole Zigbee network. A real JSON parser earns an image here rather than a clever one-liner in a manifest.
Conventions followed
Containerfile alongside the other ci/images/ entries
Base pinned by digest to the samealpine:3.21 digest the ci-k8s image already uses
kaniko step gated on its own path, so unrelated pushes don't rebuild it (per the #336 note in that pipeline)
date + latest tags pushed, matching the other images
Labelled "Runtime image, not CI tooling", following the build-secret-rotator precedent for a non-CI image in this pipeline
Step 2 (follow-up, after this builds)
Switch the seed-data init container to z2m-seed@sha256:<digest>, replace apk add jq + the jq call, and keep the fail-loudly validation (non-hex / wrong-length / missing key → explicit FATAL and non-zero exit) so a bad network_key can never be silently rendered.
Note this init container is unrelated to PR #441 (the interview watchdog), which already complies with the directive by reusing eclipse-mosquitto for its bundled mosquitto_pub.
Step **1 of 2** toward removing `apk add` from the zigbee2mqtt `seed-data` init container, which currently installs `jq` on **every pod start**. Per owner directive: baking tools into a layered image is fine, adding them at runtime is not.
## Why this is split in two
Runtime images in this repo are pinned by **digest** (as the secret-rotator CronJobs are), and the digest doesn't exist until the image has been built and pushed. So this PR adds *only* the image and its build step — **nothing consumes it yet, so it is inert on merge.**
To make step 2 cheap, the build writes the digest via kaniko's `--image-name-with-digest-file` and echoes it as `PIN THIS:`, so the follow-up can copy the reference straight out of the build log with no registry query. (`--image-name-with-digest-file` was verified against `executor:v1.23.2-debug --help` rather than assumed.)
## Why an image for one `jq` call
I first implemented this with busybox `awk` to avoid a new image, and testing turned up a genuine hazard worth recording:
**`"key"` occurs 12 times in the real backup** — once per device with an APS `link_key` — and the **first occurrence is a device key**, because `"devices"` sorts before `"network_key"`. An extraction that isn't correctly anchored returns a device link key; herdsman then forms a **NEW network and every device drops off**.
I hit exactly that during testing: a corruption fixture aimed at the network key silently modified a *device* key instead, which is what exposed the ordering. The awk version I built was anchored and did produce byte-identical output to the live value (sha256 `27c5c77a945bcecc`), with guards covering non-hex, wrong-length, and missing-key inputs — but the failure mode if anyone later edits that expression is losing the whole Zigbee network. A real JSON parser earns an image here rather than a clever one-liner in a manifest.
## Conventions followed
- `Containerfile` alongside the other `ci/images/` entries
- Base pinned by digest to the **same** `alpine:3.21` digest the `ci-k8s` image already uses
- kaniko step gated on its own path, so unrelated pushes don't rebuild it (per the #336 note in that pipeline)
- date + `latest` tags pushed, matching the other images
- Labelled "Runtime image, not CI tooling", following the `build-secret-rotator` precedent for a non-CI image in this pipeline
## Step 2 (follow-up, after this builds)
Switch the `seed-data` init container to `z2m-seed@sha256:<digest>`, replace `apk add jq` + the jq call, and keep the fail-loudly validation (non-hex / wrong-length / missing key → explicit FATAL and non-zero exit) so a bad `network_key` can never be silently rendered.
Note this init container is unrelated to PR #441 (the interview watchdog), which already complies with the directive by reusing `eclipse-mosquitto` for its bundled `mosquitto_pub`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Step 1 of 2 toward removing `apk add` from the zigbee2mqtt seed-data init
container, which currently installs jq on every pod start. Owner
directive: baking tools into a layered image is fine, adding them at
runtime is not.
This commit only adds the image and its build step; nothing consumes it
yet, so it is inert on merge. The Deployment cannot be switched in the
same commit because runtime images in this repo are pinned by DIGEST (as
the secret-rotator CronJobs are), and the digest does not exist until
this has built and pushed. The build therefore writes it out via kaniko's
--image-name-with-digest-file and echoes it as "PIN THIS:", so the
follow-up commit can copy the reference straight from the build log
without a separate registry query.
Why a real JSON parser rather than busybox sed/awk: "key" occurs 12 times
in the actual backup — once per device with an APS link_key — and the
FIRST occurrence is a device key, because "devices" sorts before
"network_key". An extraction that is not correctly anchored yields a
device link key, herdsman then forms a NEW network, and every device
drops off. This was reproduced while prototyping the awk approach, which
is what makes jq worth an image here rather than clever one-liners in the
manifest.
Follows the existing conventions: Containerfile alongside the other
ci/images entries, base pinned by digest to the same alpine:3.21 the
ci-k8s image uses, kaniko build gated on its own path so unrelated pushes
do not rebuild it, and date + latest tags pushed as the other images do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖Automated review — model CLI default (complexity: complex), commit 0d7fecad8df083b462ba081df63b486495db077a.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
Adds a new build-z2m-seed kaniko step to .woodpecker/build-ci-images.yml and a
matching ci/images/z2m-seed/Containerfile (alpine:3.21 + jq), intended as the
base for the zigbee2mqtt seed-data init container in a follow-up PR. Nothing in
the repo currently references the new image (confirmed by grepping the
zigbee2mqtt manifests and the rest of the tree), so the PR is genuinely inert on
merge as claimed. No blocking issues found.
🟡 Suggestions
.woodpecker/build-ci-images.yml:161-162 — --image-name-with-digest-file is
used together with two--destination flags (:$TAG and :latest).
kaniko writes one line per destination to that file, so /kaniko/z2m-seed.digest (and thus the PIN THIS: log output) will print two
lines with the same digest but different tags. Since the digest is identical
either way this doesn't affect correctness of the follow-up step, but it's
worth a one-line comment noting the log will show two lines so whoever does
step 2 isn't confused about which to copy.
Tests
No test infrastructure exists for ci/images/*/Containerfile or the pipeline
YAML elsewhere in the repo (the sibling ansible/k8s/tofu/secret-rotator
images have none either), so the absence of tests here is consistent with
convention. The change is validated by the pipeline itself running kaniko on
merge/manual trigger; I can't execute that here, but the YAML structure mirrors
the existing build-secret-rotator step field-for-field (same env, auth
construction, tag scheme, gating), and the Containerfile reuses the exact alpine:3.21 digest already pinned in ci/images/k8s/Containerfile, so no new
base-image trust is introduced.
Recommendation
Looks reasonable — no blocking issues; comment above is optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `0d7fecad8df083b462ba081df63b486495db077a`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
Adds a new `build-z2m-seed` kaniko step to `.woodpecker/build-ci-images.yml` and a
matching `ci/images/z2m-seed/Containerfile` (alpine:3.21 + `jq`), intended as the
base for the zigbee2mqtt `seed-data` init container in a follow-up PR. Nothing in
the repo currently references the new image (confirmed by grepping the
zigbee2mqtt manifests and the rest of the tree), so the PR is genuinely inert on
merge as claimed. No blocking issues found.
## 🟡 Suggestions
- `.woodpecker/build-ci-images.yml:161-162` — `--image-name-with-digest-file` is
used together with **two** `--destination` flags (`:$TAG` and `:latest`).
kaniko writes one line per destination to that file, so
`/kaniko/z2m-seed.digest` (and thus the `PIN THIS:` log output) will print two
lines with the same digest but different tags. Since the digest is identical
either way this doesn't affect correctness of the follow-up step, but it's
worth a one-line comment noting the log will show two lines so whoever does
step 2 isn't confused about which to copy.
## Tests
No test infrastructure exists for `ci/images/*/Containerfile` or the pipeline
YAML elsewhere in the repo (the sibling `ansible`/`k8s`/`tofu`/`secret-rotator`
images have none either), so the absence of tests here is consistent with
convention. The change is validated by the pipeline itself running kaniko on
merge/manual trigger; I can't execute that here, but the YAML structure mirrors
the existing `build-secret-rotator` step field-for-field (same env, auth
construction, tag scheme, gating), and the `Containerfile` reuses the exact
`alpine:3.21` digest already pinned in `ci/images/k8s/Containerfile`, so no new
base-image trust is introduced.
## Recommendation
Looks reasonable — no blocking issues; comment above is optional.
<!-- pr-review:sha=0d7fecad8df083b462ba081df63b486495db077a -->
Good catch, and precise. Rather than add a comment warning about the ambiguity, I removed it: the step now reduces to the bare digest, dedupes, and prints one unambiguous copy-pasteable reference. The per-destination behaviour is documented inline so a future edit does not reintroduce it.
Verified against a two-line fixture in kaniko format:
Thanks also for independently confirming the PR is inert on merge — that was the main property I wanted checked, since it is what makes the two-step split safe.
Addressed in the latest push.
### 🟡 Two-line digest file
Good catch, and precise. Rather than add a comment warning about the ambiguity, I removed it: the step now reduces to the bare digest, dedupes, and prints one unambiguous copy-pasteable reference. The per-destination behaviour is documented inline so a future edit does not reintroduce it.
Verified against a two-line fixture in kaniko format:
```
gitea.renner.uno/gitea_admin/z2m-seed:2026-07-30@sha256:abc123
gitea.renner.uno/gitea_admin/z2m-seed:latest@sha256:abc123
-> PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@sha256:abc123
```
Thanks also for independently confirming the PR is inert on merge — that was the main property I wanted checked, since it is what makes the two-step split safe.
Review point on #442: kaniko writes one line per --destination to
--image-name-with-digest-file, so with both :$TAG and :latest the file
holds two entries carrying the same digest, and "PIN THIS:" would print
two near-identical lines for whoever does step 2 to choose between.
Rather than just comment on the ambiguity, remove it: reduce to the bare
digest, dedupe, and print one unambiguous copy-pasteable reference. The
multi-line behaviour is documented inline so a future edit does not
reintroduce it.
Verified the reduction against a two-line fixture matching kaniko's
format:
gitea.renner.uno/gitea_admin/z2m-seed:2026-07-30@sha256:abc123
gitea.renner.uno/gitea_admin/z2m-seed:latest@sha256:abc123
-> PIN THIS: gitea.renner.uno/gitea_admin/z2m-seed@sha256:abc123
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Step 1 of 2 toward removing
apk addfrom the zigbee2mqttseed-datainit container, which currently installsjqon every pod start. Per owner directive: baking tools into a layered image is fine, adding them at runtime is not.Why this is split in two
Runtime images in this repo are pinned by digest (as the secret-rotator CronJobs are), and the digest doesn't exist until the image has been built and pushed. So this PR adds only the image and its build step — nothing consumes it yet, so it is inert on merge.
To make step 2 cheap, the build writes the digest via kaniko's
--image-name-with-digest-fileand echoes it asPIN THIS:, so the follow-up can copy the reference straight out of the build log with no registry query. (--image-name-with-digest-filewas verified againstexecutor:v1.23.2-debug --helprather than assumed.)Why an image for one
jqcallI first implemented this with busybox
awkto avoid a new image, and testing turned up a genuine hazard worth recording:"key"occurs 12 times in the real backup — once per device with an APSlink_key— and the first occurrence is a device key, because"devices"sorts before"network_key". An extraction that isn't correctly anchored returns a device link key; herdsman then forms a NEW network and every device drops off.I hit exactly that during testing: a corruption fixture aimed at the network key silently modified a device key instead, which is what exposed the ordering. The awk version I built was anchored and did produce byte-identical output to the live value (sha256
27c5c77a945bcecc), with guards covering non-hex, wrong-length, and missing-key inputs — but the failure mode if anyone later edits that expression is losing the whole Zigbee network. A real JSON parser earns an image here rather than a clever one-liner in a manifest.Conventions followed
Containerfilealongside the otherci/images/entriesalpine:3.21digest theci-k8simage already useslatesttags pushed, matching the other imagesbuild-secret-rotatorprecedent for a non-CI image in this pipelineStep 2 (follow-up, after this builds)
Switch the
seed-datainit container toz2m-seed@sha256:<digest>, replaceapk add jq+ the jq call, and keep the fail-loudly validation (non-hex / wrong-length / missing key → explicit FATAL and non-zero exit) so a badnetwork_keycan never be silently rendered.Note this init container is unrelated to PR #441 (the interview watchdog), which already complies with the directive by reusing
eclipse-mosquittofor its bundledmosquitto_pub.🤖 Generated with Claude Code
🤖 Automated review — model
CLI default(complexity: complex), commit0d7fecad8df083b462ba081df63b486495db077a.Summary
Adds a new
build-z2m-seedkaniko step to.woodpecker/build-ci-images.ymland amatching
ci/images/z2m-seed/Containerfile(alpine:3.21 +jq), intended as thebase for the zigbee2mqtt
seed-datainit container in a follow-up PR. Nothing inthe repo currently references the new image (confirmed by grepping the
zigbee2mqtt manifests and the rest of the tree), so the PR is genuinely inert on
merge as claimed. No blocking issues found.
🟡 Suggestions
.woodpecker/build-ci-images.yml:161-162—--image-name-with-digest-fileisused together with two
--destinationflags (:$TAGand:latest).kaniko writes one line per destination to that file, so
/kaniko/z2m-seed.digest(and thus thePIN THIS:log output) will print twolines with the same digest but different tags. Since the digest is identical
either way this doesn't affect correctness of the follow-up step, but it's
worth a one-line comment noting the log will show two lines so whoever does
step 2 isn't confused about which to copy.
Tests
No test infrastructure exists for
ci/images/*/Containerfileor the pipelineYAML elsewhere in the repo (the sibling
ansible/k8s/tofu/secret-rotatorimages have none either), so the absence of tests here is consistent with
convention. The change is validated by the pipeline itself running kaniko on
merge/manual trigger; I can't execute that here, but the YAML structure mirrors
the existing
build-secret-rotatorstep field-for-field (same env, authconstruction, tag scheme, gating), and the
Containerfilereuses the exactalpine:3.21digest already pinned inci/images/k8s/Containerfile, so no newbase-image trust is introduced.
Recommendation
Looks reasonable — no blocking issues; comment above is optional.
Addressed in the latest push.
🟡 Two-line digest file
Good catch, and precise. Rather than add a comment warning about the ambiguity, I removed it: the step now reduces to the bare digest, dedupes, and prints one unambiguous copy-pasteable reference. The per-destination behaviour is documented inline so a future edit does not reintroduce it.
Verified against a two-line fixture in kaniko format:
Thanks also for independently confirming the PR is inert on merge — that was the main property I wanted checked, since it is what makes the two-step split safe.