Skip to content

Commit 72d41a9

Browse files
Merge pull request #623 from appdevforall/feat/K2GO-440-updater-self-update
K2GO-440 fix(updaters): source the add-ons and build-assets mirrors from the self-updating clone
2 parents 9590441 + 34554f1 commit 72d41a9

3 files changed

Lines changed: 146 additions & 2 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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.

‎tools/code-addons-refresh.sh‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ PID=/var/run/code-addons-refresh.pid
2121
# base and the catalog serve-base come from mirror_addons.py's own defaults, so
2222
# they are not restated here.
2323
SERVE=/library/www/code-addons
24-
MIRROR=/opt/iiab/iiab/roles/code_addons/files/mirror_addons.py
24+
# K2GO-440: prefer the mirror from the self-updating clone, so a mirror fix ships via the dash-node
25+
# self-update (git reset on /opt/iiab-android) with NO rebake; fall back to the copy the overlay
26+
# places in the ansible roles dir at bake. Same idea as forgejo-refresh.sh sourcing from the clone.
27+
# The code_addons role is ours only (a Knowledge to Go overlay, not an IIAB upstream role).
28+
MIRROR_CLONE=/opt/iiab-android/tools/upstream-patches/overlays/roles/code_addons/files/mirror_addons.py
29+
MIRROR_BAKED=/opt/iiab/iiab/roles/code_addons/files/mirror_addons.py
30+
MIRROR=$([ -f "$MIRROR_CLONE" ] && echo "$MIRROR_CLONE" || echo "$MIRROR_BAKED")
2531

2632
: > "$LOG" 2>/dev/null || true
2733
echo running > "$STATUS" 2>/dev/null || true

‎tools/code-assets-refresh.sh‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ PID=/var/run/code-assets-refresh.pid
2121
# base and the serve-base come from mirror_code_assets.py's own defaults, so they
2222
# are not restated here.
2323
SERVE=/library/www/code-assets
24-
MIRROR=/opt/iiab/iiab/roles/code_assets/files/mirror_code_assets.py
24+
# K2GO-440: prefer the mirror from the self-updating clone, so a mirror fix ships via the dash-node
25+
# self-update (git reset on /opt/iiab-android) with NO rebake; fall back to the copy the overlay
26+
# places in the ansible roles dir at bake. Same idea as forgejo-refresh.sh sourcing from the clone.
27+
# The code_assets role is ours only (a Knowledge to Go overlay, not an IIAB upstream role).
28+
MIRROR_CLONE=/opt/iiab-android/tools/upstream-patches/overlays/roles/code_assets/files/mirror_code_assets.py
29+
MIRROR_BAKED=/opt/iiab/iiab/roles/code_assets/files/mirror_code_assets.py
30+
MIRROR=$([ -f "$MIRROR_CLONE" ] && echo "$MIRROR_CLONE" || echo "$MIRROR_BAKED")
2531

2632
: > "$LOG" 2>/dev/null || true
2733
echo running > "$STATUS" 2>/dev/null || true

0 commit comments

Comments
 (0)