|
| 1 | +# ADR: Content-updater self-update without a rootfs rebake (K2GO-440) |
| 2 | + |
| 3 | +Status: Proposed |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +The box runs three content updaters, driven by dash-node over REST (localhost): |
| 8 | +Forgejo repos refresh, Code on the Go add-ons refresh, and Code on the Go |
| 9 | +build-assets refresh. Each is a detached wrapper in `tools/` that calls the |
| 10 | +script doing the real work. |
| 11 | + |
| 12 | +dash-node already self-updates. `POST /system/dashboard/rebuild` runs |
| 13 | +`tools/rebuild-dashboard.sh`, which does `git fetch` + `git reset --hard |
| 14 | +origin/<branch>` on the whole on-device clone at `/opt/iiab-android`, then |
| 15 | +blue-green rebuilds `static/dashboard`. The reset refreshes the ENTIRE clone |
| 16 | +working tree, not only the dashboard. |
| 17 | + |
| 18 | +Today the three updaters are not consistent: |
| 19 | + |
| 20 | +- Forgejo refresh sources its orchestration from |
| 21 | + `/opt/iiab-android/static/forgejo/orchestration`: inside the self-updating |
| 22 | + clone. A change ships through self-update, no rebake. |
| 23 | +- The add-ons and build-assets roles exist ONLY in Knowledge to Go, as overlays |
| 24 | + in our repo (`tools/upstream-patches/overlays/roles/<role>/`). They are NOT |
| 25 | + IIAB upstream roles, and they are never fetched from IIAB: that is the whole |
| 26 | + point of these two roles. At rootfs-build (bake) time our overlay-apply copies |
| 27 | + each role into the on-box ansible roles directory, |
| 28 | + `/opt/iiab/iiab/roles/<role>/` (just the location where `runrole` looks, |
| 29 | + alongside IIAB's own roles): the content there is ours. The add-ons and |
| 30 | + build-assets wrappers read the mirror from that copied-in location, which the |
| 31 | + overlay-apply refreshes ONLY at bake. The authoritative source, |
| 32 | + `tools/upstream-patches/overlays/roles/<role>/files/mirror_*.py`, also sits in |
| 33 | + the self-updating clone, but the wrapper does not use that copy. |
| 34 | + |
| 35 | +So a two-line fix to an add-ons or build-assets mirror forces a fleet rebake |
| 36 | +today. Not because the role comes from upstream (it does not): purely because the |
| 37 | +overlay is copied into the ansible roles directory only at bake. Forgejo already |
| 38 | +avoids this by reading from the clone. |
| 39 | + |
| 40 | +## Decision |
| 41 | + |
| 42 | +Point the add-ons and build-assets refresh wrappers at the mirror copy in the |
| 43 | +self-updating clone, with a fallback to the baked copy. This mirrors what the |
| 44 | +Forgejo wrapper already does with its orchestration. A change to a mirror then |
| 45 | +ships through dash-node's existing self-update (`git reset`) and the live |
| 46 | +refresh uses it at once: no rebake, and no dash-node version bump (the wrapper |
| 47 | +and the mirror are not `static/dashboard`; the same `git reset` carries them). |
| 48 | + |
| 49 | +Single source of truth: our repo. The overlay roles (add-ons, build-assets) and |
| 50 | +the Forgejo patch live only in our tree. We do not take the role from IIAB |
| 51 | +upstream, and updating these needs no upstream change. There is therefore NO |
| 52 | +second source to arbitrate: the only duality is "fresh clone copy" vs "stale |
| 53 | +baked copy", resolved by a fixed preference (clone first, baked fallback). We do |
| 54 | +NOT build version arbitration between repositories ("the newer of A vs B wins"). |
| 55 | +That complexity is explicitly rejected. |
| 56 | + |
| 57 | +## Delivery (how a mirror fix reaches a deployed box) |
| 58 | + |
| 59 | +The content-refresh actions ("Update repos / add-ons / assets") re-download content |
| 60 | +only: they do NOT update the updater code. The updater code (wrappers and mirrors) |
| 61 | +reaches a box through the dash-node self-update (`POST /system/dashboard/rebuild`, |
| 62 | +`git reset --hard origin/<branch>` on the whole clone). |
| 63 | + |
| 64 | +The rebuild is reached from the app UI on the Dashboard detail screen |
| 65 | +(`redesign/DashboardDetailFragment`, also surfaced in `ModuleHubFragment`): an |
| 66 | +"Update" button when a newer version is on `origin/main`, or a de-emphasized but |
| 67 | +always-present "Rebuild" button otherwise ("Never blocks: the user can still |
| 68 | +Rebuild manually"). Both run `POST /system/dashboard/rebuild`. |
| 69 | + |
| 70 | +Two delivery paths follow from that: |
| 71 | + |
| 72 | +- Automatic prompt: the "update available" chip appears only when `package.json` |
| 73 | + differs from `origin/main` (CLAUDE.local.md: "No bump -> existing boxes never |
| 74 | + pick up the change through self-update"). The bump is the fleet-wide delivery |
| 75 | + trigger (as in ADFA-386, "the version bump is the delivery mechanism"). |
| 76 | +- Manual: the "Rebuild" button is always available, so an admin can trigger the |
| 77 | + `git reset --hard origin/main` at any time; it pulls the whole clone (new |
| 78 | + wrappers and mirrors) regardless of any version bump. |
| 79 | + |
| 80 | +So a mirror or wrapper fix does NOT strictly require a dash-node version bump to |
| 81 | +reach a box: a manual Rebuild deploys it. A bump is only needed to auto-prompt the |
| 82 | +fleet. Either way the `git reset` carries the whole clone, no rebake. A fresh bake |
| 83 | +gets the code from source regardless. This ADR does NOT add an updater-only |
| 84 | +delivery trigger independent of the rebuild; that would be extra scope. |
| 85 | + |
| 86 | +## Scope (minimal) |
| 87 | + |
| 88 | +- `tools/code-addons-refresh.sh` and `tools/code-assets-refresh.sh`: resolve |
| 89 | + `MIRROR` as the clone copy when present, else the baked copy. |
| 90 | +- No change to the role `install.yml`. Bake and on-demand `runrole` still run the |
| 91 | + overlay's copy in the on-box ansible roles directory (placed there at bake); |
| 92 | + the no-rebake benefit targets the LIVE refresh, which is where minor changes |
| 93 | + are consumed. A first install right after a self-update uses that copied-in |
| 94 | + mirror once; the next refresh uses the clone copy. |
| 95 | +- No version or identifier per script, and no ahead/behind reporting: not needed |
| 96 | + for the benefit (YAGNI). If a box ever needs to report which updater version |
| 97 | + it runs, add it then. |
| 98 | + |
| 99 | +## Forward-compatibility: rolling box vs pinned APK (boundary plus follow-up) |
| 100 | + |
| 101 | +dash-node and the rootfs now update independently of the APK (self-update, plus |
| 102 | +this change). The APK is pinned per install. This creates brain (box) / body |
| 103 | +(APK) version skew: a newer box can run updater logic an older APK was not built |
| 104 | +to drive. |
| 105 | + |
| 106 | +Boundary this ADR sets, so K2GO-440 does not make the skew worse: |
| 107 | + |
| 108 | +- A self-update to an updater MUST preserve the REST contract and the |
| 109 | + status/JSON shape that shipped APKs parse: the endpoint paths, the |
| 110 | + `done: N downloaded, R reused, K failed` and `result: up-to-date` log lines, |
| 111 | + and the status fields. An additive, contract-preserving change ships freely |
| 112 | + via self-update. |
| 113 | +- A change that BREAKS that contract is gated by the mechanism that already |
| 114 | + exists: bump the dash-node version and raise the per-module minimum in |
| 115 | + `DashNodeRequirement` (the app already checks it and degrades gracefully). A |
| 116 | + contract break is therefore never silent. |
| 117 | + |
| 118 | +The general policy for a rolling box against a stale APK (capability |
| 119 | +negotiation, a box-declared minimum APK, an "update your app" prompt) is larger |
| 120 | +than K2GO-440 and is deferred to its own ticket to analyze. This ADR only fixes |
| 121 | +the boundary above so current changes stay safe. |
| 122 | + |
| 123 | +## Consequences |
| 124 | + |
| 125 | +- add-ons and build-assets updaters become fixable without a rebake, like |
| 126 | + Forgejo. |
| 127 | +- The install and bake path is unchanged (baked copy), so a fresh install is |
| 128 | + unaffected. |
| 129 | +- The repo is the single source: no upstream dependency, no cross-repo |
| 130 | + arbitration. |
| 131 | +- The brain/body skew is bounded (contract stability plus the existing version |
| 132 | + gate); the general policy is a separate follow-up ticket. |
0 commit comments