feat(update): stable/beta update channel; stable follows release tags (#684)

Adds auto_update.channel: stable follows the newest vX.Y.Z release tag
(detached HEAD; pre-releases and other tags ignored), beta follows main as
before. Nothing ever moves a device backwards: a checkout newer than the
newest release keeps following main (or stays put when detached) until a
release contains its commit. Legacy configs migrate to stable when they
reach a release. Update Code, the weekly updater's preflight, and the
verifier's rollback (back to old_ref: branch or detached release) all
honour the channel. General tab Update Channel select, GET/POST
/api/v3/system/update-channel, release-aware Overview banner and Tools git
panel. New installs default to stable.

Rig fix (ledpi): /system/check-update reports update_available: false when
the channel's action is none (a detached HEAD newer than the newest
release), matching Update Code; the Tools panel no longer calls every
detached HEAD "a release".

Merged with main through #687 (heartbeat verifier, #683 login, #688
plugin_catalog, #685 Tailwind build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-30 15:35:26 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 64c7289593
commit 7804ea8f69
21 changed files with 1499 additions and 50 deletions
+16 -3
View File
@@ -336,8 +336,19 @@ everything else through `_reinstall_with_rollback()`.
- **Update Code** on the Overview tab and the automatic updater both call
`perform_core_update()` in
[`api_v3/system.py`](../web_interface/blueprints/api_v3/system.py):
`git pull --rebase`, reinstall changed requirement files, report whether a
restart is needed.
fetch branches and tags, move the checkout for the update channel, reinstall
changed requirement files, report whether a restart is needed.
- **Update channels** (`auto_update.channel`):
[`web_interface/update_channel.py`](../web_interface/update_channel.py)
decides the move. `stable` checks out the newest `vX.Y.Z` tag (detached
HEAD) when it contains the current commit; `beta` is
`git pull --rebase --autostash` on the current branch, and leaves a
detached release for `main` first. A stable device newer than the newest
release keeps pulling `main` until a release contains its commit, so no
update ever moves backwards; a config without the key is written as
`stable` once the device reaches a release. Checkouts carry uncommitted
edits across with `git stash create`/`apply`, and keep them in the stash
list if they no longer apply.
- **Automatic updates** (`auto_update.enabled`, off by default):
`AutoUpdater` in [`web_interface/auto_update.py`](../web_interface/auto_update.py)
runs in the web process, checks every 30 minutes, and updates at most
@@ -350,7 +361,9 @@ everything else through `_reinstall_with_rollback()`.
both services, waits for the web API to answer and the display service to
stay up -- and, when the display wrote a heartbeat before the update, to
keep one fresh from the restarted process (see Liveness) -- and on failure
resets to the previous commit and restarts again.
returns to where HEAD was (the branch, or detached on the previous
release; `old_ref` in the pending file), resets to the previous commit
and restarts again.
Plugin updates run only after a verified core update. State is in
`data/auto_update_state.json` and `data/auto_update_pending.json`.
- **Startup validator.** `StartupValidator`