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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Harshit Singh Bhandari 2026-06-23 17:51:02 +05:30 committed by GitHub
parent 47e3dddff5
commit 7ac1e2caa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 0 deletions

View File

@ -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-<sha> prerelease
shell: bash
env:

View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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();