Skip to content

Commit 060bc15

Browse files
committed
improvement(ci): reuse content-validated desktop native builds
1 parent 8e4ec49 commit 060bc15

7 files changed

Lines changed: 307 additions & 71 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Cache desktop native outputs
2+
description: Reuse validated native outputs only for matching source and toolchain inputs
3+
runs:
4+
using: composite
5+
steps:
6+
- name: Identify native build inputs
7+
id: inputs
8+
shell: bash
9+
working-directory: apps/desktop
10+
run: echo "key=$(bun run scripts/native-build-cache.ts)" >> "$GITHUB_OUTPUT"
11+
- name: Restore native compilation outputs
12+
continue-on-error: true
13+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
14+
with:
15+
path: apps/desktop/dist/native
16+
key: desktop-native-v1-${{ runner.os }}-${{ steps.inputs.outputs.key }}

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

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
paths:
1212
- '.github/workflows/desktop-e2e.yml'
1313
- '.github/workflows/desktop-release.yml'
14+
- '.github/actions/desktop-native-cache/**'
1415
- 'apps/desktop/**'
1516
- 'apps/sim/app/_shell/desktop-update-*.tsx'
1617
- 'apps/sim/app/workspace/**/home/hooks/use-mothership-resize.ts'
@@ -49,6 +50,17 @@ on:
4950
- 'package.json'
5051
- 'scripts/desktop-e2e.ts'
5152
- 'scripts/duration-shards.ts'
53+
push:
54+
branches: [main, staging]
55+
paths:
56+
- '.github/workflows/desktop-e2e.yml'
57+
- '.github/actions/desktop-native-cache/**'
58+
- 'apps/desktop/native/**'
59+
- 'apps/desktop/scripts/build.ts'
60+
- 'apps/desktop/scripts/build-native.ts'
61+
- 'apps/desktop/scripts/native-build-cache.ts'
62+
- 'apps/desktop/tsconfig.json'
63+
- 'bun.lock'
5264
schedule:
5365
- cron: '23 9 * * 1'
5466
workflow_dispatch:
@@ -61,9 +73,39 @@ concurrency:
6173
cancel-in-progress: true
6274

6375
jobs:
76+
native-cache:
77+
name: native-cache (Node ${{ matrix.node }})
78+
if: github.event_name == 'push'
79+
runs-on: macos-26
80+
timeout-minutes: 15
81+
strategy:
82+
fail-fast: false
83+
matrix:
84+
node: ['22', '24.22.1']
85+
steps:
86+
- name: Checkout code
87+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
88+
with:
89+
persist-credentials: false
90+
- name: Setup Bun
91+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
92+
with:
93+
bun-version: 1.4.4
94+
- name: Setup Node
95+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
96+
with:
97+
node-version: ${{ matrix.node }}
98+
- name: Install dependencies
99+
run: bun install --frozen-lockfile --ignore-scripts
100+
- name: Restore native compilation
101+
uses: ./.github/actions/desktop-native-cache
102+
- name: Build desktop native outputs
103+
working-directory: apps/desktop
104+
run: bun run scripts/build-native.ts --computer-use
105+
64106
e2e:
65107
name: e2e (${{ matrix.shard }}/3)
66-
if: github.event_name != 'schedule'
108+
if: github.event_name != 'schedule' && github.event_name != 'push'
67109
runs-on: macos-26
68110
timeout-minutes: 30
69111
strategy:
@@ -81,6 +123,11 @@ jobs:
81123
with:
82124
bun-version: 1.4.4
83125

126+
- name: Setup Node
127+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
128+
with:
129+
node-version: 24.22.1
130+
84131
# Electron, electron-builder and Playwright download their binaries on every cold install. All
85132
# three caches are keyed by version inside, so an older entry restored by prefix is harmless.
86133
- name: Cache Electron and Playwright downloads
@@ -96,6 +143,9 @@ jobs:
96143
- name: Install dependencies
97144
run: bun install --frozen-lockfile
98145

146+
- name: Restore native compilation
147+
uses: ./.github/actions/desktop-native-cache
148+
99149
- name: Bundle main and preload
100150
working-directory: apps/desktop
101151
run: bun run build
@@ -154,6 +204,11 @@ jobs:
154204
with:
155205
bun-version: 1.4.4
156206

207+
- name: Setup Node
208+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v6
209+
with:
210+
node-version: 24.22.1
211+
157212
# Electron, electron-builder and Playwright download their binaries on every cold install. All
158213
# three caches are keyed by version inside, so an older entry restored by prefix is harmless.
159214
- name: Cache Electron and Playwright downloads
@@ -175,6 +230,9 @@ jobs:
175230
working-directory: apps/desktop
176231
run: bun add --no-save -d --minimum-release-age=0 electron@latest
177232

233+
- name: Restore native compilation
234+
uses: ./.github/actions/desktop-native-cache
235+
178236
- name: Bundle main and preload
179237
working-directory: apps/desktop
180238
run: bun run build
@@ -205,7 +263,7 @@ jobs:
205263

206264
package-smoke:
207265
name: package-smoke
208-
if: github.event_name != 'schedule'
266+
if: github.event_name != 'schedule' && github.event_name != 'push'
209267
runs-on: macos-26
210268
timeout-minutes: 20
211269
steps:
@@ -239,6 +297,9 @@ jobs:
239297
- name: Install dependencies
240298
run: bun install --frozen-lockfile
241299

300+
- name: Restore native compilation
301+
uses: ./.github/actions/desktop-native-cache
302+
242303
- name: Bundle and package unsigned
243304
working-directory: apps/desktop
244305
env:

‎apps/desktop/e2e-durations.json‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
{
2-
"background-executor.spec.ts": 131.12,
3-
"browser-chrome.spec.ts": 15.28,
4-
"browser-focus.spec.ts": 6.53,
5-
"browser-page-dialogs.spec.ts": 11.81,
6-
"browser-tools.spec.ts": 94.66,
7-
"chat-panel.spec.ts": 31.08,
8-
"check-report.spec.ts": 3.78,
9-
"computer-use.spec.ts": 2.07,
10-
"continue-sign-in.spec.ts": 5.8,
2+
"background-executor.spec.ts": 129.01,
3+
"browser-chrome.spec.ts": 16.68,
4+
"browser-focus.spec.ts": 14.91,
5+
"browser-page-dialogs.spec.ts": 36.22,
6+
"browser-page-parity.spec.ts": 3.89,
7+
"browser-sign-in-prompts.spec.ts": 29.02,
8+
"browser-tools.spec.ts": 136.49,
9+
"chat-panel.spec.ts": 19.81,
10+
"check-report.spec.ts": 2.48,
11+
"computer-use.spec.ts": 3.62,
12+
"continue-sign-in.spec.ts": 10.93,
1113
"desktop-tools-live-sim.spec.ts": 0.0,
12-
"local-files.spec.ts": 27.68,
14+
"local-files.spec.ts": 32.44,
1315
"packaged-smoke.spec.ts": 0.0,
14-
"password-autofill.spec.ts": 46.56,
15-
"session-cookies.spec.ts": 11.45,
16-
"smoke.spec.ts": 25.56,
17-
"source-connect.spec.ts": 41.49,
18-
"terminal-cancel.spec.ts": 199.76,
19-
"update-notification.spec.ts": 19.55,
20-
"updater.spec.ts": 2.48
16+
"password-autofill.spec.ts": 40.27,
17+
"session-cookies.spec.ts": 4.98,
18+
"smoke.spec.ts": 28.65,
19+
"source-connect.spec.ts": 40.84,
20+
"terminal-cancel.spec.ts": 205.16,
21+
"update-notification.spec.ts": 19.13,
22+
"updater.spec.ts": 6.33
2123
}

‎apps/desktop/scripts/build-native.ts‎

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { execFileSync } from 'node:child_process'
2-
import { existsSync, mkdirSync, statSync } from 'node:fs'
3-
import { dirname, join } from 'node:path'
2+
import { cpSync, mkdirSync, rmSync } from 'node:fs'
3+
import { join } from 'node:path'
4+
import {
5+
nodeIncludeDirectory,
6+
recordNativeBuild,
7+
reuseNativeBuild,
8+
} from '@desktop-scripts/native-build-cache'
49
import { createLogger } from '@sim/logger'
510

611
const logger = createLogger('DesktopNativeBuild')
12+
const args = process.argv.slice(2)
13+
if (args.some((argument) => argument !== '--computer-use') || args.length > 1) {
14+
throw new Error('Usage: bun run scripts/build-native.ts [--computer-use]')
15+
}
716
if (process.platform !== 'darwin' && process.platform !== 'linux') {
817
throw new Error('Native desktop modules require macOS or Linux.')
918
}
10-
const nodeExecutable = execFileSync('node', ['-p', 'process.execPath'], { encoding: 'utf8' }).trim()
11-
const includeDirectory = join(dirname(nodeExecutable), '..', 'include', 'node')
12-
if (!existsSync(join(includeDirectory, 'node_api.h'))) {
13-
throw new Error(`Could not find Node-API headers in ${includeDirectory}`)
14-
}
19+
const includeDirectory = nodeIncludeDirectory()
1520
const outputDirectory = 'dist/native'
1621
mkdirSync(outputDirectory, { recursive: true })
1722
const modules = [
@@ -22,13 +27,20 @@ const modules = [
2227
]
2328
for (const module of modules) {
2429
const output = join(outputDirectory, `${module.name}.node`)
25-
if (
26-
existsSync(output) &&
27-
statSync(output).mtimeMs >=
28-
Math.max(statSync(module.source).mtimeMs, statSync(import.meta.filename).mtimeMs)
29-
)
30-
continue
3130
const macOS = process.platform === 'darwin'
31+
const outputs = [{ path: output, executable: true }]
32+
if (reuseNativeBuild(module.name, outputs)) {
33+
try {
34+
if (macOS) {
35+
execFileSync('xcrun', ['lipo', output, '-verify_arch', 'arm64', 'x86_64'])
36+
}
37+
continue
38+
} catch {
39+
logger.warn('Rebuilding native module with invalid cached architectures', {
40+
module: module.name,
41+
})
42+
}
43+
}
3244
execFileSync(
3345
macOS ? 'xcrun' : 'c++',
3446
[
@@ -58,5 +70,57 @@ for (const module of modules) {
5870
],
5971
{ stdio: 'inherit' }
6072
)
73+
recordNativeBuild(module.name, outputs)
6174
logger.info('Compiled native desktop module', { module: module.name })
6275
}
76+
77+
/** Bundle the native helper; packaged builds sign it with the desktop distribution identity. */
78+
function compileComputerUse(): void {
79+
if (process.platform !== 'darwin') return
80+
const bundle = 'dist/native/Sim Computer Use.app'
81+
const binaryDirectory = join(bundle, 'Contents', 'MacOS')
82+
const binary = join(binaryDirectory, 'SimComputerUse')
83+
const outputs = [
84+
{ path: binary, executable: true },
85+
{ path: join(bundle, 'Contents', 'Info.plist') },
86+
{ path: join(bundle, 'Contents', '_CodeSignature', 'CodeResources') },
87+
]
88+
if (reuseNativeBuild('computer-use', outputs)) {
89+
try {
90+
execFileSync('xcrun', ['lipo', binary, '-verify_arch', 'arm64', 'x86_64'])
91+
execFileSync('codesign', ['--verify', '--strict', bundle])
92+
return
93+
} catch {
94+
rmSync(bundle, { recursive: true, force: true })
95+
}
96+
}
97+
rmSync(bundle, { recursive: true, force: true })
98+
mkdirSync(binaryDirectory, { recursive: true })
99+
cpSync('native/computer-use/Info.plist', join(bundle, 'Contents', 'Info.plist'))
100+
const parts: string[] = []
101+
for (const arch of ['arm64', 'x86_64']) {
102+
const output = join('dist/native', `computer-use-${arch}`)
103+
execFileSync(
104+
'xcrun',
105+
[
106+
'swiftc',
107+
'-parse-as-library',
108+
'-O',
109+
'-target',
110+
`${arch}-apple-macosx14.0`,
111+
'native/computer-use/ComputerUse.swift',
112+
'-o',
113+
output,
114+
],
115+
{ stdio: 'inherit' }
116+
)
117+
parts.push(output)
118+
}
119+
execFileSync('xcrun', ['lipo', '-create', ...parts, '-output', binary], { stdio: 'inherit' })
120+
for (const part of parts) rmSync(part)
121+
execFileSync('codesign', ['--force', '--sign', '-', bundle], { stdio: 'inherit' })
122+
recordNativeBuild('computer-use', outputs)
123+
logger.info('Compiled native macOS Computer Use helper')
124+
}
125+
126+
if (args.includes('--computer-use')) compileComputerUse()

‎apps/desktop/scripts/build.ts‎

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,6 @@ rmSync(generatedIcon, { force: true, recursive: true })
3434
cpSync(appIcon, generatedIcon, { recursive: true })
3535
console.log(`• Selecting desktop icon: ${appIcon}`)
3636

37-
/** Bundle the native helper; packaged builds sign it with the desktop distribution identity. */
38-
function compileComputerUse(): void {
39-
if (process.platform !== 'darwin') return
40-
const bundle = 'dist/native/Sim Computer Use.app'
41-
const binaryDirectory = join(bundle, 'Contents', 'MacOS')
42-
mkdirSync(binaryDirectory, { recursive: true })
43-
cpSync('native/computer-use/Info.plist', join(bundle, 'Contents', 'Info.plist'))
44-
const parts: string[] = []
45-
for (const arch of ['arm64', 'x86_64']) {
46-
const output = join('dist/native', `computer-use-${arch}`)
47-
execFileSync(
48-
'xcrun',
49-
[
50-
'swiftc',
51-
'-parse-as-library',
52-
'-O',
53-
'-target',
54-
`${arch}-apple-macosx14.0`,
55-
'native/computer-use/ComputerUse.swift',
56-
'-o',
57-
output,
58-
],
59-
{ stdio: 'inherit' }
60-
)
61-
parts.push(output)
62-
}
63-
execFileSync(
64-
'xcrun',
65-
['lipo', '-create', ...parts, '-output', join(binaryDirectory, 'SimComputerUse')],
66-
{ stdio: 'inherit' }
67-
)
68-
for (const part of parts) rmSync(part)
69-
execFileSync('codesign', ['--force', '--sign', '-', bundle], { stdio: 'inherit' })
70-
console.log('• Compiled native macOS Computer Use helper')
71-
}
72-
7337
const common = {
7438
bundle: true,
7539
platform: 'node' as const,
@@ -131,8 +95,9 @@ const renderer: BuildOptions = {
13195
}
13296

13397
async function run(): Promise<void> {
134-
execFileSync(process.execPath, ['run', 'scripts/build-native.ts'], { stdio: 'inherit' })
135-
compileComputerUse()
98+
execFileSync(process.execPath, ['run', 'scripts/build-native.ts', '--computer-use'], {
99+
stdio: 'inherit',
100+
})
136101
mkdirSync('dist/renderer', { recursive: true })
137102
for (const font of ['geist-variable.woff2', 'geist-mono-variable.woff2']) {
138103
cpSync(resolve('../../packages/auth-ui/src/assets', font), join('dist/renderer', font))

0 commit comments

Comments
 (0)