feat(cli): cached non-blocking update notice + civitai upgrade self-update - #39
Merged
Conversation
…-update Adds two related features in one PR: PART 1 — daily, cached, non-blocking "new version available" notice - A root PersistentPostRun hook prints at most ONE dim stderr line after any successful command. It never does a synchronous network call: it reads a cache (~/.config/civitai/update-check.json) and, when stale, spawns a DETACHED `civitai __update-check` (hidden subcommand) that fetches the latest release and rewrites the cache. The current run uses the cached value; the refresh lands for next time. First run (no cache) only kicks off the refresh. - Refresh at most once / 24h (last_check); notice shown at most once / 24h (last_notified) even while behind. Corrupt/missing cache => empty, fail-silent. - Suppressed when: stderr is not a TTY, CI env is set, --no-update-check / CIVITAI_NO_UPDATE_CHECK, or the command is version/upgrade/completion/help/ __update-check/__complete*. Only fires when current parses and latest > current. - stderr only — never pollutes stdout, so pipes/scripts are unaffected. - Reuses the existing fetchLatestRelease / semver helpers from update_check.go; `version` keeps its own explicit synchronous check (no double-notify). PART 2 — `civitai upgrade` self-update - Resolves the latest release (unauthenticated GitHub, no token ever sent). Already >= latest and not --force => "already up to date" no-op. - Homebrew detection: if the resolved executable lives under a brew path (/Cellar/, /Caskroom/, /opt/homebrew, /usr/local/Homebrew|Cellar, /home/linuxbrew/.linuxbrew), prints the brew upgrade command instead of self-replacing (--force overrides). - Otherwise downloads the platform tarball + checksums.txt, VERIFIES the tarball SHA-256 against checksums.txt and ABORTS on mismatch (binary left untouched), extracts the binary, and atomically replaces the running executable via github.com/minio/selfupdate. Permission-denied => clear sudo/brew/go-install guidance, non-zero exit, no half-written binary. Detaching uses a small build-tagged platform split (unix Setpgid / windows CREATE_NEW_PROCESS_GROUP); the parent never Waits. Spawn + apply + executable-path + TTY are behind injectable seams so tests assert behavior without forking or replacing the test binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit 🟡#1 (https/host enforcement). Asset download URLs come straight out of the GitHub release JSON and were fetched with http.DefaultClient, which follows redirects including https->http downgrades with no scheme/host check. An http:// checksums.txt + http:// tarball pair would make the SHA-256 gate self-referential (both halves attacker-controlled). - validateAssetURL: require scheme==https AND host in an allowlist {github.com, objects.githubusercontent.com, release-assets.githubusercontent.com}; reject (abort, no download) otherwise. Applied to BOTH the tarball and checksums.txt URLs before any bytes are read. - assetDownloadClient: dedicated *http.Client with CheckRedirect that re-validates every hop, so an https->http downgrade redirect is rejected rather than followed. Context timeouts + body-size caps kept. - Checksum-verify-before-replace gate unchanged (defense-in-depth on transport). Tests: validateAssetURL allow/reject table; an http:// asset URL and an off-host https asset URL each abort before download with the binary untouched; the asset client rejects an https->http redirect. httptest fixtures now serve TLS and inject the loopback host via a test seam so the production allowlist is never weakened to pass tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two related features, one PR:
1. Cached, non-blocking daily "new version available" notice
A root
PersistentPostRunhook prints at most one dim line to stderr after any successful command:It never blocks: it reads a cache (
~/.config/civitai/update-check.json) and, when stale, spawns a detachedcivitai __update-check(hidden subcommand) that fetches the latest release and rewrites the cache. The current run uses the cached value; the refresh is for next time. First run (no cache) just kicks off the refresh — no notice.Anti-annoyance rules (all enforced + tested):
last_check); notice at most once / 24h (last_notified) even while behind.--no-update-check/CIVITAI_NO_UPDATE_CHECK, or the command isversion/upgrade/completion/help/__update-check/__complete*.latest > current.Reuses the existing
fetchLatestRelease/ semver helpers;versionkeeps its own explicit synchronous check (no double-notify).2.
civitai upgradeself-update--force→ "already up to date" no-op.brew upgrade civitai/tap/civitaiinstead of self-replacing (--forceoverrides).checksums.txt, verifies the tarball SHA-256 againstchecksums.txtand ABORTS on mismatch (binary untouched), extracts the binary, and atomically replaces the running executable viagithub.com/minio/selfupdate. Permission-denied → clear sudo/brew/go-install guidance, non-zero exit, no half-written binary.Detaching uses a build-tagged platform split (unix
Setpgid/ windowsCREATE_NEW_PROCESS_GROUP); the parent neverWaits. Spawn / apply / executable-path / TTY are behind injectable seams so tests assert behavior without forking or replacing the test binary.Security
The download path has two integrity controls; they are defense-in-depth, not redundant:
checksums.txtand a mismatch aborts before the binary on disk is touched.checksums.txt, each URL is validated to behttpson an allowlisted GitHub release host (github.com,objects.githubusercontent.com,release-assets.githubusercontent.com); anything else aborts the upgrade before any bytes are read. The asset download client also uses aCheckRedirectthat re-applies this check on every hop, so anhttps → httpdowngrade (or host-pivot) redirect is rejected rather than followed. This closes the case where anhttp://checksums.txt+http://tarball pair would make the checksum gate self-referential (both halves attacker-controlled).Accepted baseline limitation + follow-up:
checksums.txtintegrity currently rests on HTTPS-to-GitHub (the standard goreleaser baseline) — it is not cryptographically signed. The highest-value follow-up is to add a goreleasersigns:block (cosign keyless or minisign) and havecivitai upgradeverify a signature overchecksums.txtbefore trusting it. That is tracked as a follow-up, not in this PR. Until then, the checksum-verify-before-replace gate plus this PR's https/host enforcement are the current integrity controls.New dependencies
github.com/minio/selfupdate— well-vetted cross-platform atomic self-replace (the download + checksum verify are done by us; selfupdate only does the verified-binary apply).golang.org/x/term— TTY detection on stderr.Verification
go build ./...,go vet ./...,go test ./...green;gofmt -lclean. Cross-compiles for windows/amd64, darwin/arm64, linux/amd64 (rc=0 each).validateAssetURLallow/reject table (https GitHub hosts allowed; http downgrade, off-host, host-suffix spoof, wrong scheme, raw.githubusercontent rejected), an http:// asset URL aborts the upgrade before download with the binary untouched, an off-host https asset URL aborts before download, and the asset client rejects anhttps → httpredirect. The httptest fixtures now serve TLS and inject the loopback host via a test seam so the production allowlist is never weakened to pass tests.civitai --helplistsupgrade;__update-checkis hidden;--no-update-checkis a persistent flag.civitai upgrade --helprenders.v0.1.11GitHub release on linux/amd64: downloaded the real tarball + checksums, verified SHA-256, atomically replaced a throwaway binary — the replaced binary then reportedcivitai 0.1.11(real commit/build date). The detached__update-checkrefresh ran independently under a PTY and rewrote the cache (last_check→ today, corrected a stalelatest_version) while the parent command returned in ~21ms.Note
The notice appears only after a successful command — cobra runs
PersistentPostRunonly when the command'sRunEsucceeds. This matches thegh/npmupdate-notifiermodel (don't append a notice to a failed command).🤖 Generated with Claude Code