Repository navigation
feat(desktop): let the user decide when CoDev updates itself - #265
Merged
Merged
Conversation
The updater checked at launch and every ten minutes after, and downloaded whatever it found immediately, with nothing to say otherwise. On the fleet's metered, whitelist-only links that is several hundred megabytes arriving unannounced. The app already carried translated copy for a "Check for updates on startup" setting that no screen ever rendered. Settings → General → Updates now has two switches, both defaulting true so a machine that never opens Settings behaves exactly as before: Check for updates automatically — gates the launch check and the timer Download updates automatically — off stops a check at the discovery "Check for updates" still works with both off: a check the user asks for is never gated, only the automatic ones are. UpdaterState gains `available` — found, not fetched — which is the state the download setting creates and where the consent step lives. The toast offers Download rather than Install while an update is in it, and the settings button follows. The titlebar pill deliberately stays ready-only: it installs, and an undownloaded update cannot be installed. A repeat check while available is a no-op, so the ten-minute timer does not re-ask the feed a question it has already answered. Prefs live in the main process, in the existing opencode.updater store, because that is where the timer runs — the alternative was main parsing the renderer's persisted settings format. setPrefs merges partials so two windows cannot clobber each other, and prefs broadcast over a second listener on the existing updater-subscribe registration so a change in one window reaches the rest. The timer reads autoCheck at fire time, so turning it off stops the timer that is already running rather than the next launch. The dead settings.updates.row.startup.* keys are dropped: translated in both languages, rendered nowhere, and their copy claimed startup-only, which was never the whole story. Also repoints the update feed from codev-storage/codev-code to codev-storage/codev. app-update.yml is baked at package time, so installed builds keep polling the old path until they take one more update: the next release has to be published to both prefixes, after which the old one can be retired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The artifacts now live under codev-storage/codev/<version> rather than at the prefix root, so each release's installers, blockmaps and latest*.yml sit together instead of overwriting the previous release's. Note this makes the baked feed URL version-specific: app-update.yml is written at package time, so a build only ever reads the folder it shipped with.
…per version A version in the feed URL cannot work: app-update.yml is baked at package time and nothing calls setFeedURL, so the folder a build ships with is the only one it ever reads — 1.18.4-8 would look at .../codev/1.18.4-8/ forever and never see 1.18.4-9. Verified by extracting app-update.yml from a packaged build. So the base URL goes back to .../codev, where the manifests live, and the artifacts move under <version>/. electron-builder names artifacts in latest*.yml by bare filename, which resolves against the base, so scripts/stage-release.ts prefixes each url/path with the version and lays the files out the way the bucket expects. electron-updater resolves the prefixed name with new URL(name, base), so nothing changes client-side; the config test now feeds a versioned entry through the real GenericProvider to pin exactly that. The script copies blockmaps alongside their artifact — they are not named in the manifest, and without them every update silently falls back to a full download — prints the uploads artifacts-first so a manifest never names a file that is not yet in place, and is idempotent so a rerun cannot double-prefix. It fails loudly if no url/path line matched, rather than publish a manifest whose files resolve to the wrong place. Note update manifests are only written for beta/prod builds: a dev build has no publish config at all, so CODEV_CHANNEL=prod is required to produce one.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The desktop updater checked at launch and every ten minutes after, and downloaded whatever it found immediately, with nothing anywhere to say otherwise. On the fleet's metered, whitelist-only links that is several hundred megabytes arriving unannounced. The app also already carried translated copy — in both languages — for a "Check for updates on startup" setting that no screen has ever rendered.
Two switches
Settings → General → Updates, both defaulting true, so a machine that never opens Settings behaves exactly as it does today:
"Check for updates" still works with both off. A check the user asks for is never gated — only the automatic ones are.
The download switch renders disabled rather than hidden when checking is off (nothing would find an update to download); a control that vanishes reads as a setting that was lost. Both rows are guarded by
platform.updater, which is load-bearing: the Updates section is not desktop-gated, so the web build must not render them.The
availablestateUpdaterStategainsavailable— found, not fetched. It is the state the download setting creates and where the consent step lives.ready-only: it installs, and an update that has not been downloaded cannot be installed.availableis a no-op, so the ten-minute timer does not re-ask the feed a question it has already answered while the user leaves the toast up.Where the prefs live
In the main process, in the existing
opencode.updaterstore under aprefskey — that is where the timer runs, and the alternative was main parsing the renderer's persisted settings format.setPrefsmerges partials, so two windows each holding a stale copy of the other's setting cannot clobber it.updater-subscriberegistration, so a change in one window reaches the rest.autoCheckat fire time, so turning it off stops the timer that is already running rather than the next launch.Feed URL moved, and releases are now per-version
codev-storage/codev-code→codev-storage/codev. The base stays unversioned on purpose, with each release's artifacts under<version>/beneath it:A version in the base URL cannot work:
app-update.ymlis baked at package time and nothing callssetFeedURL, so the folder a build ships with is the only one it will ever read —1.18.4-8would poll its own release folder forever and never see1.18.4-9. I confirmed that by extractingapp-update.ymlfrom a packaged build.electron-builder names artifacts in
latest*.ymlby bare filename, resolved against the base. The newpackages/desktop/scripts/stage-release.ts(bun run stage-release) prefixes eachurl/pathwith the version and lays the files out to match; electron-updater resolves the prefixed name withnew URL(name, base), so nothing changes client-side. The config test now feeds a versioned entry through the realGenericProviderto pin exactly that resolution.The script also copies
.blockmapfiles alongside their artifact — they are not named in the manifest, and without them every update silently degrades to a full download — prints uploads artifacts-first so a manifest never names a file that is not yet in place, and is idempotent so a rerun cannot double-prefix. It exits non-zero if nourl/pathline matched, rather than publish a manifest whose files resolve to the wrong place.publishblock at all (pinned by the "leaves dev builds without an update feed" test), soCODEV_CHANNEL=prodis required ordist/gets artifacts with nolatest*.yml— and a stale one from an earlier build is easy to mistake for a fresh one.app-update.ymlis baked at package time, so every already-installed build keeps polling the old prefix until it takes one more update (CODEV_UPDATE_URLis build-time only). The next release must be published to both prefixes — old-style flat at…/codev-code/, new-style staged at…/codev/— after which the old path can be retired.No manifest is needed for the app installers — electron-builder already emits
latest.yml/latest-mac.yml/latest-linux.ymlwith sha512 digests per artifact, and electron-updater verifies them and uses the.blockmapfiles for differential downloads on Windows. Upload the artifacts and blockmaps first, thelatest*.ymllast, same ordering discipline aspublish-image.ts.Cleanup
The dead
settings.updates.row.startup.*keys are dropped. Translated in both languages, rendered nowhere, and the copy claimed startup-only — which was never the whole story, since the ten-minute timer ran regardless. The two new rows say what actually happens.Testing
bun testgreen indesktop(134) andapp(702);bun typecheckclean in both. Six new controller tests plus an action test cover: the both-on default matching today's behaviour,autoCheckoff skipping the launch check but not a manual one,autoDownloadoff stopping atavailablewith nothing written to disk, the fullavailable → download → readytransition sequence, the no-op repeat check, download refusing from any state butavailable, and prefs merging/persisting/replaying to subscribers.The feed change is covered by
electron-builder.config.test.ts, which asserts the exact resolved artifact URL.Not verified against a real feed — the updater is disabled unless
app.isPackaged && CHANNEL !== "dev", so end-to-end confirmation needs a packaged non-dev build pointed at the bucket.Divergences
Every file touched here exists upstream — I checked
sst/opencodedirectly rather than inferring from the fork's truncated history, which traces everything to one root commit.updater-controller.tsandapp/src/updater.tsare upstream files that simply had no divergence row yet. One new row covers the set; the existing row formain/updater.ts(allowDowngrade) is untouched and still accurate.🤖 Generated with Claude Code