Skip to content

feat(updates): background update check, status API and docs (off by default) - #108

Merged
vibesoftwarecoder merged 8 commits into
masterfrom
feat/update-check-service
Oct 9, 2026
Merged

vibesoftwarecoder merged 8 commits into
masterfrom
feat/update-check-service

Conversation

@vibesoftwarecoder

Copy link
Copy Markdown
Owner

Builds on the update-check core. This PR makes it run, exposes it, and documents it. Still off by default: with UpdateCheckEnabled false the service makes no network call.

What changes

  • UpdateCheckService: its own hosted service. Off, it idles on an options-change token. On, it checks about every 12 h (clamped 1 to 168, ±10% jitter), first check 10 to 20 min after start, backoff 15 min / 1 h / 4 h, rate-limit resets honoured, one check at a time, never throws into the host.
  • UpdateStatusProvider: holds the last result in memory and computes status and announce. It never calls GitHub. A failed check no longer switches an "update available" notice off: the last good result stays, the status is recomputed from the installed version (so it ends once you update), and the error is shown beside it. With no good result there is nothing to announce.
  • GET /api/system/updates, POST /api/system/updates/check (202 / 429 / 409; the check runs inside the request, bounded to 30 s), POST /api/system/updates/settings ({ "enabled": bool }). All behind the API key when auth is on; none is public.
  • The settings switch replaces appsettings.local.json with File.Replace: still one atomic swap, but the file keeps its existing permissions. The file can hold the API key and an administrator may have tightened its ACL, so a plain rename-over would have widened it. If something briefly holds the file open (the configuration reload, a scanner), the swap is retried a few times with a short, growing pause before it reports 500. A failed write changes nothing and the staging file is deleted. AtomicFile itself is unchanged.
  • ApiServer: the auth middleware and JSON options are split out unchanged (so tests can use them); nothing else changes.
  • Docs: security-posture.md "Outbound connections", README config rows and "Updating", CLAUDE.md, and the 0.6.20 release notes.

Safety: GitHub text (html_url, release notes) is never forwarded; links are built from the repo name and the parsed tag. The check never downloads, installs or runs anything.

Tests: service behaviour on a fake clock, status and announce rules, and the API contract over loopback HTTP through the real auth middleware and routes. Timing-dependent tests prove their overlap instead of hoping for it. Each group was mutation-checked. Full suite 1444 passed, 18 skipped; the Updates group passed 20 runs in a row.

Live-host checks still needed: TLS and proxy from SYSTEM, config reload in the running service, the ACL on update-check.json, and File.Replace onto the real appsettings.local.json.

🤖 Generated with Claude Code

vibesoftwarecoder and others added 8 commits October 9, 2026 12:20
UpdateCheckService is its own hosted service: off by default it idles on an
options-change token and makes no network call; on, it checks on a jittered
schedule with 15 min / 1 h / 4 h backoff, honours rate-limit resets, runs one
check at a time and catches every exception. UpdateStatusProvider keeps the
last result in memory and computes status and announce from it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET reads memory only; POST check (202/429/409); POST settings persists
UpdateCheckEnabled atomically into appsettings.local.json. The API key
middleware and JSON options are split out of ApiServer.Build unchanged so
tests can run the real rule against the real routes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Manual clock and counting fake handler: off makes zero calls for a simulated
day, flipping the option on starts checks, backoff, cooldown, one-at-a-time,
loop survival, stop token. Endpoint tests run over loopback HTTP through the
real auth middleware, JSON options and route mapping.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rewriting it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…notes, CLAUDE.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tch rewrites it

AtomicFile renames a staging file over the target, which makes a new file
that inherits the folder ACL and drops any tightened ACEs. The file can hold
the API key, so use File.Replace (same atomic swap, merges the old file's ACL
and attributes into the replacement) when the target exists, a plain move
when it does not, and delete the staging file on failure. AtomicFile itself
is unchanged for its other callers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A failed check no longer switches an announcement off. The last good result
stays, status is recomputed from the current installed version, and the error
is returned beside it. With no good result there is no latest release, so
nothing announces. Also removes the three new test-code warnings (blocking
waits became WaitAsync, nullability fixed).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… swap blocked by a reader

The service tests no longer wait for a quiet period. The loop counts each time
it parks on the clock or on a change, and the tests wait for that count, so a
clock step is followed by exactly the reaction it causes. The two race tests
(settings write atomicity, provider publish/read) now prove the overlap
before asserting and fail with a clear message if it never happens. The hang
tests wait for the handler to report the request in flight.

A configuration reload can hold appsettings.local.json open for a moment,
which made the swap fail about 1 in 30 under load. The swap is now retried
(6 times, growing pause) before the 500, with a test seam that proves a retry
happened.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@vibesoftwarecoder
vibesoftwarecoder merged commit 3d652a1 into master Oct 9, 2026
3 checks passed
@vibesoftwarecoder
vibesoftwarecoder deleted the feat/update-check-service branch October 20, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant