Repository navigation
feat(desktop): auto-update from the CoDev storage bucket - #195
Merged
Merged
Conversation
The desktop updater pointed at `quickbeard/codev-code` releases, which
could never have worked: the repo is private and `GitHubProvider` requests
`/releases/latest` unauthenticated, and no desktop asset has ever been
published there. Point it at the fork's MinIO server instead, where the
installers actually land.
Both channels now share one `generic` publish config. Two of its fields are
load-bearing rather than cosmetic:
- `channel: "latest"` — `detectUpdateChannel` is on by default, so without it
electron-builder takes the channel from the version's first semver
prerelease component ("8" for 1.18.4-8) and writes 8.yml/8-mac.yml while
the app requests latest.yml. A permanent ERR_UPDATER_CHANNEL_FILE_NOT_FOUND.
- `useMultipleRangeRequest: false` — MinIO answers a multipart range request
with the whole object. electron-updater hardcodes the same false for its own
s3 provider citing minio/minio#5285, but defaults it true for generic URLs.
`allowDowngrade` goes false: a mutable bucket path, unlike an immutable
release tag, would otherwise let a stale latest.yml walk every client
backwards on the next poll.
The two finalize-latest-* scripts and their call in script/publish.ts are
gone. Both uploaded into a GitHub release the fork does not use, and
finalize-latest-json.ts built a Tauri-style latest.json — signed with the
tauri CLI, read by nothing — against artifact names this fork never produces.
Also surfaces the update check off macOS. "Check for Updates..." lived only
in the macOS app menu, and neither `createMenu` (darwin-only) nor
`WindowsAppMenu` (filters on "windows", which the app menu group excludes)
offered it anywhere else, leaving Settings as the only manual path on Windows
and Linux. It joins the Help menu there. `WindowsAppMenu` also ignored
`enabled: "updater"` outright — only command entries could be disabled — so
the new item would have shown enabled in dev builds; it now reads the
updater's own disabled status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps the PR's existing tests left open. The bucket prefix has no trailing slash, and `new URL(name, base)` drops the last path segment without one — every artifact would resolve a directory too high and 404, silently. electron-updater normalises this in `newBaseUrl`, which is internal and absent from the typings, so the test drives the real `GenericProvider` built from our real publish config and asserts a resolved artifact URL instead of restating the rule. `WindowsAppMenu`'s disabled logic was unreachable from a test — the app package has no component-render harness. Moved it to `desktop-menu.ts` as `desktopMenuItemDisabled`, a sibling to the existing `desktopMenuVisible`, and covered the cases that matter: the update check greys out when the updater reports itself disabled or is absent entirely, stays live in every other updater state, and command entries still defer to the command registry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 pointed at
quickbeard/codev-codereleases, which could never have worked: the repo is private andGitHubProviderrequests/releases/latestunauthenticated, and no desktop asset has ever been published there. This points it at the fork's MinIO server, where the installers actually land.The update feed
Both channels now share one
genericpublish config. Two of its fields are load-bearing rather than cosmetic, and both are pinned by tests:channel: "latest"—detectUpdateChannelis on by default, so without it electron-builder takes the channel from the version's first semver prerelease component (8for1.18.4-8) and writes8.yml/8-mac.ymlwhile the app requestslatest.yml. A permanentERR_UPDATER_CHANNEL_FILE_NOT_FOUND.useMultipleRangeRequest: false— MinIO answers a multipart range request with the whole object. electron-updater hardcodes the samefalsefor its owns3provider citing Multiple HTTP ranges are not supported minio/minio#5285, but defaults it totruefor generic URLs.The host is base64'd behind
atob, matching the convention inpackages/codev-brand/src/index.ts, andCODEV_UPDATE_URLoverrides it per build.allowDowngradegoesfalse. A mutable bucket path, unlike an immutable release tag, would otherwise let a stale or re-uploadedlatest.ymlwalk every client backwards on the next poll.Beta keeps a publish block despite the fork shipping no beta installers:
UPDATER_ENABLEDcovers that channel, and a build with no publish config gets noapp-update.ymlat all, so every check would fail with ENOENT.Release scripts
finalize-latest-json.tsandfinalize-latest-yml.tsare deleted along with their call inscript/publish.ts. Bothgh release uploadinto a GitHub release the fork does not use;finalize-latest-json.tsadditionally built a Tauri-stylelatest.json, signed with the tauri CLI and read by nothing in the Electron path, against artifact names this fork never produces. electron-builder already writes a correct per-platformlatest*.ymlbeside each installer.Update check on Windows and Linux
"Check for Updates…" lived only in the macOS app menu.
createMenuearly-returns off darwin, andWindowsAppMenufilters on"windows", which theplatforms: ["macos"]app menu group excludes — so there was no menu path to it at all on Windows or Linux, leaving Settings → General as the only manual entry point. It joins the Help menu there.WindowsAppMenualso ignoredenabled: "updater"outright — onlycommandentries could be disabled — so the new item would have shown enabled in dev builds, where clicking it hits a disabled updater. It now reads the updater's owndisabledstatus, the renderer-side equivalent ofUPDATER_ENABLED.Verification
bun typecheckclean across all 32 packages; 657 app tests and 67 desktop tests pass.electron-builder.config.test.ts— the publish config per channel (provider,channel,useMultipleRangeRequest, and that dev still has no feed), plus a feed-resolution test that constructs electron-updater's realGenericProviderfrom our real publish config and asserts an artifact resolves to…/codev-storage/codev-code/<file>. That one is guarding a silent failure: the bucket prefix has no trailing slash, and plainnew URL(name, base)drops the last path segment without one, resolving to…/codev-storage/codev-code-desktop-mac-universal.zip— a directory too high, and a 404 with nothing in the logs to explain it. electron-updater normalises this innewBaseUrl, which is internal and absent from the typings, so the test drives the real provider rather than restating the rule.desktop-menu.test.ts— exactly one update entry per platform (so an upstream merge can't quietly duplicate it on macOS), no trailing separator in the macOS Help menu, and anitem disabled stateblock overdesktopMenuItemDisabled: greyed out when the updater reports itself disabled or is absent entirely, live in every other updater state, and command entries still deferring to the command registry.That last block needed the disabled logic to move out of
WindowsAppMenuand intodesktop-menu.tsasdesktopMenuItemDisabled, a sibling to the existingdesktopMenuVisible. The app package has no component-render harness, so logic living inside the component is unreachable from a test.On e2e
None added, because the changed surfaces are not reachable from the harness we have.
packages/app/e2edrives the web build in Chromium via a vite dev server, andentry.tsxhardcodesplatform: "web";WindowsAppMenurenders only whenplatform.platform === "desktop", so the Help menu does not exist in that harness at all. No existing spec fakes a desktop platform. Everything else in this PR — the publish config, the main-process updater — is outside the browser entirely.A real end-to-end test of the update path needs three things we do not have: a Playwright
_electronharness, a packaged build (UPDATER_ENABLEDrequiresapp.isPackaged, so an unpackaged dev launch reportsdisabled), and a way to point that build at a fake feed — whichapp-update.ymlbakes in at package time, so it would need the runtimesetFeedURLoverride this PR deliberately left out. Worth doing as its own piece of work if we want the updater under e2e; it is not a small addition to this one.The tests assert our input to
detectUpdateChannel, not electron-builder's output, so the metadata filenames were also checked against realCODEV_CHANNEL=prodbuilds — one per platform, unsigned and single-arch (--mac zip --arm64,--linux deb --x64,--win nsis --x64), which is a valid proxy sincegetArchPrefixForUpdateFileonly adds an arch component on Linux:--mac zip --arm64latest-mac.ymlcodev-code-desktop-mac-arm64.zip+.blockmap--linux deb --x64latest-linux.ymlcodev-code-desktop-linux-amd64.deb(no blockmap —FpmTargetgenerates none, so deb updates are always full downloads)--win nsis --x64latest.ymlcodev-code-desktop-win-x64.exe+.blockmapNo
8*.ymlanywhere. All three packaged apps carry the same resolved feed inresources/app-update.yml:That's the stronger result than the filenames alone — all three fields survive into the shipped binary. (
updaterCacheDirNameis derived from the@opencode-ai/desktoppackage name and only names a local download-cache directory;DIVERGENCES.mdalready records that those package ids stay upstream deliberately.)These were throwaway builds: unsigned, and without
bun run native:build, so they loggedfile source doesn't exist … /nativeand are inspectable rather than runnable. Neither affects channel resolution.What to upload to the bucket
Everything goes in one flat prefix —
resolveFilesresolves eachfiles[].urlin alatest*.ymlrelative to the base URL, so there is no room for per-platform subdirectories. Every filename below came out of a real build, except the rpm:rpmbuildis not available on macOS, so that one is electron-builder's own--packagepath from the fpm invocation it attempted rather than a file that was produced.codev-storage/codev-code/latest-mac.ymlcodev-code-desktop-mac-universal.zipcodev-code-desktop-mac-universal.zip.blockmapcodev-code-desktop-mac-universal.dmgcodev-code-desktop-mac-universal.dmg.blockmaplatest.ymlcodev-code-desktop-win-x64.execodev-code-desktop-win-x64.exe.blockmaplatest-linux.ymlcodev-code-desktop-linux-amd64.debcodev-code-desktop-linux-x86_64.AppImagecodev-code-desktop-linux-x86_64.rpmRun that once per platform build. Each machine emits only its own artifacts and its own
latest*.yml, and the three metadata filenames are distinct, so they merge into the prefix without colliding. The one exception is Windows x64 + arm64, which both writelatest.yml— shipping both arches needs them merged into a single file rather than overwriting each other.Do not upload
builder-debug.yml(file patterns only, no resolved config) or the unpacked staging directoriesmac-universal/,win-unpacked/,linux-unpacked/.Five things that are easy to get wrong:
MacUpdatercallsfindFile(files, "zip", ["pkg", "dmg"])— it wants the zip and ignores the dmg outright. The dmg is there for fresh installs only, and it is listed inlatest-mac.ymleither way..blockmap. deb and rpm get none at all (FpmTargetgenerates none), so those updates are always full downloads. AppImage does have one, but electron-builder logsbuilding embedded block mapand writes it inside the AppImage — the yml carries ablockMapSizeand there is no.AppImage.blockmapfile to upload. Only macOS and Windows produce sidecars.getArtifactArchNamemaps x64 →amd64for deb but →x86_64for rpm and AppImage, while macOS and Windows keepx64/universal. Three different spellings of the same architecture in one directory is expected, not a mistake..ymldescribes what was built, not what was uploaded.latest-linux.ymllists every Linux target present in that build, and the updater picks by installed package type —DebUpdaterlooks fordeb,RpmUpdaterforrpm,AppImageUpdaterforAppImage. Ship a partial set and users on the missing type 404. Either build and upload all three, or narrowlinux.targetto what we actually distribute..ymlfiles last. Clients poll every ten minutes and start downloading the moment one appears.Build with
--publish never: thegenericprovider has no publisher (createPublisherreturnsnullfor it), so electron-builder writesapp-update.ymland nothing else. Uploading is ours to do.DIVERGENCES.mdrows updated for every upstream file touched, plus new rows for the deletions.🤖 Generated with Claude Code