Repository navigation
Resolve update to the latest available version at install time (#6366) - #6853
Conversation
Update installs currently use the appcast item captured when the prompt was first surfaced, so a newer release shipped in the meantime is not installed — the user relaunches straight into another "update available" prompt. This commit refactors toward the fix without changing behavior: it extracts the install sequencing into AttemptUpdateCoordinator, routes every install entrypoint (the update-available popover, the app menu item, and the "Apply Update" command) through the controller's single re-resolving attemptUpdate() path, and removes the now-unused force-install path so there is one install path. The coordinator policy still confirms the already-captured update, preserving the bug, while AttemptUpdateCoordinatorTests pins the desired behavior (re-resolve to the latest before installing). Three coordinator tests fail here on purpose; the next commit flips the policy to make them pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 7 minutes and 19 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR fixes the stale-version install bug (#6366): when a user clicked "Install and Relaunch" Sparkle was installing the appcast item captured at prompt time rather than the latest available, leading to an immediate second update prompt on relaunch. The fix is a new pure state machine (
Confidence Score: 5/5The change is safe to merge: it removes the stale-item force-install path entirely and routes all four install affordances through a single coordinator that always re-fetches before confirming. All install affordances now go through AttemptUpdateCoordinator, which has deterministic phase transitions and comprehensive tests. The previously flagged cancel-then-linger bug (.idle in awaitingResult leaving the coordinator monitoring indefinitely) is correctly handled — .idle, .notFound, and .error all set phase to .inactive. The 100 ms Sparkle teardown delay and cancelActiveStateForNewCheck() are unchanged pre-existing mechanisms. No blocking, no globals, no test seams in production source. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant UI as UpdateAvailableView / Menu
participant AD as AppDelegate (UpdateActionsHost)
participant UC as UpdateController
participant Coord as AttemptUpdateCoordinator
participant Sparkle as SPUUpdater
User->>UI: Click Install and Relaunch
UI->>AD: attemptUpdate()
AD->>UC: attemptUpdate()
UC->>Coord: requestInstallLatest(.updateAvailable 0.64.15)
Coord-->>UC: ".startFreshCheck [phase=awaitingCheckRestart]"
UC->>UC: checkForUpdates()
UC->>UC: cancelActiveStateForNewCheck()
Note over UC: 100 ms recheck delay
UC->>Sparkle: checkForUpdates()
Sparkle-->>UC: state → .checking
UC->>Coord: handleStateChange(.checking)
Coord-->>UC: ".none [phase=awaitingResult]"
Sparkle-->>UC: state → .updateAvailable 0.64.16
UC->>Coord: handleStateChange(.updateAvailable 0.64.16)
Coord-->>UC: ".confirmInstall [phase=inactive]"
UC->>Sparkle: model.state.confirm()
Note over Sparkle: Downloads and installs 0.64.16
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant User
participant UI as UpdateAvailableView / Menu
participant AD as AppDelegate (UpdateActionsHost)
participant UC as UpdateController
participant Coord as AttemptUpdateCoordinator
participant Sparkle as SPUUpdater
User->>UI: Click Install and Relaunch
UI->>AD: attemptUpdate()
AD->>UC: attemptUpdate()
UC->>Coord: requestInstallLatest(.updateAvailable 0.64.15)
Coord-->>UC: ".startFreshCheck [phase=awaitingCheckRestart]"
UC->>UC: checkForUpdates()
UC->>UC: cancelActiveStateForNewCheck()
Note over UC: 100 ms recheck delay
UC->>Sparkle: checkForUpdates()
Sparkle-->>UC: state → .checking
UC->>Coord: handleStateChange(.checking)
Coord-->>UC: ".none [phase=awaitingResult]"
Sparkle-->>UC: state → .updateAvailable 0.64.16
UC->>Coord: handleStateChange(.updateAvailable 0.64.16)
Coord-->>UC: ".confirmInstall [phase=inactive]"
UC->>Sparkle: model.state.confirm()
Note over Sparkle: Downloads and installs 0.64.16
Reviews (3): Last reviewed commit: "Stop coordinating after a cancelled fres..." | Re-trigger Greptile |
| case .awaitingResult: | ||
| switch state { | ||
| case .updateAvailable: | ||
| phase = .inactive | ||
| return .confirmInstall | ||
| case .notFound, .error: | ||
| phase = .inactive | ||
| return .none | ||
| case .idle, .checking, .downloading, .extracting, .installing, .permissionRequest: | ||
| return .none | ||
| } |
There was a problem hiding this comment.
.idle can reach awaitingResult when the user cancels the fresh check (via the CheckingView Cancel button). Left as .none, the coordinator stays monitoring indefinitely and will auto-install the result of any subsequent user-triggered check — no confirmation, no user intent. Treating .idle as a terminal condition here mirrors how .notFound and .error already exit.
| case .awaitingResult: | |
| switch state { | |
| case .updateAvailable: | |
| phase = .inactive | |
| return .confirmInstall | |
| case .notFound, .error: | |
| phase = .inactive | |
| return .none | |
| case .idle, .checking, .downloading, .extracting, .installing, .permissionRequest: | |
| return .none | |
| } | |
| case .awaitingResult: | |
| switch state { | |
| case .updateAvailable: | |
| phase = .inactive | |
| return .confirmInstall | |
| case .notFound, .error, .idle: | |
| // .idle here means the fresh check was cancelled (user hit Cancel in the | |
| // CheckingView) or ended without a result. Stop coordinating so a subsequent | |
| // unrelated check is not auto-confirmed. | |
| phase = .inactive | |
| return .none | |
| case .checking, .downloading, .extracting, .installing, .permissionRequest: | |
| return .none | |
| } |
There was a problem hiding this comment.
Good catch — fixed in c9ba438. awaitingResult now treats .idle (cancelled fresh check) as terminal alongside .notFound/.error, so the coordinator stops monitoring and a later unrelated check is no longer silently auto-confirmed. Added two regression tests (cancellingFreshCheckStopsMonitoringSoLaterCheckIsNotAutoInstalled, cancellingAfterActivePromptDismissStopsMonitoring).
— Claude Code
This comment has been minimized.
This comment has been minimized.
Flip AttemptUpdateCoordinator's policy so an install request always starts a fresh appcast check and installs whatever that resolves, instead of installing the update captured when the prompt was first surfaced. If a newer release shipped between when the prompt was generated and when the user installs, they now get it directly rather than relaunching into a second update prompt. The fresh check dismisses the stale prompt, re-fetches the (latest-release) feed, and the coordinator confirms the newly resolved update. The previously failing AttemptUpdateCoordinatorTests now pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w-should-resolve-to-latest
88ce88f to
f7822bc
Compare
Greptile flagged that AttemptUpdateCoordinator's awaitingResult phase did not treat `.idle` as terminal. If the user cancels the in-flight fresh check (Cancel in the checking popover returns the model to idle), the coordinator stayed monitoring and would silently auto-confirm the result of the next unrelated user-triggered check. Treat `.idle` like `.notFound`/`.error` in awaitingResult and stop coordinating. Adds two regression tests covering the detected-path and active-prompt cancel sequences. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes #6366
Problem
When the user installs an update, cmux installs the appcast item that was captured when the prompt was first surfaced, not the latest available at install time. If a newer release ships in the window between when the prompt is generated and when the user installs (e.g. 0.64.15 → 0.64.16), the user installs 0.64.15 and then relaunches straight into another "update available" prompt for 0.64.16 — having to run the update twice.
The stable feed is GitHub's
releases/latest/download/appcast.xml, which already resolves to the latest release on each fetch. The bug is purely that the install path reused a stale captured item instead of re-fetching.Fix
Make every install entrypoint re-resolve to the latest at install time, through a single shared path:
AttemptUpdateCoordinator(new, pure/unit-tested): on an install request it ignores whatever update was captured in the prompt and starts a fresh Sparkle check, then installs whatever that check resolves. A mid-install request (downloading/extracting/installing) is a no-op so an in-progress install isn't interrupted.UpdateController.attemptUpdate()is now the one user-facing "install" action and drives the coordinator. The fresh check dismisses the stale prompt (.dismiss), re-fetches the latest-release feed, and the coordinator confirms the newly resolved update.installUpdate()/isInstalling) so there is exactly one install path and no footgun that reintroduces the stale-version behavior.No user-facing strings changed (button/menu text reuses existing localized keys), so no localization changes were required.
Tests
AttemptUpdateCoordinatorTests(CmuxUpdater package, run by CI'sswift-package-testsjob) pins the behavior. Two-commit red/green:Key case: requesting an install while
Update Available: 0.64.15is showing must return.startFreshCheck(re-resolve), not.confirmInstall(install the captured 0.64.15). The full-sequence test verifies the freshly resolved 0.64.16 is the version confirmed for install.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Always re-resolves the appcast at install time so we install the latest available release and avoid stale installs and follow-up prompts. Also stops auto-installing after the user cancels the fresh check. Fixes #6366.
Bug Fixes
AttemptUpdateCoordinatorto run a freshSparklecheck on install and confirm the newly resolved update; mid-install requests are no-ops.UpdateController.attemptUpdate()the single install path; routed the popover button, app menu item, and “Apply Update (If Available)” command through it..idleas terminal) so later checks aren’t auto-installed; added tests covering the cancel sequences and re-resolve flow.installUpdate(),isInstalling) to eliminate the stale-version install footgun.Migration
InstallUpdateMenuItemto passactions: any UpdateActionsHost, and invokeattemptUpdate()for installs.installUpdate()withattemptUpdate().Written for commit c9ba438. Summary will update on new commits.