Skip to content

fix(update): wait briefly for the background update check before exiting - #205

Merged
bojanz merged 4 commits into
mainfrom
cli-199-wait-briefly-for-the-background-update-check-before-exiting
Oct 13, 2026
Merged

bojanz merged 4 commits into
mainfrom
cli-199-wait-briefly-for-the-background-update-check-before-exiting

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

The update check runs in a background goroutine. Short commands such as --version exited before it could save its result to state.json, so the check was lost.

  • Decide synchronously whether a check is due (internal.StartUpdateCheck), and only then start it in the background.
  • Before exiting, wait up to 1 second for a running check: after the command, in exitWithError, and after --version (the check now starts before --version is handled). The wait only happens when a check is due, which is at most once per check interval (1 hour by default).
  • Record LastChecked before the request starts, so a slow or blocked network costs the wait at most once per check interval.
  • Add a 5 second timeout to the GitHub request.
  • Drop the unused version comparison from the check; the notice compares versions when it reads the cached result.

Verified by running upsun --version in a PTY with an empty home: main writes no state, this branch records the latest version.

🤖 Generated with Claude Code

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔍 Full review · 4 files reviewed

Verification
  • state.Update holds a package mutex while it reloads the file, so MarkNotified running alongside the new goroutine cannot drop each other's fields.
  • UpdateCheck.Wait handles a nil receiver, so waitForUpdateCheck is safe on completion paths, help paths and paths where no check was started.
  • The removed CheckForUpdate has no remaining callers in the Go code.
  • StartUpdateCheck keeps the GitHubRepo != "" gate (through shouldCheckForUpdate) that the old call got from its enclosing if block.

The new TestUpdateCheckWait covers only the nil, finish and timeout cases of Wait. No test covers StartUpdateCheck, the root-command wiring, or what happens when the GitHub request runs longer than the wait. The CI test/lint jobs (make test / make lint) cover these files only to that extent.

Review details
  • Commit: 7038fcf
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

Comment thread internal/update.go

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 1 file reviewed

Verification
  • LastChecked is now written synchronously, before the goroutine starts, so a request that outlives the process no longer leaves the check due on the next run.
  • The background state.Update now sets only KnownLatestVersion, and only when the release lookup succeeds with a non-empty tag, so a failed lookup no longer touches the cached version.
  • state.Update reloads the state under a mutex, so the background write of KnownLatestVersion cannot drop the LastChecked or LastNotified written on the main goroutine.

No test in this increment covers the new ordering: nothing asserts that LastChecked is saved before the request, or that a failed request leaves KnownLatestVersion alone. The repository's Go test job (make test) runs only the existing state and notification tests.

Review details

Review 2 of 10 for this pull request · View the full run

Comment thread internal/update.go Outdated
@upsun-dispatch
upsun-dispatch Bot dismissed their stale review October 8, 2026 23:48

Superseded: the latest Upsun Dispatch review no longer requests changes.

pjcdawkins and others added 3 commits October 13, 2026 13:52
Short commands such as `--version` exited before the update check could
save its result to the state file, so the check was lost.

- Decide synchronously whether a check is due, and only then start it in
  the background (`internal.StartUpdateCheck`).
- Before exiting, wait up to 1 second for a running check: after the
  command, in `exitWithError`, and after `--version` (the check now
  starts before `--version` is handled).
- Add a 5 second timeout to the GitHub request.
- Drop the unused version comparison from the check; the notice compares
  versions when it reads the cached result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Previously LastChecked was saved only after the GitHub request returned.
On a network where the request takes longer than the 1 second wait, the
process exited first, so every run started a new check and waited the
full second. Saving the attempt first limits this to once per check
interval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the state file is not writable, LastChecked could not be recorded, so
every run would start a new check and wait for it. The result could not
be cached either, so the check is skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pjcdawkins
pjcdawkins force-pushed the cli-199-wait-briefly-for-the-background-update-check-before-exiting branch from 5d848d1 to 97471ff Compare October 13, 2026 13:03

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

Reviewed — No blocking findings · 🔵 1 minor point

🔍 Full review · 4 files reviewed

🔵 Minor point

  • commands/root.go:71 — Not every exit path calls waitForUpdateCheck. commands/config_install.go:108 calls os.Exit(1) when the user answers no at the confirmation prompt, and exitSilently in commands/completion.go also exits directly (completion requests skip the check, so that one is harmless). If a check is due on a config:install run that ends at the prompt, LastChecked is already saved but the request is killed. No result is cached, and nothing retries until the check interval passes. That is the same lost-check problem this PR sets out to fix.
Verification
  • state.Update holds a package mutex while it reloads and saves, so the goroutine's KnownLatestVersion write cannot drop MarkNotified's LastNotified, which now runs at the same time.
  • StartUpdateCheck runs after the completion/internal-command early return in PersistentPreRun, so completions never start a check or wait for one.
  • UpdateCheck.Wait returns at once on a nil receiver, so waitForUpdateCheck is safe when no check was started, including the help paths that skip PersistentPreRun.
  • CLIWrapper.Exec runs a child process and returns; it does not call syscall.Exec. So Execute and exitWithError really do get to wait after legacy commands.

The new TestUpdateCheckWait tests only the Wait timeout behaviour. No test covers StartUpdateCheck: not the order of the LastChecked write, not the interval gating, not the root-command wiring. Those depend on the manual PTY check described in the PR.

Review details
  • Commit: 97471ff
  • Model: claude-opus-5-5

Review 4 of 10 for this pull request · View the full run

…lined

Declining the confirmation prompt called os.Exit(1) directly, skipping the
wait for the background update check. Return an exitError instead, so
Execute waits before exiting with code 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bojanz
bojanz self-requested a review October 13, 2026 14:00
@bojanz
bojanz merged commit 3d83d56 into main Oct 13, 2026
6 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.

2 participants