Wave 9 #120d: Phase 3 Hub — golden-image build pipeline (foundation) - #106
Merged
Conversation
Context:
- Phase 3 of the RoamCore product directive ("Reliable reference-
hardware experience") calls for a golden image that captures
the Hub's exact, known-good software state as one file. Until
now, a Hub that had to be replaced or restored required either
a 4-hour rebuild or a manual re-flash of the upstream Home
Assistant OS followed by re-installing every RoamCore addon,
package, and custom component by hand. The only thing standing
between a bricked Hub and a working Hub was a long recovery
session, not a single file.
- This slice ships the smallest possible foundation for the
golden-image promise: a canonical manifest
(scripts/build/hub-golden-image.manifest.yml) that pins the
HAOS 14.1 base image + the RoamCore layer contents + the
output filename; a build script (scripts/build/hub-golden-
image.sh) that downloads + verifies + stages + would-bake the
image on a real Linux+Docker host; a Dockerfile.hub that is
what the build script invokes on a real build host; a smoke
check (scripts/checks/hub-golden-image-smoke.sh) that asserts
the script + manifest + their cross-references are healthy;
and an IKEA-style runbook (docs/runbooks/hub-golden-image.md)
so the next integrator who needs to flash the Hub knows where
the file lives + how to verify the pipeline.
- Scope is intentionally narrow — script-only delivery is
acceptable, mirroring the Wave 9 #106 OpenWrt Image Builder
precedent. The actual .img.gz is produced on a Linux+Docker
build host; on this host the script verifies what it can +
prints a clear "Baking skipped — script-only delivery" note
+ exits 0. No Proxmox / HA / OpenWrt / networking change.
Pure repo-local code + data. Rollback is a single 'git
revert'.
Changes:
- scripts/build/hub-golden-image.manifest.yml (NEW, 83 lines):
schema-version-1 manifest that pins the base image (HAOS 14.1
generic-x86-64, reachable URL + real SHA256 verified on
2026-08-06 + byte size + architectures), the RoamCore layer
(custom_components/roamcore + 6 addons + packages/roamcore_*),
the output filename (roamcore-hub-golden-v0.1.0.img.gz) +
a TBD output SHA256 placeholder, and the build metadata
(Docker image tag + Dockerfile path + expected bake minutes).
The aarch64 base image URL + SHA are commented inline for the
future ARM build (Raspberry Pi 5, Home Assistant Green, etc).
- scripts/build/hub-golden-image.sh (NEW, 355 lines): the bake
script. 8 sections (tooling prereq check, manifest load,
base-image download with retry-with-backoff at 0s/5s/15s +
60s per-attempt timeout, SHA256 verification, layer assembly,
host-gated Docker bake, output SHA computation, plain-English
summary). All user-facing error messages translate the
operator→vanlifer table ("Could not download the base image
— check your internet connection and try again", NOT
"curl: HTTP 503 from upstream"). The script is idempotent
(re-runs reuse the cached base image; re-bake is a no-op when
the output SHA already matches the manifest pin). On a host
without Docker (script-only delivery), the script prints a
clear summary of what would run on a real build host.
- scripts/checks/hub-golden-image-smoke.sh (NEW, 198 lines):
bash smoke wrapper. 10 assertions: script exists + executable
+ right shebang, manifest exists + parses as YAML, script
references the manifest path in its body, base_image URL is
reachable (HTTP 200 or 302 → 200), base_image expected_sha256
is a 64-char hex string, script --help is plain English (no
forbidden jargon: errno 2 / HTTP 503 / Traceback / etc),
output filename ends in .img.gz, script body has a cache-skip
pattern, script body has a retry/backoff pattern, Dockerfile
advisory present. Plain-English ✓/✗ markers + exit 0/non-zero
+ runnable on any host (only needs curl + python3 + PyYAML).
- docs/runbooks/hub-golden-image.md (NEW, 58 lines): IKEA-style
runbook. Opens with one plain-English sentence ("Your Hub is
a small computer that runs your dashboard. The golden image
is the exact, known-good version of that computer's software,
captured as a single file."). §1-§4 are integrator-facing
(What this is, When to rebuild it, How to build it, How to
verify it) and contain NO file paths (except the one bash
one-liner in §3 per the discipline block §B), NO function
names, NO PR numbers, NO commit SHAs, NO branch names, NO
'Wave N' or 'tier-X' labels, NO bash code blocks. §5 is the
developer-facing reference (the build-host prerequisites, the
manifest schema, the SHA-pinning workflow).
- scripts/check.sh (+9 lines, --core-only chain): wires the new
smoke into the existing run_if_present chain right before the
non-core block, so every check.sh run exercises the
golden-image pipeline foundation. The slot is below the
connection-state smoke + above the 'if [ CORE_ONLY -eq 0 ]'
block, which positions it cleanly alongside the other
Phase 3 Hub smokes (#120b restart-stability, #120c support-
bundle-export) without merge overlap.
- homeassistant/addons/roamcore-hub-builder/Dockerfile.hub
(NEW, 87 lines): the actual Docker build for the golden
image. Minimal but valid — installs the toolchain (xz-utils,
util-linux, rsync, e2fsprogs, dosfstools, ca-certificates),
copies the staged RoamCore layer, and prints a clear
'hub-golden-image builder ready' note. The actual
decompress + mount + rsync + recompress sequence is left as
a follow-up slice (the TODO(beyond-script-only) marker is
in the script's docker invocation + in the Dockerfile), once
the manifest pin + staging contract are settled.
- .gitignore (+6 lines): adds .cache/ + roamcore-hub-golden-*.{img,
img.gz,img.xz} so the script's local cache + the produced
output never accidentally get committed.
User-facing: Gives me a single, known-good snapshot of my Hub's
software, so that if my Hub ever has to be replaced or restored,
recovery is one image and one flash away — not a 4-hour rebuild.
GOLDEN.md alignment:
- Serves P1 (Novice-first UX): the runbook opens with one plain-
English sentence a vanlifer can understand; the script's error
messages are plain-English ("Could not download the base image
— check your internet connection" not "curl: HTTP 503"); the
operator→vanlifer translation table is honoured in every
user-facing string.
- Serves P2 (Mission-critical connectivity): the golden image is
the canonical recovery surface for a Hub that won't boot. A
vanlifer with a bricked Hub can flash a known-good image and
be back online, instead of trying to rebuild from scratch
while parked on a remote site with no signal.
- Respects E1 (Customer-facing repo): the runbook lives in
docs/runbooks/; no internal logs or sensitive infra land in
the user tree. The script + manifest + smoke + Dockerfile
stay in scripts/build/, scripts/checks/, and the addon
directory (developer plumbing locations, NOT user-facing).
- Respects E2 (Documentation-driven): the manifest is the
source of truth for 'what's in the image'; the runbook is
the source of truth for the integrator-facing recovery
surface; the script is the source of truth for the bake;
the smoke is the source of truth for the chain. Every layer
of the pipeline is data-driven + verifiable.
- Respects E3 (Backup + rollback discipline): no Proxmox / HA /
OpenWrt / networking change. Pure repo-local code + data.
Rollback is a single 'git revert' with no state to recover.
The build script itself is idempotent (re-running produces
the same end state — cached base image + SHA-verified
re-bake) and safe (doesn't touch the host's existing HA
install).
- Respects E4 (Git fast mode): direct-to-branch push on
subagent/hub-golden-image; one PR for Bernard per protocol.
- Respects E5 (do not touch vmbr0): no networking config
touched.
- E7 (rc-entity-naming.md): N/A — no new entities in this slice.
- Avoids ❌ 'Hand-configuring Victron' (purely build
infrastructure; no device-specific config).
- Avoids ❌ 'Touching vmbr0' (no networking config touched).
- Avoids ❌ 'Committing secrets' (no secrets in the script or
manifest; the build pulls from public URLs).
- Avoids ❌ 'Wide PRs' (pure build-infra slice; no cross-
cutting changes; 5 new files + 2 minor modifications to
.gitignore + scripts/check.sh).
- Avoids ❌ 'Pulling in unrelated project context' (strictly
Phase 3 / Hub scope).
- Avoids ❌ 'Putting internal engineering logs on the public
GitHub' (the runbook is the only user-facing piece and is
written for integrators, not operators or developers — the
developer plumbing stays in scripts/build/ + scripts/checks/).
Verification:
- bash scripts/check.sh --core-only → GREEN (exit 0)
- 33 connection manifest tests + 22 connection-state tests
+ HA-beta smoke + roamcore integration smoke + openwrt
smoke + imagebuilder smoke + docs-link-integrity +
catalog-state-chip-smoke + the new hub-golden-image smoke
(10 assertions) all PASS.
- bash scripts/build/hub-golden-image.sh --help exits 0 with
plain-English output (the smoke verifies this).
- bash scripts/build/hub-golden-image.sh end-to-end: verifies
tooling prereqs, loads manifest, downloads HAOS 14.1 base
image (396 MB, real SHA matches), stages the RoamCore
layer, and prints the script-only-delivery summary
(Docker is installed on this host but the script's
'would-run' path + the §5 Dockerfile TODO defer the actual
bake to a follow-up slice).
- Re-run with cached base image: skips the download
(idempotent cache-skip pattern verified).
Rollback:
- Single 'git revert' on this PR. No state to recover.
- The script's local cache lives in .cache/hub-golden/ which
is in .gitignore; a revert removes the script + manifest +
smoke + Dockerfile + runbook + .gitignore line, and the
cache directory becomes harmless leftover (already ignored).
- No Proxmox / HA / OpenWrt / networking change to back out.
Co-Authored-By: Claude <noreply@anthropic.com>
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.
Phase 3 Hub — golden-image build pipeline (foundation)
This is the 4th and final sub-slice of Wave 9 #120 (Phase 3 Hub).
Scope (6 files, +802 LOC)
scripts/build/hub-golden-image.manifest.ymlscripts/build/hub-golden-image.shscripts/checks/hub-golden-image-smoke.shdocs/runbooks/hub-golden-image.mdscripts/check.shrun_if_presentwiring into the --core-only chainhomeassistant/addons/roamcore-hub-builder/Dockerfile.hub.gitignoreVerification
The 10 smoke assertions:
https://github.com/home-assistant/operating-system/releases/download/14.1/haos_generic-x86-64-14.1.img.xz)504c10f5703ebadc…, verified on 2026-08-06)roamcore-hub-golden-v0.1.0.img.gz)Doctrine (Bernard, 2026-08-05)
GOLDEN.md alignment
git revert.subagent/hub-golden-image.Anti-patterns avoided
Open question for Bernard
The +9 lines added to
scripts/check.shslot the smoke right after the connection-state smoke + right before theif [ "$CORE_ONLY" -eq 0 ]block. This is the same general area as PR #104 (#120b restart-stability) and PR #105 (#120c support-bundle-export). The orchestrator's sequential merge can rebase any two of the three onto the other without conflict; my placement is non-overlapping with the #120c slot (PR #105 adds inside the connection-state block; mine adds immediately after it) and adjacent to the #120b slot (PR #104 adds immediately after the connection-state block too — there could be a tiny adjacency conflict there). If you'd prefer, I'm happy to rebase my one-liner to slot right afterconnections/openclaw-api/tests/test_connection_yml.pyinstead; let me know.Rollback
Single
git reverton this PR. No state to recover. The script's local cache (.cache/hub-golden/) is already in .gitignore, so a revert cleans up cleanly.Co-Authored-By: Claude noreply@anthropic.com