Skip to content

Commit e4bd2fa

Browse files
committed
desktop app, setup page, keychain secrets, release pipeline
- Electron desktop app with sidecar CLI binary bundling - Setup page on marketing site with OS-detected download buttons - GitHub Actions workflow for building desktop distributables - Re-enable keychain secrets plugin - Fix React production build (resolve.dedupe) - Support EXECUTOR_SCOPE_DIR env var for scope selection
1 parent 723b8f3 commit e4bd2fa

13 files changed

Lines changed: 470 additions & 138 deletions

File tree

‎.changeset/desktop-app-release.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"executor": minor
3+
---
4+
5+
Desktop app, setup page, and re-enable keychain secrets
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: Publish Desktop App
2+
run-name: "${{ format('publish desktop {0}', github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name) }}"
3+
4+
on:
5+
push:
6+
tags:
7+
- "v*"
8+
workflow_dispatch:
9+
inputs:
10+
tag:
11+
description: Git tag to publish
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: write
17+
18+
concurrency:
19+
group: publish-desktop-${{ github.ref }}
20+
cancel-in-progress: false
21+
22+
jobs:
23+
build:
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
include:
28+
- os: macos-latest
29+
arch: arm64
30+
platform: mac
31+
- os: macos-13
32+
arch: x64
33+
platform: mac
34+
- os: ubuntu-latest
35+
arch: x64
36+
platform: linux
37+
- os: windows-latest
38+
arch: x64
39+
platform: win
40+
41+
runs-on: ${{ matrix.os }}
42+
43+
steps:
44+
- name: Checkout
45+
uses: actions/checkout@v4
46+
with:
47+
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
48+
49+
- name: Setup Bun
50+
uses: oven-sh/setup-bun@v2
51+
with:
52+
bun-version: 1.3.11
53+
54+
- name: Setup Node
55+
uses: actions/setup-node@v4
56+
with:
57+
node-version: 24
58+
59+
- name: Install dependencies
60+
run: bun install --frozen-lockfile
61+
62+
- name: Build web app
63+
run: bun run build
64+
working-directory: apps/web
65+
66+
- name: Build CLI sidecar binary
67+
run: bun run src/build.ts binary --single
68+
working-directory: apps/cli
69+
70+
- name: Copy sidecar to desktop resources
71+
shell: bash
72+
run: |
73+
mkdir -p apps/desktop/resources
74+
if [ "${{ matrix.platform }}" = "win" ]; then
75+
BINARY_NAME="executor.exe"
76+
else
77+
BINARY_NAME="executor"
78+
fi
79+
80+
PLATFORM="${{ matrix.platform == 'mac' && 'darwin' || matrix.platform == 'win' && 'windows' || 'linux' }}"
81+
ARCH="${{ matrix.arch }}"
82+
SRC="apps/cli/dist/executor-${PLATFORM}-${ARCH}/bin"
83+
84+
cp "${SRC}/${BINARY_NAME}" "apps/desktop/resources/${BINARY_NAME}"
85+
chmod +x "apps/desktop/resources/${BINARY_NAME}" 2>/dev/null || true
86+
87+
if [ -f "${SRC}/emscripten-module.wasm" ]; then
88+
cp "${SRC}/emscripten-module.wasm" "apps/desktop/resources/"
89+
fi
90+
91+
- name: Build desktop app (TypeScript)
92+
run: npx tsc -p tsconfig.json
93+
working-directory: apps/desktop
94+
95+
- name: Build desktop distributables
96+
env:
97+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
98+
run: npx electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish never
99+
working-directory: apps/desktop
100+
101+
- name: Upload artifacts
102+
uses: actions/upload-artifact@v4
103+
with:
104+
name: desktop-${{ matrix.platform }}-${{ matrix.arch }}
105+
path: |
106+
apps/desktop/dist/*.dmg
107+
apps/desktop/dist/*.zip
108+
apps/desktop/dist/*.exe
109+
apps/desktop/dist/*.AppImage
110+
apps/desktop/dist/*.deb
111+
if-no-files-found: warn
112+
113+
release:
114+
needs: build
115+
runs-on: ubuntu-latest
116+
117+
steps:
118+
- name: Download all artifacts
119+
uses: actions/download-artifact@v4
120+
with:
121+
path: artifacts
122+
merge-multiple: true
123+
124+
- name: List artifacts
125+
run: find artifacts -type f | head -20
126+
127+
- name: Upload to GitHub Release
128+
env:
129+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
131+
run: |
132+
# Wait for the CLI release to create the GitHub release first
133+
for i in $(seq 1 30); do
134+
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
135+
echo "Release $TAG found"
136+
break
137+
fi
138+
echo "Waiting for release $TAG to be created... ($i/30)"
139+
sleep 10
140+
done
141+
142+
# Upload desktop artifacts
143+
find artifacts -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.deb" \) | while read file; do
144+
echo "Uploading: $file"
145+
gh release upload "$TAG" "$file" --repo "$GITHUB_REPOSITORY" --clobber || true
146+
done

‎.github/workflows/publish-executor-package.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Setup Bun
3535
uses: oven-sh/setup-bun@v2
3636
with:
37-
bun-version: 1.3.10
37+
bun-version: 1.3.11
3838

3939
- name: Setup Node
4040
uses: actions/setup-node@v4

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup Bun
3030
uses: oven-sh/setup-bun@v2
3131
with:
32-
bun-version: 1.3.10
32+
bun-version: 1.3.11
3333

3434
- name: Setup Node
3535
uses: actions/setup-node@v4
@@ -88,10 +88,11 @@ jobs:
8888
git tag "$tag"
8989
git push origin "$tag"
9090
91-
- name: Trigger publish workflow
91+
- name: Trigger publish workflows
9292
if: steps.changesets.outputs.hasChangesets == 'false' && steps.detect_release.outputs.changed == 'true'
9393
env:
9494
GH_TOKEN: ${{ github.token }}
9595
run: |
9696
tag="v${{ steps.detect_release.outputs.version }}"
9797
gh workflow run publish-executor-package.yml --ref "$tag" -f tag="$tag"
98+
gh workflow run publish-desktop.yml --ref "$tag" -f tag="$tag"

‎.gitignore‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3636
personal-notes/
3737
*.har.executor
3838
executor.har
39-
.executor/
39+
.executor/
40+
41+
# desktop app build artifacts
42+
apps/desktop/resources/

‎apps/desktop/package.json‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,23 @@
22
"name": "@executor/desktop",
33
"version": "1.4.0",
44
"private": true,
5+
"description": "Executor desktop app",
6+
"author": "Rhys Sullivan",
57
"main": "dist/main.js",
68
"scripts": {
79
"dev": "node scripts/dev.js",
8-
"build": "tsc && electron-builder",
10+
"build": "tsc -p tsconfig.json",
11+
"bundle-cli": "node scripts/bundle-cli.js",
12+
"dist": "npm run build && npm run bundle-cli && electron-builder",
13+
"dist:mac": "npm run build && npm run bundle-cli && electron-builder --mac",
14+
"dist:win": "npm run build && npm run bundle-cli && electron-builder --win",
15+
"dist:linux": "npm run build && npm run bundle-cli && electron-builder --linux",
916
"typecheck": "tsc --noEmit"
1017
},
11-
"dependencies": {
12-
"electron-store": "^10.0.0"
13-
},
18+
"dependencies": {},
1419
"devDependencies": {
1520
"@types/node": "catalog:",
16-
"electron": "^35.0.1",
21+
"electron": "35.7.5",
1722
"electron-builder": "^26.0.12",
1823
"typescript": "^5.9.3"
1924
},
@@ -23,8 +28,40 @@
2328
"files": [
2429
"dist/**/*"
2530
],
31+
"extraResources": [
32+
{
33+
"from": "resources/",
34+
"to": "",
35+
"filter": ["executor*", "emscripten-module.wasm"]
36+
}
37+
],
2638
"mac": {
27-
"category": "public.app-category.developer-tools"
39+
"category": "public.app-category.developer-tools",
40+
"target": [
41+
{ "target": "dmg", "arch": ["arm64", "x64"] },
42+
{ "target": "zip", "arch": ["arm64", "x64"] }
43+
],
44+
"icon": "build/icon.icns"
45+
},
46+
"win": {
47+
"target": ["nsis"],
48+
"icon": "build/icon.ico"
49+
},
50+
"linux": {
51+
"target": ["AppImage", "deb"],
52+
"category": "Development",
53+
"icon": "build/icons"
54+
},
55+
"dmg": {
56+
"title": "Executor",
57+
"contents": [
58+
{ "x": 130, "y": 220 },
59+
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
60+
]
61+
},
62+
"nsis": {
63+
"oneClick": false,
64+
"allowToChangeInstallationDirectory": true
2865
}
2966
}
3067
}

‎apps/desktop/scripts/bundle-cli.js‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Builds the executor CLI binary and copies it into the desktop app's
3+
* resources/ folder so electron-builder can bundle it as a sidecar.
4+
*/
5+
const { execSync, spawnSync } = require("node:child_process");
6+
const { existsSync, mkdirSync, cpSync, chmodSync } = require("node:fs");
7+
const { resolve, join } = require("node:path");
8+
9+
const root = resolve(__dirname, "..");
10+
const repoRoot = resolve(root, "../..");
11+
const cliRoot = resolve(repoRoot, "apps/cli");
12+
const resourcesDir = resolve(root, "resources");
13+
14+
// Build CLI for current platform
15+
console.log("Building executor CLI binary...");
16+
17+
// Resolve bun binary path explicitly
18+
const { homedir } = require("node:os");
19+
const bunBin = resolve(process.env.BUN_INSTALL || join(homedir(), ".bun"), "bin", "bun");
20+
21+
const result = spawnSync(bunBin, ["run", "src/build.ts", "binary", "--single"], {
22+
cwd: cliRoot,
23+
stdio: "inherit",
24+
});
25+
26+
if (result.error) {
27+
console.error("CLI build spawn error:", result.error.message);
28+
process.exit(1);
29+
}
30+
31+
if (result.status !== 0) {
32+
console.error(`CLI build failed with exit code ${result.status}`);
33+
process.exit(1);
34+
}
35+
36+
// Find the built binary
37+
const platform = process.platform === "win32" ? "windows" : process.platform;
38+
const arch = process.arch === "arm64" ? "arm64" : "x64";
39+
const binaryName = process.platform === "win32" ? "executor.exe" : "executor";
40+
const targetDir = join(cliRoot, "dist", `executor-${platform}-${arch}`, "bin");
41+
42+
if (!existsSync(join(targetDir, binaryName))) {
43+
console.error(`Binary not found at ${join(targetDir, binaryName)}`);
44+
process.exit(1);
45+
}
46+
47+
// Copy to resources/
48+
mkdirSync(resourcesDir, { recursive: true });
49+
cpSync(join(targetDir, binaryName), join(resourcesDir, binaryName));
50+
chmodSync(join(resourcesDir, binaryName), 0o755);
51+
52+
// Copy QuickJS WASM if present
53+
const wasmPath = join(targetDir, "emscripten-module.wasm");
54+
if (existsSync(wasmPath)) {
55+
cpSync(wasmPath, join(resourcesDir, "emscripten-module.wasm"));
56+
}
57+
58+
console.log(`Sidecar binary copied to ${resourcesDir}`);

‎apps/desktop/src/main.ts‎

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,31 @@ let serverProcess: ChildProcess | null = null;
6363
let currentScope: string | null = null;
6464
let currentPort = DEFAULT_PORT;
6565

66-
const findServerScript = (): string => {
67-
// In development, use the CLI source directly
68-
const cliMain = resolve(__dirname, "../../cli/src/main.ts");
69-
if (existsSync(cliMain)) return cliMain;
66+
const isDev = !app.isPackaged;
67+
68+
const binaryName = process.platform === "win32" ? "executor.exe" : "executor";
69+
70+
/**
71+
* Returns { command, args, cwd } for spawning the server.
72+
* - In dev mode: uses `bun run` with the CLI source
73+
* - In production: uses the bundled sidecar binary
74+
*/
75+
const resolveServerCommand = (): { command: string; args: string[] } => {
76+
if (isDev) {
77+
const cliMain = resolve(__dirname, "../../cli/src/main.ts");
78+
if (existsSync(cliMain)) {
79+
return { command: "bun", args: ["run", cliMain] };
80+
}
81+
throw new Error("Could not find executor CLI entry point for dev mode");
82+
}
7083

71-
// Fallback: look for compiled CLI
72-
const cliBin = resolve(__dirname, "../../cli/bin/executor.ts");
73-
if (existsSync(cliBin)) return cliBin;
84+
// Production: sidecar binary bundled via extraResources
85+
const sidecar = join(process.resourcesPath, binaryName);
86+
if (existsSync(sidecar)) {
87+
return { command: sidecar, args: [] };
88+
}
7489

75-
throw new Error("Could not find executor CLI entry point");
90+
throw new Error(`Sidecar binary not found at ${sidecar}`);
7691
};
7792

7893
const isServerReady = async (port: number): Promise<boolean> => {
@@ -117,13 +132,17 @@ const startServer = async (
117132
currentScope = scopePath;
118133
currentPort = port;
119134

120-
const script = findServerScript();
121-
const args = ["run", script, "web", "--port", String(port)];
135+
const server = resolveServerCommand();
136+
const args = [...server.args, "web", "--port", String(port)];
137+
138+
// In dev mode, run from repo root so bun can resolve workspace deps.
139+
// In production, the sidecar is self-contained.
140+
const cwd = isDev ? resolve(__dirname, "../../..") : scopePath;
122141

123-
serverProcess = spawn("bun", args, {
124-
cwd: scopePath,
142+
serverProcess = spawn(server.command, args, {
143+
cwd,
125144
stdio: ["ignore", "pipe", "pipe"],
126-
env: { ...process.env },
145+
env: { ...process.env, EXECUTOR_SCOPE_DIR: scopePath },
127146
});
128147

129148
serverProcess.stdout?.on("data", (data: Buffer) => {

0 commit comments

Comments
 (0)