Skip to content

Commit bbf52b6

Browse files
kevin-dpclaude
andauthored
feat(agents-desktop): wire electron-updater for update notifications (#4441)
## Summary Phase 1 of desktop autoupdate — integrates [`electron-updater`](https://www.electron.build/auto-update) so users can be notified about new releases. Designed to be the first half of a two-phase rollout: - **Phase 1 (this PR):** notification + update download on Windows/Linux; notify-only on macOS (no auto-install). - **Phase 2 (follow-up):** Developer ID signing + notarization in CI, then flip `MAC_AUTO_INSTALL_SUPPORTED = true` in `updater.ts` and macOS upgrades become automatic too. ### What ships in this PR - **`electron-updater` integration** in a new `src/app/updater.ts` module. - **"Check for Updates…" menu items** are now functional — the existing disabled placeholder in the app-icon menu, plus new entries in the Electric Agents menu (macOS) and Help menu (Windows/Linux). - **Background check on launch** (10s after window comes up) and unlimited manual checks via the menu. - **Dock progress bar** during downloads on signed platforms so manual download clicks give immediate feedback. ### macOS notify-only fallback (until phase 2) Squirrel.Mac requires a Developer ID signature to swap the bundle, so on unsigned macOS we behave as a notifier: - "Check for Updates…" → dialog: "Version X available — Open releases page / Later" - Auto check stays silent so it doesn't pop dialogs while users are mid-task - **No download is performed** — the file would just be discarded, and the user has to grab it from the browser anyway When phase 2 lands, signed macOS users get the same full flow as Windows/Linux without further code changes. ### Update channel topology - **Switched publish provider from `github` to `generic`** pointed at the moving `agents-desktop-latest` tag. The GitHub provider was picking up the wrong release because `electric-sql/electric` publishes many packages to the same Releases page (changesets-style) and GitHub's "latest" marker tracks whichever package was published most recently — which is rarely the desktop app. - **Canary builds get full separation** — CI passes `-c.channel=beta` and overrides `-c.publish.url=…agents-desktop-canary`. Canary builds emit `beta-*.yml` metadata and ship apps that fetch from the canary tag, so stable users never accidentally upgrade to canaries. - **Canary asset publishing** also now uploads the `*.yml` and blockmap metadata alongside the existing renamed predictable-name copies (the renames break what the updater metadata references, so we need both). ### Files touched | File | Change | | --- | --- | | `packages/agents-desktop/package.json` | adds `electron-updater@^6.3.9` | | `packages/agents-desktop/vite.config.ts` | externalizes `electron-updater` | | `packages/agents-desktop/src/app/updater.ts` *(new)* | full updater module | | `packages/agents-desktop/src/app/controller.ts` | instantiates updater, exposes `initializeUpdater` + `checkForUpdates` | | `packages/agents-desktop/src/main.ts` | calls `initializeUpdater()` after window is up | | `packages/agents-desktop/src/ui/application-menu.ts` | enables the existing placeholder + adds Help menu entry | | `packages/agents-desktop/electron-builder.yml` | switches publish provider to `generic` | | `.github/workflows/agents_desktop_build.yml` | canary channel separation + metadata upload | ## Followups (not in this PR) 1. **Canary versioning.** Today every canary builds as `0.1.10` because the workflow's `version: canary-${{ github.run_number }}-${{ github.sha }}` input isn't injected into `package.json`. Auto-update version compare won't trigger between canaries until we wire a real semver prerelease version like `0.1.11-canary.N` into the build. 2. **Phase 2: signing + notarization.** Apple Developer ID cert, notarization credentials, and GitHub Actions secrets (`CSC_LINK`, `CSC_KEY_PASSWORD`, plus notarization). Then flip the `MAC_AUTO_INSTALL_SUPPORTED` constant. ## Test plan - [x] Local typecheck + production build pass - [x] Smoke-tested end-to-end on macOS by temporarily downgrading `package.json` version to `0.0.1` and rebuilding: the app correctly fetched `latest-mac.yml` from the `agents-desktop-latest` tag, detected `0.1.10` as available, showed the "Open releases page" dialog (notify-only path), and re-checking shows the dialog again as expected - [x] Background auto-check at +10s does not pop a duplicate dialog when the manual dialog is dismissed (notifier-mode auto checks stay silent) - [x] Windows installer build picks up `electron-updater` (CI will exercise this) - [ ] Linux AppImage build picks up `electron-updater` (CI will exercise this) - [ ] First post-merge stable release populates `agents-desktop-latest/latest-mac.yml` and a subsequent stable upgrade triggers the "update available" prompt for real users 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f222d39 commit bbf52b6

10 files changed

Lines changed: 427 additions & 13 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@electric-ax/agents-desktop": patch
3+
---
4+
5+
Wire `electron-updater` so the desktop app can detect new releases. Phase
6+
one of two:
7+
8+
* Adds a working **Check for Updates…** menu item (Electric Agents menu
9+
on macOS, Help menu on Windows/Linux, plus the in-window app-icon
10+
menu) and a quiet background check ~10s after launch.
11+
* On Windows/Linux, signed-platform flow is wired end-to-end: downloads
12+
in the background with a dock/taskbar progress bar, then prompts
13+
"Restart now" to apply via `quitAndInstall()`.
14+
* On macOS, ships as **notify-only** until Developer ID signing lands —
15+
Squirrel.Mac can't swap an unsigned bundle, so we skip the download
16+
entirely and prompt to open the GitHub releases page instead.
17+
* Switches the publish provider from `github` to `generic` pointed at
18+
the moving `agents-desktop-latest` tag, because the repo's overall
19+
"latest" release is shared across packages and the GitHub provider
20+
was picking the wrong one.
21+
* Adds channel separation so canary builds publish to the `beta`
22+
channel against an `agents-desktop-canary` URL — stable users never
23+
auto-update to canaries.

.github/workflows/agents_desktop_build.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,29 @@ jobs:
254254
if [[ "${{ inputs.sign }}" != "true" ]]; then
255255
builder_args+=("-c.mac.identity=-")
256256
fi
257+
if [[ "${{ inputs.channel }}" == "canary" ]]; then
258+
builder_args+=("-c.channel=beta")
259+
builder_args+=("-c.publish.url=https://github.com/electric-sql/electric/releases/download/agents-desktop-canary")
260+
fi
257261
258262
pnpm --filter @electric-ax/agents-desktop exec electron-builder "${builder_args[@]}" --publish never
259263
260264
- name: Package desktop app
261265
if: ${{ matrix.id != 'macos' }}
266+
shell: bash
262267
env:
263268
CSC_IDENTITY_AUTO_DISCOVERY: ${{ inputs.sign && 'true' || 'false' }}
264269
GH_TOKEN: ${{ github.token }}
265-
run: pnpm --filter @electric-ax/agents-desktop exec electron-builder ${{ matrix.builder_args }} --publish never
270+
run: |
271+
set -euo pipefail
272+
273+
builder_args=(${{ matrix.builder_args }})
274+
if [[ "${{ inputs.channel }}" == "canary" ]]; then
275+
builder_args+=("-c.channel=beta")
276+
builder_args+=("-c.publish.url=https://github.com/electric-sql/electric/releases/download/agents-desktop-canary")
277+
fi
278+
279+
pnpm --filter @electric-ax/agents-desktop exec electron-builder "${builder_args[@]}" --publish never
266280
267281
- name: Verify macOS app signatures
268282
if: ${{ matrix.id == 'macos' }}
@@ -539,6 +553,13 @@ jobs:
539553
exit 1
540554
}
541555
556+
# Always include the original artifacts + electron-updater metadata
557+
# (latest*.yml / beta*.yml + .blockmap files). The updater expects
558+
# the filenames referenced in the yml; renamed copies are added on
559+
# top for canary so users get stable "latest canary" download URLs.
560+
cp packages/agents-desktop/release/*.{dmg,zip,exe,AppImage,deb,blockmap,yml,yaml} \
561+
packages/agents-desktop/publish-assets/ 2>/dev/null || true
562+
542563
if [[ "$CHANNEL" == "canary" ]]; then
543564
case "$MATRIX_ID" in
544565
macos)
@@ -555,9 +576,6 @@ jobs:
555576
copy_first "Electric-Agents-canary-linux-x64.deb" packages/agents-desktop/release/*.deb
556577
;;
557578
esac
558-
else
559-
cp packages/agents-desktop/release/*.{dmg,zip,exe,AppImage,deb,blockmap,yml,yaml} \
560-
packages/agents-desktop/publish-assets/ 2>/dev/null || true
561579
fi
562580
563581
assets=(packages/agents-desktop/publish-assets/*)

packages/agents-desktop/electron-builder.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ linux:
7373
- x64
7474

7575
publish:
76-
provider: github
77-
owner: electric-sql
78-
repo: electric
76+
# Using `generic` (not `github`) because the electric-sql/electric repo
77+
# publishes many packages to the same Releases page (changesets-style).
78+
# GitHub's "latest" marker tracks whichever release was most recently
79+
# tagged across all packages, so the `github` provider grabs the wrong
80+
# release and can't find latest-mac.yml.
81+
# Instead we point at the moving `agents-desktop-latest` tag that the
82+
# release workflow maintains specifically for desktop stable builds.
83+
# Canary builds override `url` to `agents-desktop-canary` at build time.
84+
provider: generic
85+
url: 'https://github.com/electric-sql/electric/releases/download/agents-desktop-latest'

packages/agents-desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"better-sqlite3": "^12.9.0",
3333
"dockerode": "^5.0.0",
3434
"e2b": ">=2.0.0",
35+
"electron-updater": "^6.3.9",
3536
"fix-path": "^4.0.0",
3637
"jsdom": "^28.1.0",
3738
"pino": "^10.3.1",

packages/agents-desktop/src/app/controller.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BrowserWindow } from 'electron'
22
import type { DesktopAppContext } from './context'
33
import * as AppLifecycle from './lifecycle'
44
import * as LoginItems from './login-items'
5+
import { createDesktopUpdater } from './updater'
56
import * as CloudAuthInjection from '../cloud/auth-injection'
67
import * as ServerFetch from '../cloud/server-fetch'
78
import { createCredentialsController } from '../credentials/controller'
@@ -305,12 +306,20 @@ export function createDesktopMainController(ctx: DesktopAppContext) {
305306
AppLifecycle.applyNativeAppearance(ctx, appearance)
306307
}
307308

309+
const updater = createDesktopUpdater({
310+
showOrCreateWindow,
311+
})
312+
313+
const checkForUpdates = (): Promise<void> =>
314+
updater.checkForUpdates({ triggeredManually: true })
315+
308316
const applicationMenuDeps: ApplicationMenu.ApplicationMenuDeps = {
309317
windows,
310318
createWindow,
311319
sendCommand,
312320
quitApp,
313321
showAboutDialog,
322+
checkForUpdates,
314323
}
315324

316325
function showAboutDialog(): void {
@@ -345,7 +354,11 @@ export function createDesktopMainController(ctx: DesktopAppContext) {
345354
win: BrowserWindow,
346355
bounds: DesktopMenuPopupBounds
347356
): void => {
348-
ApplicationMenu.popupAppIconMenu({ showAboutDialog }, win, bounds)
357+
ApplicationMenu.popupAppIconMenu(
358+
{ showAboutDialog, checkForUpdates },
359+
win,
360+
bounds
361+
)
349362
}
350363

351364
const desktopIpcDeps: DesktopIpc.RegisterDesktopIpcDeps = {
@@ -473,6 +486,7 @@ export function createDesktopMainController(ctx: DesktopAppContext) {
473486
syncLaunchAtLoginSetting,
474487
connectConfiguredServers,
475488
startDiscoveryLoop: localDiscovery.startDiscoveryLoop,
489+
initializeUpdater: updater.initialize,
476490
quitApp,
477491
}
478492
}

0 commit comments

Comments
 (0)