Repository navigation
feat(update): stable/beta update channel; stable follows release tags - #684
Conversation
Devices updated by pulling main's HEAD, so every merge reached them unreleased. auto_update.channel now picks what Update Code and the weekly automatic update install: - stable: the newest vX.Y.Z tag by semver (pre-releases and other tags ignored), checked out with a detached HEAD. - beta: main, exactly as before. Nothing moves a device backwards. stable only checks out a tag that contains HEAD; a device newer than the newest release keeps following main until a release contains its commit, then moves to it. A config without the key behaves the same way and is written as stable at that point. New installs get stable from the config template and the installer's fallback config. - web_interface/update_channel.py: tag parsing, channel resolution, checkouts that carry edits across like pull --autostash. - perform_core_update, the auto-updater preflight and check-update are channel-aware; the Overview banner names the release on stable. - The health check's rollback returns to the previous ref (branch or detached) via old_ref in the pending file; files without it roll back as before. - General tab: Update Channel select; GET/POST /api/v3/system/update-channel. - Docs and CHANGELOG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (21)
Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughAdds stable and beta update channels. Stable selects a release tag when it contains the current commit; beta follows ChangesUpdate Channels
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant User
participant UpdateAPI
participant ChannelResolver
participant Git
participant Config
User->>UpdateAPI: Request a core update
UpdateAPI->>ChannelResolver: Resolve configured channel
ChannelResolver->>Git: Inspect tags, branches, and commit ancestry
Git-->>ChannelResolver: Return refs and ancestry results
ChannelResolver-->>UpdateAPI: Return update action
UpdateAPI->>Git: Check out release or update branch
UpdateAPI->>Config: Persist stable channel when migration applies
UpdateAPI-->>User: Return update result and channel information
Merge Risk: ⚪ Minimal · up to This change adds Stable and Beta update channels, channel-aware updates, and rollback to the prior branch or release. The reported hardware and scratch-clone checks match the intended behavior, and no confirmed defect remains. The change appears ready to merge. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Channel choices are constrained and reuse the existing update permissions. However, an interrupted channel switch can discard plugin edits before their recovery information is durably recorded, weakening update failure containment. Retained concerns
Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 105 functions across 9 files. (12 skipped: 12 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 130 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…ng them as SQL "Update channel set to ..." in an f-string matches Bandit B608's UPDATE ... SET pattern. The messages now say "Switched to the beta/stable channel." No SQL is involved; the text is only shown in the web UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hardware result (ledpi, Pi 4, 2026-09-30, branch at c51f2c6): the update paths pass, but the check-update banner is wrong on a detached checkout that is newer than the newest release. I did not run a real update on ledpi's checkout. The flows ran in a scratch clone and were then deleted. Fetch time on the Pi. Live, read-only, on ledpi (detached at the branch head, config channel unset):
Real
Issue: the last row, which is ledpi's real state. On a detached checkout newer than the newest release, Suggested fix: in Not tested: ledpi was restored to main with config byte-identical to its backup. 🤖 Generated with Claude Code |
Found on the ledpi rig: a detached checkout newer than the newest release (it pulled main, then was detached) has channel action "none" -- Update Code leaves it where it is until a release includes it -- but /system/check-update fell through to the origin/main comparison, reported update_available: true, and the Overview banner offered "A new LEDMatrix update is available (12 commits)" with an Update Now that then answered "already up to date". check-update now reports update_available: false whenever the channel's action is none, with channel_message in the General tab's words. An on-branch device behind origin/main (beta, or stable waiting on a branch) still gets update_available: true. The Tools git panel said "On a release" for every detached HEAD; git-info now carries current_release and channel_message, and the panel names the release or says what the channel does with a checkout that is not on one. Tests cover both directions against Update Code's actual behaviour; mutation-checked (dropping the action-none rule, widening it to every waiting device, and never naming the release each fail a test). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - CHANGELOG.md: kept both; "Update channels" first, then main's sections. - docs/ARCHITECTURE.md: the verifier paragraph now says both things -- the heartbeat check from #687 and the return to old_ref from this branch. - web_interface/blueprints/pages_v3.py: the General partial gets both update_channel_status (this branch) and web_login (#683). Follow-ups for main's changes: - #688: the channel API test fixture stubs api_v3.plugin_catalog, not the retired plugin_manager. - #683: test_web_auth pins that GET/POST /system/update-channel need login from the LAN when login is on (the default rule; mutation-checked by exempting the endpoints). - #685: scripts/build_css.py regenerates identical CSS; --check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
Devices update by pulling
main's HEAD (Update Code, and the weekly automatic update, both throughperform_core_update), so every merge tomainreached devices before it was released. This adds the owner-approved stable/beta update channel.Design
auto_update.channelinconfig.json:stablevX.Y.Ztag by semver, checked out with a detached HEAD. Pre-releases (v3.8.0-rc1), build metadata, leading zeros and other tags (nightly,v3.7) are ignored.betamain, exactly as before (git pull --rebase --autostash). From a detached release it first moves ontomain.No downgrade, ever.
stableonly checks out a tag that contains HEAD (git merge-base --is-ancestor HEAD <tag>).checkout_release()checks this again and refuses to move backwards. A device newer than the newest release (anything that pulledmainsince that release) keeps followingmain("waiting") until a release contains its commit, then moves to it. On a detached HEAD with no branch to follow it stays where it is.Migration. A config with no key behaves as beta until the newest tag contains HEAD. At that point it moves to stable and writes
channel: "stable"(Update Code, the weekly preflight, and the case where it is already on the release commit). Because "stable, but newer than the newest release" also followsmain, a missing key andstablebehave the same, so nobody is downgraded either way.ConfigManager._migrate_configcopies new template keys into existing configs. Most existing devices will therefore getchannel: "stable"written on first load of this version, not at migration time. Behaviour is still what the owner specified: they keep followingmainuntil a release contains their commit. It just means the key is persisted earlier than the migration rule describes.New installs default to stable:
config/config.template.jsonhas it, and so does the installer's fallback config for when the template is missing. The installer diff is 4 lines inside that heredoc.main's tip, which is usually newer than the newest release. So it followsmainuntil the next release, then moves to stable. Checking out the newest tag at install time would mean touching the one-shot/installer flow, which other branches are changing. I left that as a follow-up.Where it lives
web_interface/update_channel.py(new, stdlib only):resolve()returns the action:none/checkout_tag/pull/switch_to_beta) andfetch(--tags --force origin)checkout()carries uncommitted edits across a checkout the waypull --autostashdoes (git stash create→ reset → checkout →stash apply). If the edits no longer apply, they are stored in the stash list rather than left half-merged, as git's own autostash does.origin/main(checkout -B main origin/main) when localmainhas no commits of its own. Otherwise the edits would be applied to an older commit where plugin files may not exist yet.perform_core_update(api_v3/system.py) fetches branches and tags, resolves the channel, then pulls or checks out. The existing explicit stash for Update Code, the no-stash refusal for the automatic updater, dependency sync, restart detection and the uninstalled-plugin purge are unchanged.AutoUpdater.preflightis channel-aware:origin/mainis blocked.rolled_back_headis compared against the tag commit, so a release that failed its health check is not retried.old_ref(branch name, or""when detached) andreleasein the pending file. Messages name the release: "Updated LEDMatrix from 6508b43 to v1.4.0 (7720d14)".auto_update_verify.pyrollback first returns toold_ref:checkout --force <branch>, or--detach <old>when the old ref was a detached release, thenreset --hard <old>as before.mainback to the release commit.old_ref(written by older updaters) roll back exactly as before./system/check-updateon stable compares release tags, notmain. It addschannel,waiting,newest_release,current_releaseandtarget_version, and fetches onlymainand tags, since it runs on page loads with a 10 s timeout.git describe --tags) already readsv1.0.0on a detached tag, as the WSL proof shows.main).GET /api/v3/system/update-channel(?fetch=1fetches first) andPOST {"channel": "stable"|"beta"}. Anything else is a 400. The response message says when stable keeps followingmain.auto_update_channelis also accepted on/config/main.except; the blueprint handler suppliesdescribe_exception.Rig result (ledpi, Pi 4) and the fix it led to
Tested on ledpi in a scratch clone against the real GitHub tags:
stable, local edit carried;mainahead of v3.7.0 keeps pullingmain;maintrackingorigin/main;GET /system/update-channeland the General tab status line were correct.none, so Update Code leaves the device where it is. But/system/check-updatefell through to theorigin/maincomparison. It returnedupdate_available: true, and the Overview banner offered "A new LEDMatrix update is available (12 commits)" with Update Now, which then answered "already up to date".Fix (commit 6c99803):
check-updatereportsupdate_available: falsewhenever the channel's action isnone.channel_messagecarries the General tab's own wording ("...it stays on it and moves to the first release that includes it").origin/main(beta, or stable waiting on a branch) still getsupdate_available: true.git-infonow returnscurrent_releaseandchannel_message, and the panel either names the release or explains what the channel does with a checkout that is not on one.update_availableis true.nonerule, widening it to every waiting device, and never naming the release.Tests
test/test_update_channel.py(62 tests; real git against a throwaway bare origin and device clone):v3.10.0>v3.9.0); pre-releases, junk and leading zeros ignored; annotated tags resolve to their commitresolve()andcheckout_release()perform_core_updatefor stable, beta, and stable→beta→stable round tripsmainis not dragged back)Four mutations were each caught by the suite: dropping the ancestry check, accepting pre-release tags, never migrating, and ignoring
old_refin the rollback.Full suite vs baseline (Windows, Python 3.14, Pillow 12.3.0 before and after both runs). The baseline was a separate temporary worktree at
origin/main(c3a7a11), the commit this branch is rebased on.Merge with main (64c7289). Merge commit 27fe9c9.
old_ref.pages_v3._load_general_partial: passes bothupdate_channel_statusand feat(web): optional web login and API tokens, off by default (stacked on #674) #683'sweb_login.auto_update_verify.py, so both apply. All oftest_auto_update_verifyand this PR's rollback tests pass.GETandPOST /system/update-channelreturn 401 from the LAN, through the defaultbefore_requestrule. That is now pinned intest_web_authand mutation-checked by exempting the endpoints.plugin_catalog. None of this PR's routes usedplugin_manager.build_css.pyregenerates identical CSS, and--checkpasses.origin/mainworktree (Windows, Python 3.14, Pillow 12.3.0 before and after both runs):check_types.py,build_css.py --check, and the JS unit suites. The DOM suites are left to CI's Web UI JS job.After the squash merge (7804ea8): main's tree is identical to the tested merge tree 27fe9c9 (tree 032f886a). A full suite run on final main gave 61 failed, 6808 passed, 143 skipped, 6 errors. The FAILED/ERROR IDs are identical to the
origin/mainbaseline (Pillow 12.3.0 before and after).WSL proof (Ubuntu 24.04, git 2.43, Python 3.12)
Setup: a local bare
originwithv1.0.0,v1.1.0-rc1(pre-release),nightly(junk) and an unreleasedmaintip, plus a device clone. The realperform_core_update,AutoUpdaterandVerifierran against it. Only sudo and systemd were faked. All checks passed:nightlyormain, and the config was saved asstable. A plugin-repos edit and the installer's mode-only change were carried across, and nothing was left in the stash.get_git_version()readsv1.0.0-dirty.main, trackingorigin/main, and the plugin edit was kept.mainforward and reported "newer than the newest release ... keeps following main".main. Rollback returned to v1.3.0 detached, andmainwas not dragged back.main(old_ref=main). Rollback returned to branchmainat the old commit.Also run under Linux:
test_update_channel,test_auto_update,test_auto_update_verify,test_update_prompts_restart,test_update_check_reports_failureandtest_git_pull_resolution: 181 passed. One test was deselected because it runsgit ls-fileson the project, and the /tmp copy is not a checkout.bash -n first_time_install.shpasses. The config section, sliced out and sourced, was run as the user and as root withSUDO_USER:channel: "stable"with auto-update on or offstabletest_install_*,test_web_autostart_default): 77 passed.Still needs checking on a Pi
Not yet tested on hardware (needs root):
ledmatrix-update-verifyand its rollback, end to end. The rest of this list is from before the rig run.mainafter 3.7.0: the General tab shows "waiting", and Update Code keeps pullingmain.ledmatrix-update-verifyend to end on a detached tag: a weekly update to a newer release passes. Also force a failure (e.g. stop the web service during the check) and confirm the rollback lands back on the previous tag and the services come back.mainis stale, over a real (slow) network.fetch --tags origin mainmust stay inside the 10 s timeout.one-shot-install.shon a stable device (it pullsmaininto the detached HEAD; afterwards the device waits for the next release).Not changed:
origin/main.main.🤖 Generated with Claude Code
Summary by CodeRabbit