fix(updates): supersede pending updates instead of stepping one per restart - #3724
fix(updates): supersede pending updates instead of stepping one per restart#3724thiagosalvatore wants to merge 4 commits into
Conversation
…estart The update state machine latched onto the first update it found: once a release was available or downloaded it stopped re-checking the feed (and cleared the periodic interval entirely on download) until the app was installed and relaunched. When several releases were behind, each restart only advanced one version, so users had to restart repeatedly to reach the latest build. Keep the periodic check running while an update is pending and supersede it in place when a strictly-newer release appears: - periodic checks now perform a silent background re-check while an update is available or downloaded (no banner flicker, no status emitted) - a strictly-newer available/downloaded version replaces the pending one (seamlessly re-downloading when auto-download is on) - an install handoff and duplicate/older events are still never disturbed Skipping intermediate versions is safe here: SQLite and zustand-persist migrations both run from current on-disk state to expected state, never keyed on the previous app version, and differential downloads are already disabled. Adds a small semver-ish comparator (versionCompare.ts) with tests. Generated-By: PostHog Code Task-Id: d03c3d60-e63e-4075-89dc-2f0af6c01b01
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
…lease ordering Round 1 of PR review found two issues: - A failed superseding auto-download discarded an already-downloaded, ready-to-install update: superseding nulled downloadedVersion before the newer build existed, so a download error dropped to `error` with nothing installable. Keep the prior build in `supersededReadyVersion` and roll back to it in handleError instead of losing it. - compareVersions compared prerelease identifiers lexically, so numeric identifiers ordered wrong (e.g. alpha.10 < alpha.9). Compare per semver §11: numeric identifiers numerically, numeric below alphanumeric, longer sets above shorter prefixes. Also folds in a simplify pass: performCheck/performSilentCheck share a runUpdaterCheck scaffold, and handleUpdateAvailable's ready/available guards are unified into one isStrictlyNewer check. Generated-By: PostHog Code Task-Id: d03c3d60-e63e-4075-89dc-2f0af6c01b01
|
Note 🤖 stamphog reviewed Greptile's P1 concern ("Stale Download Bypasses Version Guard") is still present in the diff: handleUpdateDownloaded only guards against stale/duplicate events when state === "ready", but after a supersede kicks off (state moves to "available"/"downloading" while a newer version downloads), a delayed update-downloaded event for the old artifact skips that guard entirely and overwrites the in-flight newer update with the stale one — reintroducing the exact stuck-on-old-version problem this PR is meant to fix. This is a substantive, unaddressed reviewer concern I independently confirmed in the code, not resolved by a later commit.
Gate mechanics and policy version
|
stamphog and Greptile flagged that handleUpdateDownloaded only rejected stale/older download events when state === "ready". While a supersede is in flight (state "downloading"/"available", downloadedVersion momentarily null), a delayed update-downloaded event for the old artifact bypassed the guard and overwrote the pending newer update — stranding the app on the old version, the exact bug this PR fixes. Make the guard state-independent: reject any download no newer than the version we already have staged or are superseding away from (downloadedVersion ?? supersededReadyVersion). Adds a test for the stale-event-mid-supersede case. Generated-By: PostHog Code Task-Id: d03c3d60-e63e-4075-89dc-2f0af6c01b01
|
CI note: the failing |
There was a problem hiding this comment.
Contained fix to the desktop update-check state machine with thorough new tests; both P1 concerns Greptile raised (lexical prerelease sort, stale download bypassing the version guard) are visibly fixed in the current diff via comparePrerelease and the floorVersion guard, each with a dedicated regression test. Not risky territory (no data migration, API contract, billing, auth, or CI change), so independent assurance beyond my own diff reading isn't required.
- 👍 on the PR from greptile-apps[bot].
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 299L, 2F substantive, 578L/4F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1d-complex (578L, 4F, single-area, fix) |
| stamphog 2.0.0b3 | .stamphog/policy.yml @ aebec27 · reviewed head 005ec76 |
Generated-By: PostHog Code Task-Id: d03c3d60-e63e-4075-89dc-2f0af6c01b01
|
this was mostly me prompting in a slack thread that Thiago started feels significant enough not to just go on stamphog tbh |
|
This PR has had no activity for 7 days and has been marked stale. We are moving to the monorepo and tightening PR staleness in preparation, so it will be closed in 7 days if no further activity occurs. |
|
PostHog/posthog#76331 takes care of this in the monorepo. |
Problem
When several PostHog Code releases had shipped, the app updated one version at a time — you'd download an update, restart, immediately get prompted for another, restart again, and so on. Reported from Slack; users were restarting 3+ times to reach the latest build.
Root cause is entirely client-side (the release feed always advertises the single newest build). The update state machine latched onto the first update it found: once a release was
availableordownloaded, it stopped re-checking the feed — and on download it cleared the hourly check interval entirely — until the staged update was installed and the app relaunched. So a session could only ever advance one version, and any newer release that shipped in the meantime wasn't noticed until after the next restart.Changes
Keep checking in the background while an update is pending and supersede it in place when a strictly-newer release appears, so the app converges to the latest version within one session:
availableorready— no banner flicker, no status emitted, and the pending update is left untouched unless something newer comes back.available/downloadedversion replaces the pending one (re-downloading seamlessly when auto-download is on).versionCompare.ts) so supersede decisions are based on real version ordering, tolerating a leadingv, build metadata, and prereleases.Why: skipping intermediate versions is safe in this app — SQLite (drizzle) and zustand-persist migrations both run from the current on-disk state to the expected state (keyed on the data's own shape/ledger, never on the previous app version), and differential downloads are already disabled — so collapsing the multi-restart chain into a single jump-to-latest carries no migration-style risk. The only user-visible difference is seeing accumulated release notes at once.
How did you test this?
pnpm --filter @posthog/core exec vitest run— full core suite, 2491 passed, including the reworkedupdates.test.tsand newversionCompare.test.ts.pnpm --filter @posthog/core typecheckandbiome lint packages/core/src/updates— both clean.Automatic notifications
Created with PostHog from a Slack thread