Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 4664d32

Browse files
authored
refactor(code): migrate from electron forge to electron-builder (#2795)
## Problem Auto-update runs on Electron's native `autoUpdater` (Squirrel via update.electronjs.org) and has been a recurring source of failures we have repeatedly tried and failed to fix: - Updates that download but never apply (ShipIt fails the macOS bundle swap, leaving users stuck on the old version). - Users unable to download a new version at all. It also gives us no control over the update UX: we cannot let users review and confirm an update before it downloads, show byte-level download progress, or surface "what's new" changelogs. This migrates `apps/code` from Electron Forge to electron-builder + electron-updater, the more stable, production-standard toolchain. It fixes the reliability issues and gives us the primitives (explicit download/install, progress events, release manifests) to build that update UX. ## Changes Forge 7 to electron-builder 26, landed as small commits: - **Build/dev**: replaced Forge's Vite plugin with `scripts/build.mjs` and `scripts/dev.mjs` (keeps the CDP `:9222` debug port). - **Packaging**: `electron-builder.config.cjs` (mac dmg+zip, win NSIS+Squirrel, linux AppImage/deb/rpm). A `before-pack.cjs` hook stages native modules from the hoisted root `node_modules`; `files` drops the deps Vite already inlines (bundle 1.7 GB to 669 MB). - **Auto-update**: native `autoUpdater` to `electron-updater`, which reads `latest*.yml` from the GitHub release and exposes progress plus explicit download/install. - **Release CI**: `code-release.yml` builds, signs and publishes via electron-builder (`CSC_LINK` signing, per-arch macOS manifest merge, dual Windows NSIS+Squirrel for the transition). - **Cleanup**: removed Forge and the dead update.electronjs.org feed code; updated `docs/UPDATES.md`. ## How did you test this? Locally on macOS arm64, I ran: - `node scripts/build.mjs` and `electron-builder build --mac --arm64` (dmg + zip + `latest-mac.yml`). `asar list` confirms native JS is in the manifest with `.node` unpacked, the bundle is 669 MB, and artifact filenames match the manifest urls. - Launched the packaged app: boots in ~2 to 3 s, renderer loads from the production asar, no module/ABI errors (over CDP). - `pnpm typecheck`, `pnpm lint`, the updates unit tests (`vitest run updates`, 69 passing) and core's full suite (171 files). The PR's CI `unit-test` and `build` jobs pass. Not validated locally (needs a tagged CI run because these cannot build on macOS): real signing/notarization, the Windows and Linux builds, the finalize manifest merge plus draft promotion, and end-to-end auto-update. Recommend a staging tag (for example `v0.0.0-eb-test.1`) before merge. ## Migration strategy Per-platform rollout: - **macOS**: transparent. update.electronjs.org serves the new electron-builder zip to current users, who then move onto the electron-updater feed. - **Linux**: AppImage/deb/rpm are published for manual download, with no auto-update. This is unchanged from the Forge setup (the updater already supported only macOS and Windows, `isSupported()` is byte-identical to `main`). electron-updater unlocks AppImage auto-update as a later, separate change. - **Windows**: ships both NSIS and Squirrel for a transition window (detail below). electron-updater requires NSIS, but the live base is installed via Squirrel.Windows and a Squirrel install cannot auto-update across to NSIS, so this avoids stranding those users. Windows dual installer (temporary): - `win.target: ["nsis", "squirrel"]` produces the NSIS installer (`*.exe` + `latest.yml`, consumed by electron-updater) and the Squirrel.Windows artifacts (`*.nupkg` + `RELEASES` + Setup, under `out/squirrel-windows/`). CI uploads both to the same GitHub release. - `squirrelWindows.name: "PostHogCode"` keeps the package identity identical to the old Forge MakerSquirrel build, so `update.electronjs.org` keeps serving the existing Squirrel installs. - Flow: existing Squirrel users get one more update through the legacy feed, then reinstall once via the NSIS installer (communicated in the release notes). New users install NSIS directly. After that everyone is on NSIS + electron-updater. - When to remove it: once a release or two have shipped both formats and the Windows base has moved to NSIS (confirm via telemetry, or after a fixed deprecation window), drop `"squirrel"` from `win.target` and delete the `squirrelWindows` block in `electron-builder.config.cjs`, leaving NSIS only. No UI changes in this PR; the confirm/progress/changelog UX is a follow-up on top of these primitives. ## Automatic notifications - [ ] Publish to changelog? - [ ] Alert Sales and Marketing teams?
1 parent 0ea73a7 commit 4664d32

31 files changed

Lines changed: 1531 additions & 3039 deletions

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

Lines changed: 153 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ jobs:
3434
APP_VERSION: ${{ steps.version.outputs.version }}
3535
run: |
3636
# Pre-create a single draft release so every parallel publish job uploads
37-
# into the same release. Without this, electron-forge's GitHub publisher
38-
# creates-if-missing from each job at once, producing multiple releases for
39-
# one tag and scattering assets (the orphans get left as drafts).
37+
# into the same release. Without this, parallel jobs each create-if-missing
38+
# at once, producing multiple releases for one tag and scattering assets
39+
# (the orphans get left as drafts).
4040
if gh release view "v$APP_VERSION" --repo PostHog/code >/dev/null 2>&1; then
4141
echo "Release v$APP_VERSION already exists — reusing it"
4242
else
@@ -68,13 +68,11 @@ jobs:
6868
POSTHOG_SOURCEMAP_API_KEY: ${{ secrets.POSTHOG_SOURCEMAP_API_KEY }}
6969
POSTHOG_ENV_ID: ${{ secrets.POSTHOG_ENV_ID }}
7070
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
71-
APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }}
71+
CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }}
72+
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
7273
APPLE_ID: ${{ secrets.APPLE_ID }}
7374
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
7475
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
75-
APPLE_CODESIGN_CERT_BASE64: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }}
76-
APPLE_CODESIGN_CERT_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
77-
APPLE_CODESIGN_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_CODESIGN_KEYCHAIN_PASSWORD }}
7876
steps:
7977
- name: Get app token
8078
id: app-token
@@ -145,35 +143,52 @@ jobs:
145143
- name: Build agent package
146144
run: pnpm --filter @posthog/agent run build
147145

148-
- name: Import code signing certificate
149-
if: env.APPLE_CODESIGN_IDENTITY != ''
146+
- name: Build release artifacts
150147
env:
151-
CERT_BASE64: ${{ env.APPLE_CODESIGN_CERT_BASE64 }}
152-
CERT_PASSWORD: ${{ env.APPLE_CODESIGN_CERT_PASSWORD }}
153-
KEYCHAIN_PASSWORD: ${{ env.APPLE_CODESIGN_KEYCHAIN_PASSWORD }}
148+
APP_VERSION: ${{ steps.version.outputs.version }}
149+
MATRIX_ARCH: ${{ matrix.arch }}
150+
working-directory: apps/code
154151
run: |
155-
if [ -z "$CERT_BASE64" ] || [ -z "$CERT_PASSWORD" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then
156-
echo "Missing code signing certificate secrets"
157-
exit 1
152+
node scripts/build.mjs
153+
if [[ "$MATRIX_ARCH" == "arm64" ]]; then
154+
pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.config.cjs
155+
else
156+
pnpm exec electron-builder build --mac --x64 --publish never --config electron-builder.config.cjs
158157
fi
159-
KEYCHAIN="$RUNNER_TEMP/codesign.keychain-db"
160-
echo "$CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
161-
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
162-
security set-keychain-settings -lut 21600 "$KEYCHAIN"
163-
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
164-
security import "$RUNNER_TEMP/certificate.p12" -k "$KEYCHAIN" -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
165-
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
166-
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
167-
rm "$RUNNER_TEMP/certificate.p12"
168-
169-
- name: Build native modules
170-
run: pnpm --filter code run build-native
171158
172-
- name: Build release artifacts
159+
- name: Verify package
173160
env:
174-
APP_VERSION: ${{ steps.version.outputs.version }}
175-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
176-
run: pnpm --filter code exec electron-forge publish --dry-run --arch=${{ matrix.arch }} --platform=darwin
161+
MATRIX_ARCH: ${{ matrix.arch }}
162+
run: |
163+
if [[ "$MATRIX_ARCH" == "arm64" ]]; then
164+
APP_BUNDLE="apps/code/out/mac-arm64/PostHog Code.app"
165+
else
166+
APP_BUNDLE="apps/code/out/mac/PostHog Code.app"
167+
fi
168+
RESOURCES="$APP_BUNDLE/Contents/Resources"
169+
UNPACKED="$RESOURCES/app.asar.unpacked/node_modules"
170+
171+
if [[ ! -f "$RESOURCES/app-update.yml" ]]; then
172+
echo "FAIL: app-update.yml missing at $RESOURCES/app-update.yml"
173+
exit 1
174+
fi
175+
echo "OK: app-update.yml"
176+
177+
for mod in node-pty better-sqlite3 "@parcel/watcher"; do
178+
if [[ ! -d "$UNPACKED/$mod" ]]; then
179+
echo "FAIL: $mod missing in app.asar.unpacked/node_modules"
180+
exit 1
181+
fi
182+
echo "OK: $mod"
183+
done
184+
185+
for bin in claude-cli codex-acp; do
186+
if [[ ! -d "$RESOURCES/app.asar.unpacked/.vite/build/$bin" ]]; then
187+
echo "FAIL: $bin missing in bundled binaries"
188+
exit 1
189+
fi
190+
echo "OK: $bin"
191+
done
177192
178193
- name: Install Playwright
179194
run: pnpm --filter code exec playwright install
@@ -192,11 +207,22 @@ jobs:
192207
path: apps/code/playwright-report/
193208
retention-days: 7
194209

195-
- name: Publish release artifacts
210+
- name: Upload release artifacts
196211
env:
212+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
197213
APP_VERSION: ${{ steps.version.outputs.version }}
198-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
199-
run: pnpm --filter code exec electron-forge publish --from-dry-run
214+
run: |
215+
gh release upload "v$APP_VERSION" --repo PostHog/code --clobber \
216+
apps/code/out/*.dmg \
217+
apps/code/out/*-mac.zip \
218+
apps/code/out/*.blockmap
219+
220+
- name: Upload mac manifest artifact
221+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
222+
with:
223+
name: mac-manifest-${{ matrix.arch }}
224+
path: apps/code/out/latest-mac.yml
225+
retention-days: 1
200226

201227
publish-windows:
202228
needs: [prepare-release]
@@ -274,18 +300,46 @@ jobs:
274300
- name: Build agent package
275301
run: pnpm --filter @posthog/agent run build
276302

277-
- name: Publish with Electron Forge
303+
- name: Build release artifacts
304+
env:
305+
APP_VERSION: ${{ steps.version.outputs.version }}
306+
working-directory: apps/code
307+
run: |
308+
node scripts/build.mjs
309+
pnpm exec electron-builder build --win --x64 --publish never --config electron-builder.config.cjs
310+
311+
- name: Upload release artifacts
312+
shell: pwsh
278313
env:
314+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
279315
APP_VERSION: ${{ steps.version.outputs.version }}
280-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
281-
run: pnpm --filter code run publish
316+
run: |
317+
$out = "apps/code/out"
318+
$sq = Join-Path $out "squirrel-windows"
319+
$items = @()
320+
# NSIS installer + electron-updater metadata live in the output root.
321+
$items += Get-ChildItem $out -File -Filter "*.exe"
322+
$items += Get-ChildItem $out -File -Filter "latest.yml"
323+
$items += Get-ChildItem $out -File -Filter "*.blockmap"
324+
# Squirrel.Windows artifacts (nupkg + RELEASES + Setup) land in out/squirrel-windows.
325+
if (Test-Path $sq) {
326+
$items += Get-ChildItem $sq -File -Filter "*.nupkg"
327+
$items += Get-ChildItem $sq -File -Filter "RELEASES"
328+
$items += Get-ChildItem $sq -File -Filter "*.exe"
329+
}
330+
$files = $items | Select-Object -ExpandProperty FullName
331+
gh release upload "v$env:APP_VERSION" --repo PostHog/code --clobber @files
282332
283333
publish-linux:
284334
needs: [prepare-release]
285335
strategy:
286336
fail-fast: false
287337
matrix:
288-
runner: [ubuntu-24.04, ubuntu-24.04-arm]
338+
include:
339+
- runner: ubuntu-24.04
340+
arch: x64
341+
- runner: ubuntu-24.04-arm
342+
arch: arm64
289343
runs-on: ${{ matrix.runner }}
290344
permissions:
291345
id-token: write
@@ -362,11 +416,28 @@ jobs:
362416
- name: Build agent package
363417
run: pnpm --filter @posthog/agent run build
364418

365-
- name: Publish with Electron Forge
419+
- name: Build release artifacts
366420
env:
367421
APP_VERSION: ${{ steps.version.outputs.version }}
368-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
369-
run: pnpm --filter code run publish
422+
MATRIX_ARCH: ${{ matrix.arch }}
423+
working-directory: apps/code
424+
run: |
425+
node scripts/build.mjs
426+
if [[ "$MATRIX_ARCH" == "arm64" ]]; then
427+
pnpm exec electron-builder build --linux --arm64 --publish never --config electron-builder.config.cjs
428+
else
429+
pnpm exec electron-builder build --linux --x64 --publish never --config electron-builder.config.cjs
430+
fi
431+
432+
- name: Upload release artifacts
433+
env:
434+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
435+
APP_VERSION: ${{ steps.version.outputs.version }}
436+
run: |
437+
gh release upload "v$APP_VERSION" --repo PostHog/code --clobber \
438+
apps/code/out/*.AppImage \
439+
apps/code/out/*.deb \
440+
apps/code/out/*.rpm
370441
371442
finalize-release:
372443
needs: [publish-macos, publish-windows, publish-linux]
@@ -391,6 +462,47 @@ jobs:
391462
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
392463
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
393464
465+
- name: Checkout merge script
466+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
467+
with:
468+
sparse-checkout: |
469+
apps/code/scripts/merge-mac-manifests.mjs
470+
sparse-checkout-cone-mode: false
471+
472+
- name: Setup Node.js
473+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
474+
with:
475+
node-version: 22
476+
477+
- name: Install yaml dependency
478+
run: npm install --prefix apps/code yaml@^2
479+
480+
- name: Download arm64 mac manifest
481+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
482+
with:
483+
name: mac-manifest-arm64
484+
path: /tmp/mac-manifests/arm64
485+
486+
- name: Download x64 mac manifest
487+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
488+
with:
489+
name: mac-manifest-x64
490+
path: /tmp/mac-manifests/x64
491+
492+
- name: Merge mac manifests
493+
run: |
494+
node apps/code/scripts/merge-mac-manifests.mjs \
495+
/tmp/mac-manifests/arm64/latest-mac.yml \
496+
/tmp/mac-manifests/x64/latest-mac.yml \
497+
/tmp/latest-mac.yml
498+
499+
- name: Upload merged mac manifest
500+
env:
501+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
502+
APP_VERSION: ${{ steps.version.outputs.version }}
503+
run: |
504+
gh release upload "v$APP_VERSION" --repo PostHog/code --clobber /tmp/latest-mac.yml
505+
394506
- name: Publish GitHub release
395507
env:
396508
GH_TOKEN: ${{ steps.app-token.outputs.token }}

‎.github/workflows/test.yml‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,6 @@ jobs:
126126
pnpm --filter agent build &
127127
wait
128128
129-
- name: Build native modules
130-
run: pnpm --filter code run build-native
131-
132129
- name: Package Electron app
133130
run: pnpm --filter code run package
134131
env:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>com.apple.security.cs.allow-jit</key>
6+
<true/>
7+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8+
<true/>
9+
<key>com.apple.security.cs.disable-library-validation</key>
10+
<true/>
11+
<key>com.apple.security.inherit</key>
12+
<true/>
13+
</dict>
14+
</plist>

0 commit comments

Comments
 (0)