Skip to content

feat: surface update info via status and daemon startup#23

Merged
StuBehan merged 1 commit intomainfrom
feat/status-update-info
Apr 30, 2026
Merged

feat: surface update info via status and daemon startup#23
StuBehan merged 1 commit intomainfrom
feat/status-update-info

Conversation

@StuBehan
Copy link
Copy Markdown
Collaborator

Summary

Best-effort PyPI update check that's script-friendly by default: the network call only fires from `stackvox status` and at daemon startup. Hooks, CI runs, `say`, `speak`, `stackvox-say` — every script path stays silent. Users see the notice when they're already looking at the terminal.

How it surfaces

Where When Output channel
`stackvox status` Always (synchronous fetch with 2s timeout — already an interactive query) stdout, alongside daemon state
`stackvox serve` startup Once at boot, in a background thread daemon's stderr log
Per-invocation stderr Opt-in only via `STACKVOX_UPDATE_NOTICE=1` — reads cache, never fetches stderr

Default behaviour

```
$ stackvox status
running (pid 12345) on /Users/.../daemon.sock
version: 0.3.1 (update available: 0.3.1 → 0.4.0 (run `pipx upgrade stackvox`))

$ stackvox serve
[stackvox] daemon listening on … (pid 12345)
[stackvox] update available: 0.3.1 → 0.4.0 (run `pipx upgrade stackvox`)
```

Disable / opt-in env vars

Var Effect
`STACKVOX_NO_UPDATE_CHECK=1` Disable entirely. No fetch, no notice.
`STACKVOX_UPDATE_NOTICE=1` Print one-line stderr notice on every invocation (cache-only).
`CI`, `GITHUB_ACTIONS`, `BUILDKITE`, `CIRCLECI`, `GITLAB_CI`, `TRAVIS` Auto-skipped to keep build logs clean.

Mechanics

  • 24h cache at `~/.cache/stackvox/update-check.json` (`{"checked_at": "...", "latest": "..."}`).
  • Cache TTL exceeded → re-fetch. Fetch failure → fall back to stale cache. No cache + failed fetch → silent no-op.
  • 2s timeout, swallows all network/parse errors as best-effort.
  • `_current_version()` reads installed metadata via `importlib.metadata` — late-bound to dodge a circular import.

Tests

24 new cases in `tests/test_updates.py`: version comparison (parametrized), disable env vars, cache I/O round-trip + corruption + missing-keys, fetch happy / error / malformed-JSON / disabled paths, `cached_update` and `check_for_update` freshness + stale-fallback, `format_notice`.

`tests/test_cli.py` adds a global fixture mocking `updates.check_for_update` / `cached_update` so the existing suite never hits PyPI, plus dedicated cases for status's update line and the opt-in notice.

108 tests, all green. ruff + mypy clean. Coverage of the new module is at 100% from the new tests.

Test plan

  • CI passes.
  • Manual: bump local pyproject to 0.0.1, run `stackvox status` — should fetch PyPI and report 0.3.1 (or whatever is current) is available.
  • Manual: `stackvox serve` from a fresh terminal — daemon log includes update notice (or not, if you're on latest).
  • Manual: `STACKVOX_UPDATE_NOTICE=1 stackvox say "hi"` — stderr has the notice.
  • Manual: `STACKVOX_NO_UPDATE_CHECK=1 stackvox status` — no PyPI call, no notice.

Adds a best-effort PyPI update check that's deliberately script-friendly:
the network call only fires from `stackvox status` and `stackvox serve`
startup. Hooks, CI, `say`, `speak`, `stackvox-say` — every script path
stays silent unless the user opts in explicitly.

 - stackvox/updates.py: cache-backed PyPI fetch
   - 24h cache at ~/.cache/stackvox/update-check.json
   - 2s timeout, fails silently on network/parse errors
   - Auto-skipped when CI/GITHUB_ACTIONS/etc. are set
   - STACKVOX_NO_UPDATE_CHECK=1 disables entirely

 - cli._cmd_status: synchronous fetch (acceptable here — `status` is
   the canonical "is everything OK?" query and the user is at a
   terminal). Always prints `version: X.Y.Z` plus the upgrade notice
   if applicable.

 - daemon.serve: spawns a background thread to do the check at startup
   and log the notice via the daemon's stderr — high-leverage moment
   to surface "you should upgrade" because the user's at the terminal.
   Doesn't block daemon startup.

 - cli.main: STACKVOX_UPDATE_NOTICE=1 turns on a per-invocation stderr
   notice for users who want the gh-style behaviour. Off by default.
   Reads cache only — never fetches on this path.

 - tests/test_updates.py (new): 24 cases covering version comparison,
   disable env vars, cache I/O round-trip + corruption, fetch
   happy/error/disabled paths, cached_update + check_for_update
   freshness, fallback-to-stale-cache, and format_notice.

 - tests/test_cli.py: autouse fixture mocks `updates.check_for_update`
   and `cached_update` so existing tests don't hit PyPI. New cases
   for status's update line and the opt-in stderr notice.

 - README: daemon section explains the policy and the two env vars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@StuBehan StuBehan force-pushed the feat/status-update-info branch from b3c7fb1 to 10f7e9e Compare April 30, 2026 21:56
@StuBehan StuBehan merged commit 6f0a405 into main Apr 30, 2026
9 checks passed
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