Skip to content

feat(update): update-check nudge + ana update self-update verb#23

Merged
bradfair merged 3 commits into
mainfrom
feature/update-check-and-self-update
Apr 22, 2026
Merged

feat(update): update-check nudge + ana update self-update verb#23
bradfair merged 3 commits into
mainfrom
feature/update-check-and-self-update

Conversation

@bradfair

@bradfair bradfair commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Passive cached 4h check after every verb prints a one-line stderr nudge when a newer GitHub release is out. Suppressed on --json, on dev builds, and when config sets updateCheckInterval to "0" / "disable".
  • New ana update verb downloads the matching goreleaser archive, sha256-verifies against checksums.txt, and atomically replaces the running binary (Unix rename; Windows .old swap with rollback).
  • Stdlib-only; new internal/update package is split across semver.go / check.go / selfupdate.go / download.go / extract.go. 100% covered.

Closes #7

Notes for reviewers

  • internal/update imports nothing new — verify via go.mod still showing an empty require block.
  • The nudge goroutine runs concurrently with cli.Dispatch; drainNudge timeout-bounds it so slow DNS can't delay the CLI.
  • Path-traversal safe: filepath.Base(h.Name) on every tar/zip entry, with a dedicated regression test.
  • Review-pass fixes pushed on top of the initial commit: propagated Close() errors on writers, tightened the sha256sum parser to accept *binary-mode, capped checksums body at 1 MiB, and renamed UpdateDepsDeps per package convention.

Summary by CodeRabbit

  • New Features

    • Added ana update command (supports plain and JSON output) to install the latest release.
    • Passive, asynchronous update checks now run during CLI commands and optionally nudge users when upgrades are available.
    • Configurable update-check interval persisted in config (can disable checks).
  • Tests

    • Extensive test coverage for update checking, caching, download, extraction, installation, semver comparison, and related behaviors.

- 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
@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 84f199a8-4844-4f0c-84e4-142452d7da6d

📥 Commits

Reviewing files that changed from the base of the PR and between 7e90edc and 77e6417.

⛔ Files ignored due to path filters (1)
  • internal/update/CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (6)
  • cmd/ana/main_test.go
  • internal/update/check.go
  • internal/update/check_test.go
  • internal/update/download.go
  • internal/update/download_test.go
  • internal/update/selfupdate.go

📝 Walkthrough

Walkthrough

A passive, background update-check (cached, configurable interval) runs around CLI commands and may print a stderr nudge when a newer GitHub release exists. Adds ana update to download, verify, extract, and atomically replace the running binary. Adds UpdateCheckInterval config and exported IsHelpArg() helper.

Changes

Cohort / File(s) Summary
CLI main & tests
cmd/ana/main.go, cmd/ana/main_test.go
Start a buffered nudge goroutine (startNudge) around cli.Dispatch, drain with timeout (drainNudge), register "update" verb; tests for nudge skip/drain and verb presence.
Update verb & version help
cmd/ana/update.go, cmd/ana/version.go
New updateCmd implementing self-update (delegates to update.SelfUpdate), unify help detection using cli.IsHelpArg.
CLI helper refactor
internal/cli/cli.go, internal/cli/dispatch.go
Export previously internal help-check as IsHelpArg and switch call sites to it.
Config shape & tests
internal/config/config.go, internal/config/config_test.go
Add UpdateCheckInterval *string to Config with omitempty; tests ensure round-trip and non-serialization when nil.
Update check core + semver
internal/update/check.go, internal/update/semver.go, internal/update/update.go, internal/update/*_test.go
New update-check subsystem: LatestRelease fetch, CachePath, ParseInterval, CachedCheck (file cache with TTL), semver comparator, and extensive unit tests.
Self-update execution & helpers
internal/update/selfupdate.go, internal/update/download.go, internal/update/extract.go, internal/update/*_test.go
Implements SelfUpdate flow: download archive & checksums, checksum verification, extract binary from tar.gz/zip, atomic replace (Windows rollback), and comprehensive tests including sad paths.
Test harness & utilities
internal/update/update_test.go
Shared test utilities: fake HTTP doer, in-memory archive builders, releaseServer fixture, staging helpers, and URL redirection helpers for tests.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A little nudge that softly hops,
I check the sky for version tops,
If newer shines upon the view,
I whisper: "Run ana update" to you,
Then dance—no deps—just stdlib hops.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(update): update-check nudge + ana update self-update verb' clearly summarizes the main changes: adding an update-check feature and a new update verb.
Description check ✅ Passed The description covers summary, type selection, and includes all checklist items. It provides sufficient context on the update-check behavior, the new ana update verb, and implementation notes.
Linked Issues check ✅ Passed The code changes fully implement the requirements in issue #7: passive cached update check (4h default, configurable), stderr nudge suppression for --json/dev/disabled, ana update verb with archive download/verification/atomic replace, semver comparison without new deps, 100% test coverage, and zero new go.mod dependencies.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to implementing the update-check nudge and ana update self-update verb. No unrelated modifications to other features or subsystems are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/update-check-and-self-update

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ba9159d and 7e90edc.

⛔ Files ignored due to path filters (8)
  • README.md is excluded by !**/*.md
  • cmd/ana/CLAUDE.md is excluded by !**/*.md
  • docs/cli-readiness.md is excluded by !**/docs/**, !**/*.md
  • e2e/harness/CLAUDE.md is excluded by !**/*.md
  • internal/CLAUDE.md is excluded by !**/*.md
  • internal/cli/CLAUDE.md is excluded by !**/*.md
  • internal/config/CLAUDE.md is excluded by !**/*.md
  • internal/update/CLAUDE.md is excluded by !**/*.md
📒 Files selected for processing (20)
  • cmd/ana/main.go
  • cmd/ana/main_test.go
  • cmd/ana/update.go
  • cmd/ana/version.go
  • internal/cli/cli.go
  • internal/cli/dispatch.go
  • internal/config/config.go
  • internal/config/config_test.go
  • internal/update/check.go
  • internal/update/check_test.go
  • internal/update/download.go
  • internal/update/download_test.go
  • internal/update/extract.go
  • internal/update/extract_test.go
  • internal/update/selfupdate.go
  • internal/update/selfupdate_test.go
  • internal/update/semver.go
  • internal/update/semver_test.go
  • internal/update/update.go
  • internal/update/update_test.go

Comment thread cmd/ana/main_test.go
Comment thread internal/update/check.go
Comment thread internal/update/download.go Outdated
Comment thread internal/update/selfupdate.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
@bradfair bradfair added this pull request to the merge queue Apr 22, 2026
Merged via the queue into main with commit 8da2017 Apr 22, 2026
10 checks passed
@bradfair bradfair deleted the feature/update-check-and-self-update branch April 22, 2026 21:46
@hpt-bot hpt-bot Bot mentioned this pull request Apr 22, 2026
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.

feat: update check on every run + ana update self-update command

1 participant