Update check: zero-config default remote + "new commits" channel toggle¶
Motivation¶
Two papercuts in Settings → Updates:
- Required manual configuration. The upgrade subsystem stayed inert until
an operator set
UPGRADE_GIT_REMOTEon the Container App, so a fresh control plane showed "NOT CONFIGURED" and never surfaced updates. - Releases-only discovery. The check only looked at
refs/tags/vX.Y.Z, so a control plane sitting between releases could not see that the tracking branch had moved ahead — there was no "preview / new commits" channel like most apps offer.
User-facing change¶
- Works with zero configuration. When
UPGRADE_GIT_REMOTEis unset, the check now falls back to the project's own public remote (DEFAULT_GIT_REMOTE = https://github.com/dotnetpower/elb-dashboard.git). Operators can still override via the env var. The "Not configured" state is only reachable by blanking the in-code default. - New "Allow updates from new commits" toggle (default ON). Settings → Updates gains a switch:
- ON (default) — discovery also surfaces the latest commit on the
tracking branch (
main). The "Latest available" badge readsnew commit <short-sha>when the branch has moved past the running build and no newer release tag exists. - OFF — only tagged releases are checked (the previous behaviour); the commit indicator is cleared.
- The toggle is persisted server-side (upgrade-state row) so the periodic beat discovery honours it across revisions. Flipping it re-runs a discovery check immediately so the badge updates without waiting for the next poll.
- The gear-dot / "Latest available" badge now lights up for a new commit (when the channel is on) in addition to a newer release.
API / IaC diff summary¶
api/services/upgrade/remote_tags.pyDEFAULT_GIT_REMOTE,DEFAULT_TRACK_BRANCHconstants.configured_remote()falls back to the in-code default (env still wins).- Extracted
_advertise_refs/_tags_from_refs/_branch_head_from_refs; addedfetch_branch_head(remote, *, branch)returning the tracking-branch HEAD sha (or"").fetch_release_tagsstays the primary, well-tested seam. api/services/upgrade/state.py— newtrack_commits: bool = Trueandlatest_commit_sha: str = ""fields, round-tripped through the Tables entity converters;_coerce_booldefaults a legacy row's missing column to ON.api/tasks/upgrade/pipeline.py—check_latest_inlineis channel-aware: release tags first (always), then a best-effort branch-head fetch whentrack_commitsis on. A branch-head failure never sinks the release check.api/routes/upgrade.pyPOST /api/upgrade/settings { track_commits: bool }(require_caller, consistent with the existing any-callerPOST /check) persists the toggle._mask_statefills the responsegit_remotewith the effective (masked) remote when the persisted row has none yet, so a cold-start control plane never shows "not configured".- SPA:
web/src/api/upgrade.ts(track_commits+latest_commit_shaonUpgradeStatus,setTrackCommits,isCommitUpdateAvailable),useUpgradeAvailability(commit-awareavailable,applyStatus),SettingsPanelUpdates section (channel toggle + commit badge + copy). - No Bicep / RBAC / network changes. The default remote is a hardcoded,
trusted, anonymous HTTPS URL — never caller-controlled — so the SSRF posture
of
remote_tagsis unchanged.
Scope boundary (follow-up)¶
This change makes the check/discovery channel-aware and config-free. The
upgrade execution pipeline (git_workspace.clone, image_builder,
reconciler, blue/green, rollback) remains keyed on vA.B.C release-tag
semver and api.__version__. Building/deploying an arbitrary tracking-branch
commit is a larger, separate change (commit→semver resolution at clone time,
backend version stamping, reconciler version-match) and is intentionally out of
scope here.
Validation¶
uv run pytest -q api/tests— 2872 passed, 3 skipped.- New:
test_configured_remote_*,test_fetch_branch_head_*,test_status_fills_effective_default_remote,test_settings_toggles_track_commits,test_check_commit_channel_*,test_check_release_only_skips_branch_head,test_track_commits_*(state round-trip + legacy default). uv run ruff check api/...(touched files) — clean.cd web && npm run build— OK;npm test -- --run— 637 passed.npx eslinton touched SPA files — clean.