From 7ac1e2caa2b1759c1cfb1fa8eac8c525d70c4e9f Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Tue, 23 Jun 2026 17:51:02 +0530 Subject: [PATCH] fix(desktop): handle Squirrel startup events + CI smoke-install the Windows build (#398) * fix(desktop): handle Squirrel startup events + CI smoke-install the Windows build Two Windows-install fixes. 1. Add electron-squirrel-startup handling. main.ts had no handler for the --squirrel-{install,updated,uninstall,obsolete} flags Squirrel runs the exe with during install/update/uninstall. Without it the install hook booted the full app (window + daemon spawn) and never exited, so the installer hung waiting on it. Now we create/remove shortcuts and quit immediately; it is a no-op on macOS/Linux. 2. Add a Windows CI smoke-install step. After the build, run Setup.exe --silent on the clean native x64 windows-latest runner, assert the install dir is created, and upload SquirrelSetup.log as an artifact. This captures the install log we otherwise cannot get and gives a build-vs-host verdict: a clean install proves the artifact is good, so a failing user machine is host-side (AV/disk/signing). The runner has no real-time AV, so it does NOT prove SmartScreen/Defender will accept the unsigned binaries on end-user machines; code-signing stays the durable fix. Co-Authored-By: Claude Opus 4.8 * ci(windows): capture SquirrelSetup.log from the app dir, fix misleading warning Update.exe writes SquirrelSetup.log into %LocalAppData%\AgentOrchestrator, not SquirrelTemp; the smoke step looked only in SquirrelTemp and so printed "failed before Update.exe ran" even on a successful install (exit 0, dir created). Look in the app root dir first, fall back to SquirrelTemp, and drop the false-failure wording. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .github/workflows/testing-build.yml | 40 +++++++++++++++++++++ frontend/package-lock.json | 25 +++++++++++++ frontend/package.json | 1 + frontend/src/electron-squirrel-startup.d.ts | 7 ++++ frontend/src/main.ts | 16 +++++++++ 5 files changed, 89 insertions(+) create mode 100644 frontend/src/electron-squirrel-startup.d.ts diff --git a/.github/workflows/testing-build.yml b/.github/workflows/testing-build.yml index bd30fae09..77f28b213 100644 --- a/.github/workflows/testing-build.yml +++ b/.github/workflows/testing-build.yml @@ -58,6 +58,46 @@ jobs: # `npm run make` keeps the premake daemon build; --targets restricts to this # platform's maker. run: npm run make -- --targets ${{ matrix.target }} + # Smoke-install the Squirrel installer on a clean, native x64 Windows runner. + # This is a build-vs-host verdict: if it installs here it proves the artifact + # is good and a failing user machine is host-side (AV/disk/signing); if it + # fails here the build/Squirrel config is wrong. continue-on-error so a failed + # install never blocks publishing the artifacts. The runner has no real-time + # AV blocking, so a clean install here does NOT prove SmartScreen/Defender + # won't reject the unsigned binaries on end-user machines. + - name: Smoke-install the Windows installer + if: runner.os == 'Windows' + continue-on-error: true + timeout-minutes: 5 + shell: pwsh + run: | + $setup = Get-ChildItem -Path out/make/squirrel.windows -Recurse -Filter '*Setup.exe' | Select-Object -First 1 + if (-not $setup) { Write-Host '::error::no Setup.exe produced under out/make/squirrel.windows'; exit 1 } + Write-Host "Running $($setup.FullName) --silent" + $proc = Start-Process -FilePath $setup.FullName -ArgumentList '--silent' -PassThru -Wait + Write-Host "Setup.exe exit code: $($proc.ExitCode)" + $installDir = Join-Path $env:LOCALAPPDATA 'AgentOrchestrator' + # Update.exe writes SquirrelSetup.log into the app root dir; the Setup.exe + # bootstrapper only logs to SquirrelTemp for the earliest extraction stage. + # Grab whichever exists and copy it into the workspace for upload. + $log = @( + (Join-Path $installDir 'SquirrelSetup.log'), + (Join-Path $env:LOCALAPPDATA 'SquirrelTemp\SquirrelSetup.log') + ) | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($log) { Copy-Item $log squirrel-setup.log; Write-Host "captured log: $log" } else { Write-Host '::warning::no SquirrelSetup.log found in install dir or SquirrelTemp' } + if (Test-Path $installDir) { + Write-Host "INSTALL OK: $installDir created" + Get-ChildItem $installDir | Select-Object Name | Format-Table -AutoSize + } else { + Write-Host "::warning::INSTALL FAILED: $installDir was not created" + } + - name: Upload SquirrelSetup.log + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: squirrel-setup-log-${{ github.sha }} + path: frontend/squirrel-setup.log + if-no-files-found: warn - name: Publish to a 0.0.0-testing- prerelease shell: bash env: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 893c97691..2680f0113 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "electron-squirrel-startup": "^1.0.1", "lucide-react": "^1.17.0", "openapi-fetch": "^0.17.0", "posthog-js": "^1.390.2", @@ -8518,6 +8519,30 @@ "node": ">=10" } }, + "node_modules/electron-squirrel-startup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", + "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.371", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4f0ce5a90..ea2d60be4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -68,6 +68,7 @@ "@xterm/xterm": "^5.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "electron-squirrel-startup": "^1.0.1", "lucide-react": "^1.17.0", "openapi-fetch": "^0.17.0", "posthog-js": "^1.390.2", diff --git a/frontend/src/electron-squirrel-startup.d.ts b/frontend/src/electron-squirrel-startup.d.ts new file mode 100644 index 000000000..dbf93a9a8 --- /dev/null +++ b/frontend/src/electron-squirrel-startup.d.ts @@ -0,0 +1,7 @@ +// electron-squirrel-startup ships no types. Its default export is the boolean +// result of handling any --squirrel-* startup event (true when the app should +// quit, false on macOS/Linux or a normal launch). +declare module "electron-squirrel-startup" { + const startup: boolean; + export default startup; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 274a140ca..a1ca63613 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -12,6 +12,7 @@ import { type OpenDialogOptions, } from "electron"; import { updateElectronApp } from "update-electron-app"; +import squirrelStartup from "electron-squirrel-startup"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; @@ -48,6 +49,18 @@ const ignoreStdStreamError = (err: NodeJS.ErrnoException): void => { process.stdout.on("error", ignoreStdStreamError); process.stderr.on("error", ignoreStdStreamError); +// Windows Squirrel runs this exe with a --squirrel-{install,updated,uninstall, +// obsolete} flag during install/update/uninstall. electron-squirrel-startup +// creates/removes the shortcuts for that event and returns true; we then quit +// immediately instead of booting the full app. A full launch (window + daemon +// spawn) would hang the installer, which runs this hook and waits for it to +// exit — that hang is the Squirrel-side failure. Returns false on macOS/Linux, +// so this is a no-op there. +const isSquirrelStartup: boolean = squirrelStartup; +if (isSquirrelStartup) { + app.quit(); +} + // Must run before app ready so the About panel and default-menu role labels use it. app.setName("Agent Orchestrator"); @@ -678,6 +691,9 @@ function initAutoUpdates(): void { } app.whenReady().then(() => { + // A Squirrel install/update hook already called app.quit() above; do no + // startup work (no window, no daemon spawn) so the hook exits promptly. + if (isSquirrelStartup) return; registerRendererProtocol(); createWindow(); void startDaemon();