feat(update): update-check nudge + ana update self-update verb#23
Conversation
- passive cached 4h check after every verb (stderr nudge; --json suppresses) - `ana update` downloads the matching goreleaser archive, sha256-verifies, and atomically replaces the running binary (Unix rename; Windows .old swap) - new `internal/update` package (stdlib-only, 100% covered) - new `UpdateCheckInterval` field on Config (omitempty; "0"/"disable" off) - exports `cli.IsHelpArg` so leaves share one help-token check Closes #7
…path-traversal test - propagate defer Close() errors on downloadFile / writeBinary so a failed flush can't silently produce a truncated archive or staged binary and land the user with a busted `ana` - accept sha256sum's `*binary-mode` filename prefix + whitespace in checksums parser (`>= 2` fields, TrimPrefix `*` on last field) - cap checksums.txt body at 1 MiB via io.LimitReader - wrap emitStatus write error under the `update:` house style - rename update.UpdateDeps → update.Deps (matches connector.Deps shape) - add path-traversal regression test for tar.gz + zip - add ctx-cancel test for SelfUpdate
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughA passive, background update-check (cached, configurable interval) runs around CLI commands and may print a stderr nudge when a newer GitHub release exists. Adds Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant CLI as CLI (main.run)
participant Dispatch as cli.Dispatch
participant Nudge as update.CachedCheck (goroutine)
participant Cache as File Cache
participant GitHub as GitHub API
participant Stderr as Stderr
User->>CLI: Execute command
CLI->>Dispatch: dispatch and run verb
Dispatch-->>CLI: return
CLI->>Nudge: startNudge (spawn goroutine)
Nudge->>Cache: read/validate cache TTL
alt cache fresh
Nudge->>Nudge: use cached tag
else
Nudge->>GitHub: GET /releases/latest
GitHub-->>Nudge: latest tag
Nudge->>Cache: write cache
end
Nudge->>Nudge: CmpSemver(current, latest)
alt newer available
Nudge->>CLI: send nudge message via channel
else
Nudge->>CLI: send empty
end
CLI->>CLI: drainNudge (500ms)
CLI->>Stderr: print nudge if non-empty
sequenceDiagram
participant User as User
participant CLI as "ana update"
participant UpdateCmd as updateCmd
participant SelfUpdate as SelfUpdate()
participant GitHub as GitHub API
participant Download as download/extract
participant Disk as Disk/Binary
participant Stdout as Stdout
User->>CLI: ana update
CLI->>UpdateCmd: execute updateCmd
UpdateCmd->>SelfUpdate: SelfUpdate(ctx, deps, version)
SelfUpdate->>GitHub: GET /releases/latest
GitHub-->>SelfUpdate: latest tag
SelfUpdate->>SelfUpdate: CmpSemver
alt up-to-date
SelfUpdate->>Stdout: print up-to-date (or JSON)
else update needed
SelfUpdate->>GitHub: download archive & checksums
GitHub-->>SelfUpdate: archive + checksums
SelfUpdate->>SelfUpdate: verify checksum
SelfUpdate->>Download: extract binary
Download->>Disk: write .new binary
SelfUpdate->>Disk: atomic replace (.new → exe), Windows rollback if needed
SelfUpdate->>Stdout: print updated (or JSON)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/ana/main_test.go`:
- Around line 627-639: The test TestUpdateCmd_Help should use errors.Is to
compare the returned error against cli.ErrHelp to handle wrapped errors
robustly; update the assertion in TestUpdateCmd_Help to call errors.Is(err,
cli.ErrHelp) instead of err != cli.ErrHelp and add the "errors" import if
missing, leaving the rest of the test (including calling (updateCmd{}).Run and
checking the help output) unchanged.
In `@internal/update/check.go`:
- Around line 143-157: The json.Marshal error is currently ignored in
writeCache; change the call to capture and handle the error (e.g., data, err :=
json.Marshal(c)) and return a formatted error via fmt.Errorf if marshaling
fails, so writeCache consistently reports failures (refer to function writeCache
and json.Marshal; readCache already handles corrupt files so mirror defensive
error handling here).
In `@internal/update/download.go`:
- Around line 91-96: The checksum currently reads the whole file with
os.ReadFile into memory; change it to stream the archive by opening archivePath
with os.Open, create a sha256.New() hash, and io.Copy the file contents into the
hash (remember to close the file), then compute the hex string from
hash.Sum(nil) to set got; update the error messages to reflect failures from
os.Open or io.Copy as needed while keeping the same variable names (archivePath,
got) so callers remain valid.
In `@internal/update/selfupdate.go`:
- Around line 166-174: The rollback error rbErr returned by deps.Rename(oldPath,
exePath) is wrapped with %v which prevents error unwrapping; update the
fmt.Errorf call in the error branch that currently returns fmt.Errorf("update:
replace %s failed (%w); rollback also failed (%v); recover from %s", exePath,
err, rbErr, oldPath) to use %w for rbErr as well so both the original replace
error (err) and rollback error (rbErr) are properly wrapped and can be inspected
via errors.Is/errors.As when calling the function that performs the deps.Rename
and handles exePath/oldPath.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 62082717-d17f-4dee-b44d-8bbc5136d92d
⛔ Files ignored due to path filters (8)
README.mdis excluded by!**/*.mdcmd/ana/CLAUDE.mdis excluded by!**/*.mddocs/cli-readiness.mdis excluded by!**/docs/**,!**/*.mde2e/harness/CLAUDE.mdis excluded by!**/*.mdinternal/CLAUDE.mdis excluded by!**/*.mdinternal/cli/CLAUDE.mdis excluded by!**/*.mdinternal/config/CLAUDE.mdis excluded by!**/*.mdinternal/update/CLAUDE.mdis excluded by!**/*.md
📒 Files selected for processing (20)
cmd/ana/main.gocmd/ana/main_test.gocmd/ana/update.gocmd/ana/version.gointernal/cli/cli.gointernal/cli/dispatch.gointernal/config/config.gointernal/config/config_test.gointernal/update/check.gointernal/update/check_test.gointernal/update/download.gointernal/update/download_test.gointernal/update/extract.gointernal/update/extract_test.gointernal/update/selfupdate.gointernal/update/selfupdate_test.gointernal/update/semver.gointernal/update/semver_test.gointernal/update/update.gointernal/update/update_test.go
…r, streaming sha - test assertions: errors.Is(err, cli.ErrHelp) over == - writeCache: check json.Marshal error (errchkjson flags unsafe time.Time) - atomicReplace: wrap rbErr with %w too (multi-%w Go 1.20+) - fold sha256 into downloadFile via io.TeeReader; verifyChecksum becomes a pure string compare with no I/O branches; eliminates a ReadFile of the whole archive + a redundant open/seek dance
Summary
--json, ondevbuilds, and when config setsupdateCheckIntervalto"0"/"disable".ana updateverb downloads the matching goreleaser archive, sha256-verifies againstchecksums.txt, and atomically replaces the running binary (Unix rename; Windows.oldswap with rollback).internal/updatepackage is split acrosssemver.go/check.go/selfupdate.go/download.go/extract.go. 100% covered.Closes #7
Notes for reviewers
internal/updateimports nothing new — verify viago.modstill showing an emptyrequireblock.cli.Dispatch;drainNudgetimeout-bounds it so slow DNS can't delay the CLI.filepath.Base(h.Name)on every tar/zip entry, with a dedicated regression test.Close()errors on writers, tightened the sha256sum parser to accept*binary-mode, capped checksums body at 1 MiB, and renamedUpdateDeps→Depsper package convention.Summary by CodeRabbit
New Features
ana updatecommand (supports plain and JSON output) to install the latest release.Tests