Skip to content

Wave 9 #120d: Phase 3 Hub — golden-image build pipeline (foundation) - #106

Merged
bernardc6 merged 1 commit into
mainfrom
subagent/hub-golden-image
Aug 6, 2026
Merged

Wave 9 #120d: Phase 3 Hub — golden-image build pipeline (foundation)#106
bernardc6 merged 1 commit into
mainfrom
subagent/hub-golden-image

Conversation

@bernardc6

Copy link
Copy Markdown
Collaborator

Phase 3 Hub — golden-image build pipeline (foundation)

This is the 4th and final sub-slice of Wave 9 #120 (Phase 3 Hub).

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.

Scope (6 files, +802 LOC)

File Kind LOC Role
scripts/build/hub-golden-image.manifest.yml NEW 83 Schema-version-1 manifest pinning HAOS 14.1 base + RoamCore layer + output filename
scripts/build/hub-golden-image.sh NEW 355 Bake script: tooling prereq + manifest load + retry-download + SHA verify + layer stage + host-gated Docker build
scripts/checks/hub-golden-image-smoke.sh NEW 198 Bash smoke wrapper (10 assertions)
docs/runbooks/hub-golden-image.md NEW 58 IKEA-style runbook (integrator-facing §1-§4, developer-facing §5)
scripts/check.sh MOD +9 +9 run_if_present wiring into the --core-only chain
homeassistant/addons/roamcore-hub-builder/Dockerfile.hub NEW 87 Minimal valid Dockerfile (the "what the script invokes on a Linux+Docker host" piece)
.gitignore MOD +6 +6 Exclude the script's local cache + produced outputs

Verification

$ bash scripts/check.sh --core-only
[snip]
Hub golden-image smoke: 10 passed, 0 failed
✓ Hub golden-image smoke check PASSED

✓ all requested smoke checks passed.
exit 0

The 10 smoke assertions:

  1. ✓ scripts/build/hub-golden-image.sh exists + is executable + uses env-bash shebang
  2. ✓ scripts/build/hub-golden-image.manifest.yml exists + parses as valid YAML
  3. ✓ Script references the manifest path in its body
  4. ✓ Manifest's base_image URL is reachable (HTTP 200 via 302 redirect — https://github.com/home-assistant/operating-system/releases/download/14.1/haos_generic-x86-64-14.1.img.xz)
  5. ✓ Manifest's base_image.expected_sha256 is a 64-char hex string (504c10f5703ebadc…, verified on 2026-08-06)
  6. ✓ Script --help output is plain English (no jargon: no errno 2, no HTTP 503, no Traceback, no operator-speak)
  7. ✓ Manifest's output.filename is non-empty + ends in .img.gz (roamcore-hub-golden-v0.1.0.img.gz)
  8. ✓ Script body shows an idempotent cache-skip pattern (re-runs reuse the cached base)
  9. ✓ Script body shows a retry/backoff pattern for the download (resilient to transient failures)
  10. ✓ (advisory) Dockerfile.hub is present — the script's docker build step is actionable on a real Linux+Docker host

Doctrine (Bernard, 2026-08-05)

  • Verification is mandatory ✓ — the smoke validates the script + manifest + cross-references + base-image reachability + SHA format + plain-English --help. Real (not stub).
  • Auto-recover ✓ — the script detects its prerequisites (docker, sha256sum, curl), downloads with retry-with-backoff at 0s/5s/15s + 60s timeout per attempt, and reports plain-English errors.
  • Plain-English errors ✓ — "Could not download the base image — check your internet connection", not "curl: HTTP 503".
  • Idempotent installer ✓ — re-running produces the same end state (cached base + SHA-verified re-bake).
  • Backup-before-mutate ✓ — no Proxmox / HA / OpenWrt / networking change. Pure repo-local.
  • Tier discipline ✓ — no tier-a/b/c claim in the manifest or script.

GOLDEN.md alignment

  • Serves P1 (Novice-first UX): runbook opens with one plain-English sentence a vanlifer can understand; script errors are plain-English.
  • Serves P2 (Mission-critical connectivity): golden image is the canonical recovery surface for a bricked Hub.
  • Respects E1 (Customer-facing repo): runbook in docs/runbooks/; no internal logs in user tree.
  • Respects E2 (Documentation-driven): manifest is source of truth for "what's in the image"; runbook is source of truth for integrators; script is source of truth for the bake; smoke is source of truth for the chain.
  • Respects E3 (Backup + rollback discipline): no Proxmox / HA / OpenWrt / networking change. Pure repo-local. Rollback = single git revert.
  • Respects E4 (Git fast mode): direct-to-branch push on subagent/hub-golden-image.
  • Respects E5 (do not touch vmbr0): no networking config touched.
  • E7 (rc-entity-naming.md): N/A — no new entities in this slice.

Anti-patterns avoided

  • ❌ "Hand-configuring Victron" — purely build infrastructure, no device-specific config.
  • ❌ "Touching vmbr0" — no networking config touched.
  • ❌ "Committing secrets" — no secrets in the script or manifest; the build pulls from public URLs.
  • ❌ "Wide PRs" — pure build-infra slice, no cross-cutting changes.
  • ❌ "Pulling in unrelated project context" — strictly Phase 3 / Hub scope.
  • ❌ "Putting internal engineering logs on the public GitHub" — the runbook is the only user-facing piece.

Open question for Bernard

The +9 lines added to scripts/check.sh slot the smoke right after the connection-state smoke + right before the if [ "$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 after connections/openclaw-api/tests/test_connection_yml.py instead; let me know.

Rollback

Single git revert on 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

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>
@bernardc6
bernardc6 merged commit d0ac2bc into main Aug 6, 2026
2 checks passed
@bernardc6
bernardc6 deleted the subagent/hub-golden-image branch August 6, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant