From 407c34edbae4c636685e2f0997717302e9aa630a Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Sat, 3 Oct 2026 00:46:55 -0600 Subject: [PATCH 1/2] K2GO-440 fix(updaters): source the add-ons and build-assets mirrors from the self-updating clone Point the refresh wrappers at the mirror in /opt/iiab-android (which dash-node self-updates via git reset), with a fallback to the bake-time copy in the ansible roles dir. A mirror fix then ships via self-update with no rebake, like forgejo-refresh.sh already sources its orchestration from the clone. The two roles are Knowledge to Go overlays, not IIAB upstream. ADR: controller/docs/ADR-updater-self-update-no-rebake.md. --- .../docs/ADR-updater-self-update-no-rebake.md | 103 ++++++++++++++++++ tools/code-addons-refresh.sh | 8 +- tools/code-assets-refresh.sh | 8 +- 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 controller/docs/ADR-updater-self-update-no-rebake.md diff --git a/controller/docs/ADR-updater-self-update-no-rebake.md b/controller/docs/ADR-updater-self-update-no-rebake.md new file mode 100644 index 000000000..77ca42d26 --- /dev/null +++ b/controller/docs/ADR-updater-self-update-no-rebake.md @@ -0,0 +1,103 @@ +# ADR: Content-updater self-update without a rootfs rebake (K2GO-440) + +Status: Proposed + +## Context + +The box runs three content updaters, driven by dash-node over REST (localhost): +Forgejo repos refresh, Code on the Go add-ons refresh, and Code on the Go +build-assets refresh. Each is a detached wrapper in `tools/` that calls the +script doing the real work. + +dash-node already self-updates. `POST /system/dashboard/rebuild` runs +`tools/rebuild-dashboard.sh`, which does `git fetch` + `git reset --hard +origin/` on the whole on-device clone at `/opt/iiab-android`, then +blue-green rebuilds `static/dashboard`. The reset refreshes the ENTIRE clone +working tree, not only the dashboard. + +Today the three updaters are not consistent: + +- Forgejo refresh sources its orchestration from + `/opt/iiab-android/static/forgejo/orchestration`: inside the self-updating + clone. A change ships through self-update, no rebake. +- The add-ons and build-assets roles exist ONLY in Knowledge to Go, as overlays + in our repo (`tools/upstream-patches/overlays/roles//`). They are NOT + IIAB upstream roles, and they are never fetched from IIAB: that is the whole + point of these two roles. At rootfs-build (bake) time our overlay-apply copies + each role into the on-box ansible roles directory, + `/opt/iiab/iiab/roles//` (just the location where `runrole` looks, + alongside IIAB's own roles): the content there is ours. The add-ons and + build-assets wrappers read the mirror from that copied-in location, which the + overlay-apply refreshes ONLY at bake. The authoritative source, + `tools/upstream-patches/overlays/roles//files/mirror_*.py`, also sits in + the self-updating clone, but the wrapper does not use that copy. + +So a two-line fix to an add-ons or build-assets mirror forces a fleet rebake +today. Not because the role comes from upstream (it does not): purely because the +overlay is copied into the ansible roles directory only at bake. Forgejo already +avoids this by reading from the clone. + +## Decision + +Point the add-ons and build-assets refresh wrappers at the mirror copy in the +self-updating clone, with a fallback to the baked copy. This mirrors what the +Forgejo wrapper already does with its orchestration. A change to a mirror then +ships through dash-node's existing self-update (`git reset`) and the live +refresh uses it at once: no rebake, and no dash-node version bump (the wrapper +and the mirror are not `static/dashboard`; the same `git reset` carries them). + +Single source of truth: our repo. The overlay roles (add-ons, build-assets) and +the Forgejo patch live only in our tree. We do not take the role from IIAB +upstream, and updating these needs no upstream change. There is therefore NO +second source to arbitrate: the only duality is "fresh clone copy" vs "stale +baked copy", resolved by a fixed preference (clone first, baked fallback). We do +NOT build version arbitration between repositories ("the newer of A vs B wins"). +That complexity is explicitly rejected. + +## Scope (minimal) + +- `tools/code-addons-refresh.sh` and `tools/code-assets-refresh.sh`: resolve + `MIRROR` as the clone copy when present, else the baked copy. +- No change to the role `install.yml`. Bake and on-demand `runrole` still run the + overlay's copy in the on-box ansible roles directory (placed there at bake); + the no-rebake benefit targets the LIVE refresh, which is where minor changes + are consumed. A first install right after a self-update uses that copied-in + mirror once; the next refresh uses the clone copy. +- No version or identifier per script, and no ahead/behind reporting: not needed + for the benefit (YAGNI). If a box ever needs to report which updater version + it runs, add it then. + +## Forward-compatibility: rolling box vs pinned APK (boundary plus follow-up) + +dash-node and the rootfs now update independently of the APK (self-update, plus +this change). The APK is pinned per install. This creates brain (box) / body +(APK) version skew: a newer box can run updater logic an older APK was not built +to drive. + +Boundary this ADR sets, so K2GO-440 does not make the skew worse: + +- A self-update to an updater MUST preserve the REST contract and the + status/JSON shape that shipped APKs parse: the endpoint paths, the + `done: N downloaded, R reused, K failed` and `result: up-to-date` log lines, + and the status fields. An additive, contract-preserving change ships freely + via self-update. +- A change that BREAKS that contract is gated by the mechanism that already + exists: bump the dash-node version and raise the per-module minimum in + `DashNodeRequirement` (the app already checks it and degrades gracefully). A + contract break is therefore never silent. + +The general policy for a rolling box against a stale APK (capability +negotiation, a box-declared minimum APK, an "update your app" prompt) is larger +than K2GO-440 and is deferred to its own ticket to analyze. This ADR only fixes +the boundary above so current changes stay safe. + +## Consequences + +- add-ons and build-assets updaters become fixable without a rebake, like + Forgejo. +- The install and bake path is unchanged (baked copy), so a fresh install is + unaffected. +- The repo is the single source: no upstream dependency, no cross-repo + arbitration. +- The brain/body skew is bounded (contract stability plus the existing version + gate); the general policy is a separate follow-up ticket. diff --git a/tools/code-addons-refresh.sh b/tools/code-addons-refresh.sh index 9bf9f992b..3f1e5f138 100644 --- a/tools/code-addons-refresh.sh +++ b/tools/code-addons-refresh.sh @@ -21,7 +21,13 @@ PID=/var/run/code-addons-refresh.pid # base and the catalog serve-base come from mirror_addons.py's own defaults, so # they are not restated here. SERVE=/library/www/code-addons -MIRROR=/opt/iiab/iiab/roles/code_addons/files/mirror_addons.py +# K2GO-440: prefer the mirror from the self-updating clone, so a mirror fix ships via the dash-node +# self-update (git reset on /opt/iiab-android) with NO rebake; fall back to the copy the overlay +# places in the ansible roles dir at bake. Same idea as forgejo-refresh.sh sourcing from the clone. +# The code_addons role is ours only (a Knowledge to Go overlay, not an IIAB upstream role). +MIRROR_CLONE=/opt/iiab-android/tools/upstream-patches/overlays/roles/code_addons/files/mirror_addons.py +MIRROR_BAKED=/opt/iiab/iiab/roles/code_addons/files/mirror_addons.py +MIRROR=$([ -f "$MIRROR_CLONE" ] && echo "$MIRROR_CLONE" || echo "$MIRROR_BAKED") : > "$LOG" 2>/dev/null || true echo running > "$STATUS" 2>/dev/null || true diff --git a/tools/code-assets-refresh.sh b/tools/code-assets-refresh.sh index 82a4048cf..36583c38e 100644 --- a/tools/code-assets-refresh.sh +++ b/tools/code-assets-refresh.sh @@ -21,7 +21,13 @@ PID=/var/run/code-assets-refresh.pid # base and the serve-base come from mirror_code_assets.py's own defaults, so they # are not restated here. SERVE=/library/www/code-assets -MIRROR=/opt/iiab/iiab/roles/code_assets/files/mirror_code_assets.py +# K2GO-440: prefer the mirror from the self-updating clone, so a mirror fix ships via the dash-node +# self-update (git reset on /opt/iiab-android) with NO rebake; fall back to the copy the overlay +# places in the ansible roles dir at bake. Same idea as forgejo-refresh.sh sourcing from the clone. +# The code_assets role is ours only (a Knowledge to Go overlay, not an IIAB upstream role). +MIRROR_CLONE=/opt/iiab-android/tools/upstream-patches/overlays/roles/code_assets/files/mirror_code_assets.py +MIRROR_BAKED=/opt/iiab/iiab/roles/code_assets/files/mirror_code_assets.py +MIRROR=$([ -f "$MIRROR_CLONE" ] && echo "$MIRROR_CLONE" || echo "$MIRROR_BAKED") : > "$LOG" 2>/dev/null || true echo running > "$STATUS" 2>/dev/null || true From 34554f10f6e902808f0afea9a441e971eb8c4cde Mon Sep 17 00:00:00 2001 From: "Luis Guzman (AppDevForAll)" Date: Sat, 3 Oct 2026 02:08:22 -0600 Subject: [PATCH 2/2] K2GO-440 docs(adr): clarify delivery via the dash-node rebuild (manual Rebuild needs no version bump) The updater code reaches a box through the dash-node rebuild (git reset of the clone), triggered from the Dashboard detail screen. Note both paths: the auto "update available" prompt needs a package.json bump, but the always-present manual Rebuild deploys a mirror/wrapper fix with no bump. Content-refresh actions do not update the updater code. --- .../docs/ADR-updater-self-update-no-rebake.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/controller/docs/ADR-updater-self-update-no-rebake.md b/controller/docs/ADR-updater-self-update-no-rebake.md index 77ca42d26..ae04a00e7 100644 --- a/controller/docs/ADR-updater-self-update-no-rebake.md +++ b/controller/docs/ADR-updater-self-update-no-rebake.md @@ -54,6 +54,35 @@ baked copy", resolved by a fixed preference (clone first, baked fallback). We do NOT build version arbitration between repositories ("the newer of A vs B wins"). That complexity is explicitly rejected. +## Delivery (how a mirror fix reaches a deployed box) + +The content-refresh actions ("Update repos / add-ons / assets") re-download content +only: they do NOT update the updater code. The updater code (wrappers and mirrors) +reaches a box through the dash-node self-update (`POST /system/dashboard/rebuild`, +`git reset --hard origin/` on the whole clone). + +The rebuild is reached from the app UI on the Dashboard detail screen +(`redesign/DashboardDetailFragment`, also surfaced in `ModuleHubFragment`): an +"Update" button when a newer version is on `origin/main`, or a de-emphasized but +always-present "Rebuild" button otherwise ("Never blocks: the user can still +Rebuild manually"). Both run `POST /system/dashboard/rebuild`. + +Two delivery paths follow from that: + +- Automatic prompt: the "update available" chip appears only when `package.json` + differs from `origin/main` (CLAUDE.local.md: "No bump -> existing boxes never + pick up the change through self-update"). The bump is the fleet-wide delivery + trigger (as in ADFA-386, "the version bump is the delivery mechanism"). +- Manual: the "Rebuild" button is always available, so an admin can trigger the + `git reset --hard origin/main` at any time; it pulls the whole clone (new + wrappers and mirrors) regardless of any version bump. + +So a mirror or wrapper fix does NOT strictly require a dash-node version bump to +reach a box: a manual Rebuild deploys it. A bump is only needed to auto-prompt the +fleet. Either way the `git reset` carries the whole clone, no rebake. A fresh bake +gets the code from source regardless. This ADR does NOT add an updater-only +delivery trigger independent of the rebuild; that would be extra scope. + ## Scope (minimal) - `tools/code-addons-refresh.sh` and `tools/code-assets-refresh.sh`: resolve