Skip to content

Commit 57ef5ff

Browse files
authored
Merge pull request #240 from jayleaton/fix/ada-windows-uninstaller-20261113
fix(desktop): the Windows installer check looks for the uninstaller packaging writes
2 parents 89b5cb1 + 0dbe6ce commit 57ef5ff

6 files changed

Lines changed: 179 additions & 13 deletions
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: Fork Windows installer check
2+
3+
# Builds an unsigned Ada installer for Windows x64 through the nightly's own release-desktop.yml
4+
# and runs its "Verify Agents installer isolation" step, so installer changes are proven on the PR
5+
# instead of on the next nightly. Product name Ada exercises the renamed product with the exe
6+
# pinned to its old name; the nightly's FORK_PRODUCT_NAME may still lag behind.
7+
8+
on:
9+
pull_request:
10+
paths:
11+
- apps/desktop/resources/*installer.nsh
12+
- scripts/test-agents-windows-installer.ps1
13+
- scripts/lib/windows-package-names.ts
14+
- scripts/build-desktop-artifact.ts
15+
- .github/workflows/release-desktop.yml
16+
- .github/workflows/fork-windows-installer-check.yml
17+
workflow_dispatch:
18+
19+
permissions:
20+
# release-desktop.yml's Windows build reads this run's artifacts to wait for the Linux CLI archive.
21+
actions: read
22+
contents: read
23+
24+
concurrency:
25+
group: fork-windows-installer-check-${{ github.event.pull_request.number || github.ref }}
26+
cancel-in-progress: true
27+
28+
jobs:
29+
prepare:
30+
if: github.repository == 'jayleaton/ada'
31+
runs-on: ubuntu-24.04
32+
outputs:
33+
version: ${{ steps.version.outputs.version }}
34+
ref: ${{ steps.version.outputs.ref }}
35+
steps:
36+
- uses: actions/checkout@v6
37+
with:
38+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
39+
- id: version
40+
env:
41+
REF: ${{ github.event.pull_request.head.sha || github.sha }}
42+
run: |
43+
base=$(node -p 'JSON.parse(require("fs").readFileSync("apps/desktop/package.json", "utf8")).version.split("-")[0]')
44+
echo "version=$base-nightly.$(date -u +%Y%m%d).$((GITHUB_RUN_NUMBER * 1000 + GITHUB_RUN_ATTEMPT))" >> "$GITHUB_OUTPUT"
45+
echo "ref=$REF" >> "$GITHUB_OUTPUT"
46+
47+
build_bundle:
48+
needs: prepare
49+
runs-on: ubuntu-24.04
50+
env:
51+
NODE_ENV: production
52+
T3CODE_DESKTOP_BRAND: agents
53+
APP_VERSION: ${{ needs.prepare.outputs.version }}
54+
steps:
55+
- uses: actions/checkout@v6
56+
with:
57+
ref: ${{ needs.prepare.outputs.ref }}
58+
sparse-checkout: |
59+
/*
60+
!/.repos/
61+
sparse-checkout-cone-mode: false
62+
- uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb
63+
with:
64+
node-version-file: package.json
65+
run-install: false
66+
- name: Install build dependencies
67+
env:
68+
NODE_ENV: development
69+
run: vp install --frozen-lockfile
70+
- run: sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
71+
- run: node scripts/update-release-package-versions.ts "$APP_VERSION"
72+
- run: vp run build:desktop
73+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
74+
with:
75+
name: js-bundle
76+
path: |
77+
apps/server/dist
78+
apps/desktop/dist-electron
79+
if-no-files-found: error
80+
retention-days: 1
81+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
82+
with:
83+
name: mcp-gateway-bundle
84+
path: packages/mcp-gateway/dist
85+
if-no-files-found: error
86+
retention-days: 1
87+
88+
# The Windows build embeds this job's CLI archive as its WSL runtime.
89+
desktop_linux_x64:
90+
name: Desktop Linux x64
91+
needs: [prepare, build_bundle]
92+
uses: ./.github/workflows/release-desktop.yml
93+
with:
94+
version: ${{ needs.prepare.outputs.version }}
95+
ref: ${{ needs.prepare.outputs.ref }}
96+
release_channel: nightly
97+
brand: agents
98+
app_id: ${{ vars.FORK_APP_ID || 'com.jayleaton.t3agents' }}
99+
product_name: Ada
100+
update_repository: ${{ github.repository }}
101+
label: Linux x64
102+
runner: ubuntu-24.04
103+
platform: linux
104+
target: AppImage
105+
arch: x64
106+
rust_target: x86_64-unknown-linux-gnu
107+
resource_key: linux-x64
108+
cli_archive: true
109+
110+
# No secrets: the installer is unsigned, which the installer check does not depend on.
111+
desktop_win_x64:
112+
name: Desktop Windows x64
113+
needs: [prepare, build_bundle, desktop_linux_x64]
114+
uses: ./.github/workflows/release-desktop.yml
115+
with:
116+
version: ${{ needs.prepare.outputs.version }}
117+
ref: ${{ needs.prepare.outputs.ref }}
118+
release_channel: nightly
119+
brand: agents
120+
app_id: ${{ vars.FORK_APP_ID || 'com.jayleaton.t3agents' }}
121+
product_name: Ada
122+
update_repository: ${{ github.repository }}
123+
label: Windows x64
124+
runner: ubuntu-24.04
125+
verify_runner: windows-2025
126+
platform: win
127+
target: nsis
128+
arch: x64
129+
rust_target: x86_64-pc-windows-msvc
130+
resource_key: win32-x64
131+
cli_archive: true

‎.github/workflows/release-desktop.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,4 +1008,6 @@ jobs:
10081008
run: |
10091009
$installers = @(Get-ChildItem release/Ada-*.exe)
10101010
if ($installers.Count -ne 1) { throw 'Expected exactly one Agents installer.' }
1011-
./scripts/test-agents-windows-installer.ps1 -Installer $installers[0].FullName -ProductName $env:PRODUCT_NAME
1011+
$names = node scripts/lib/windows-package-names.ts $env:PRODUCT_NAME '${{ inputs.brand }}' | ConvertFrom-Json
1012+
if ($LASTEXITCODE -ne 0) { throw 'Could not resolve the Windows package names.' }
1013+
./scripts/test-agents-windows-installer.ps1 -Installer $installers[0].FullName -ProductName $env:PRODUCT_NAME -ExecutableName $names.executable -UninstallerName $names.uninstaller

‎scripts/build-desktop-artifact.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
import { loadRepoEnv } from "./lib/public-config.ts";
4444
import { selectDesktopRuntimeExternalDependencies } from "./lib/desktop-external-packages.ts";
4545
import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
46+
import { resolveWindowsExecutableName } from "./lib/windows-package-names.ts";
4647

4748
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4849
import * as NodeServices from "@effect/platform-node/NodeServices";
@@ -2580,13 +2581,6 @@ export function resolvePackageManagerUserAgent(packageManager: string): string {
25802581
return `${trimmed.slice(0, versionSeparator)}/${trimmed.slice(versionSeparator + 1)}`;
25812582
}
25822583

2583-
const LEGACY_FORK_WINDOWS_EXECUTABLE_NAME = "T3 Agents";
2584-
2585-
/** The packaged Windows exe's base name; the agents brand pins it across product renames. */
2586-
export function resolveWindowsExecutableName(productName: string, brand?: string): string {
2587-
return brand === "agents" ? LEGACY_FORK_WINDOWS_EXECUTABLE_NAME : productName;
2588-
}
2589-
25902584
export function resolveDesktopProductName(version: string): string {
25912585
return resolveDesktopUpdateChannel(version) === "nightly"
25922586
? `${APP_NAME} (Nightly)`
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Dependency-free so the Windows installer check in CI can run it without an install.
2+
3+
const LEGACY_FORK_WINDOWS_EXECUTABLE_NAME = "T3 Agents";
4+
5+
/** The packaged Windows exe's base name; the agents brand pins it across product renames. */
6+
export function resolveWindowsExecutableName(productName: string, brand?: string): string {
7+
return brand === "agents" ? LEGACY_FORK_WINDOWS_EXECUTABLE_NAME : productName;
8+
}
9+
10+
/**
11+
* NSIS names the uninstaller after electron-builder's productFilename, which is the exe name when
12+
* one is set, while shortcuts and the Apps entry follow the product name.
13+
*/
14+
export function resolveWindowsUninstallerName(productName: string, brand?: string): string {
15+
return `Uninstall ${resolveWindowsExecutableName(productName, brand)}.exe`;
16+
}
17+
18+
if (import.meta.main) {
19+
const [productName, brand] = process.argv.slice(2);
20+
if (!productName) throw new Error("Usage: windows-package-names.ts <product name> [brand]");
21+
process.stdout.write(
22+
`${JSON.stringify({
23+
executable: `${resolveWindowsExecutableName(productName, brand)}.exe`,
24+
uninstaller: resolveWindowsUninstallerName(productName, brand),
25+
})}\n`,
26+
);
27+
}

‎scripts/test-agents-windows-installer.ps1‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
param(
22
[Parameter(Mandatory = $true)][string]$Installer,
3-
# Shortcuts and the uninstaller follow the product name; the exe stays pinned to 'T3 Agents.exe'.
4-
[Parameter(Mandatory = $true)][string]$ProductName
3+
# Shortcuts and the Apps entry follow the product name. The exe and uninstaller file names come
4+
# from scripts/lib/windows-package-names.ts, the rule packaging uses.
5+
[Parameter(Mandatory = $true)][string]$ProductName,
6+
[Parameter(Mandatory = $true)][string]$ExecutableName,
7+
[Parameter(Mandatory = $true)][string]$UninstallerName
58
)
69
$ErrorActionPreference = 'Stop'
710
if ($env:GITHUB_ACTIONS -ne 'true') { throw 'This destructive installer test is only for disposable GitHub runners.' }
@@ -34,7 +37,7 @@ function Get-Shortcuts([string]$Name) {
3437
}
3538
}
3639
function Assert-Installed {
37-
$exe = Join-Path $agents 'T3 Agents.exe'
40+
$exe = Join-Path $agents $ExecutableName
3841
if (!(Test-Path $exe)) { throw 'Agents executable missing from its separate directory.' }
3942
if ((Get-ItemProperty $installKey).InstallLocation.TrimEnd('\') -ne $agents) {
4043
throw 'Agents registered the wrong install directory.'
@@ -48,9 +51,18 @@ function Assert-Installed {
4851
Assert-OfficialPreserved
4952
}
5053
function Remove-Agents {
51-
$uninstaller = Join-Path $agents "Uninstall $ProductName.exe"
54+
# Settings > Apps runs the registered UninstallString, so it must name the installed uninstaller.
55+
$registered = (Get-ItemProperty $uninstallKey).UninstallString
56+
$uninstaller = Join-Path $agents $UninstallerName
57+
if ($registered -notmatch '^"([^"]+)"' -or $Matches[1] -ne $uninstaller -or !(Test-Path $uninstaller)) {
58+
throw "The Apps entry runs '$registered', not the installed '$uninstaller'."
59+
}
5260
$process = Start-Process -FilePath $uninstaller -ArgumentList "/S _?=$agents" -PassThru -Wait
5361
if ($process.ExitCode -ne 0) { throw "Uninstaller failed: $($process.ExitCode)" }
62+
if (Test-Path $uninstallKey) { throw 'Uninstalling left the Apps entry behind.' }
63+
if (@(Get-Shortcuts $ProductName | Where-Object { Test-Path $_ }).Count -ne 0) {
64+
throw 'Uninstalling left an Agents shortcut behind.'
65+
}
5466
Assert-OfficialPreserved
5567
}
5668
function Install-And-Remove-Agents {

‎scripts/verify-windows-desktop-artifact.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import { Command, Flag } from "effect/cli";
1212

1313
import {
1414
resolveDesktopProductName,
15-
resolveWindowsExecutableName,
1615
verifyWindowsPackagedRuntime,
1716
} from "./build-desktop-artifact.ts";
17+
import { resolveWindowsExecutableName } from "./lib/windows-package-names.ts";
1818

1919
const verifyWindowsDesktopArtifactCli = Command.make("verify-windows-desktop-artifact", {
2020
appDir: Flag.String("app-dir").pipe(

0 commit comments

Comments
 (0)