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

Commit 04e004a

Browse files
committed
add local Forge update runner from CI artifacts
1 parent c4f0248 commit 04e004a

3 files changed

Lines changed: 133 additions & 1 deletion

File tree

.github/workflows/code-update-e2e.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ on:
2424
- "docs/**"
2525
- "**/*.md"
2626
- "apps/code/scripts/dev-update/run-from-ci.sh"
27+
- "apps/code/scripts/dev-update/run-from-ci-forge.sh"
2728
schedule:
2829
- cron: "0 7 * * *"
2930
workflow_dispatch:
@@ -247,11 +248,34 @@ jobs:
247248
if-no-files-found: warn
248249
retention-days: 7
249250

251+
# upload-artifact follows symlinks and repacks them as real files, which
252+
# breaks the framework layout and the code signature. ditto -c -k preserves
253+
# both, so the pulled zip extracts to a bundle Squirrel still accepts (the
254+
# same way the baseline old app travels). run-from-ci-forge.sh consumes this.
255+
- name: Package old Forge app for upload
256+
if: always()
257+
working-directory: apps/code
258+
run: |
259+
APP="out/old-forge/PostHog Code.app"
260+
ZIP="out/PostHog-Code-forge-1.0.0-arm64-mac.zip"
261+
if [ ! -d "$APP" ]; then
262+
echo "no Forge app to package (an earlier step likely failed); skipping"
263+
exit 0
264+
fi
265+
ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"
266+
# Prove the zip round-trips to a still-valid signature, so the artifact
267+
# run-from-ci-forge.sh pulls is usable and not silently corrupt.
268+
VERIFY_DIR="$(mktemp -d)"
269+
ditto -x -k "$ZIP" "$VERIFY_DIR"
270+
codesign --verify --strict "$VERIFY_DIR/PostHog Code.app"
271+
rm -rf "$VERIFY_DIR"
272+
echo "OK: packaged Forge app zip verifies"
273+
250274
- name: Upload old Forge build (v0.55.132)
251275
if: always()
252276
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
253277
with:
254278
name: update-old-forge-build-1.0.0
255-
path: apps/code/out/old-forge/
279+
path: apps/code/out/PostHog-Code-forge-1.0.0-arm64-mac.zip
256280
if-no-files-found: warn
257281
retention-days: 7
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bash
2+
# One command to try the Forge -> electron-builder macOS auto-update locally
3+
# against the CI-signed builds, no local signing. It downloads the old Forge
4+
# (v0.55.132, versioned 1.0.0) app and the new (2.0.0) feed from the latest green
5+
# Code Update E2E run, serves the feed, and launches the Forge app pointed at it.
6+
# Its genuine built-in Squirrel.Mac client checks the feed, downloads 2.0.0, and
7+
# on restart swaps + relaunches into 2.0.0 -- the path real Forge users take.
8+
#
9+
# Squirrel verifies signatures cryptographically, so the CI-signed pair updates
10+
# here without the cert being in your keychain.
11+
#
12+
# Usage:
13+
# bash scripts/dev-update/run-from-ci-forge.sh [run-id]
14+
# AUTOMATED=1 bash scripts/dev-update/run-from-ci-forge.sh # run the Playwright spec instead
15+
set -euo pipefail
16+
17+
cd "$(dirname "$0")/../.."
18+
19+
command -v gh >/dev/null || {
20+
echo "gh (GitHub CLI) is required and must be authenticated" >&2
21+
exit 1
22+
}
23+
24+
if pgrep -x "PostHog Code" >/dev/null; then
25+
echo "PostHog Code is already running. Quit it first; the test build shares its single-instance lock and data dir." >&2
26+
exit 1
27+
fi
28+
29+
RUN_ID="${1:-$(gh run list --workflow=code-update-e2e.yml --status success -L 1 --json databaseId -q '.[0].databaseId')}"
30+
[[ -n "$RUN_ID" ]] || {
31+
echo "no successful Code Update E2E run found; pass a run id explicitly" >&2
32+
exit 1
33+
}
34+
echo "==> using CI run $RUN_ID"
35+
36+
TMP="$(mktemp -d)"
37+
cleanup() {
38+
[[ -n "${SERVE_PID:-}" ]] && kill "$SERVE_PID" 2>/dev/null || true
39+
rm -rf "$TMP"
40+
}
41+
trap cleanup EXIT
42+
43+
echo "==> downloading signed builds from CI"
44+
gh run download "$RUN_ID" -n update-old-forge-build-1.0.0 -D "$TMP/old"
45+
gh run download "$RUN_ID" -n update-new-build-2.0.0 -D "$TMP/new"
46+
47+
OLD_ZIP="$(find "$TMP/old" -name '*.zip' | head -1)"
48+
FEED_YML="$(find "$TMP/new" -name latest-mac.yml | head -1)"
49+
[[ -n "$OLD_ZIP" ]] || {
50+
echo "old Forge app zip not found in artifact" >&2
51+
exit 1
52+
}
53+
[[ -n "$FEED_YML" ]] || {
54+
echo "latest-mac.yml not found in new build artifact" >&2
55+
exit 1
56+
}
57+
58+
echo "==> old Forge 1.0.0 app -> out/old-forge"
59+
rm -rf out/old-forge && mkdir -p out/old-forge
60+
ditto -x -k "$OLD_ZIP" out/old-forge
61+
xattr -dr com.apple.quarantine "out/old-forge/PostHog Code.app" 2>/dev/null || true
62+
63+
# The Squirrel swap only completes if the running app's signature is intact, so
64+
# fail loudly here rather than time out mid-swap if the artifact arrived corrupt.
65+
echo "==> verifying the CI signature survived transport"
66+
codesign --verify --strict "out/old-forge/PostHog Code.app" || {
67+
echo "old Forge app failed signature verification; the Squirrel swap will not complete" >&2
68+
exit 1
69+
}
70+
71+
echo "==> new 2.0.0 feed -> out/dev-update-feed"
72+
rm -rf out/dev-update-feed && mkdir -p out/dev-update-feed
73+
cp "$(dirname "$FEED_YML")"/* out/dev-update-feed/
74+
75+
if [[ "${AUTOMATED:-}" == "1" ]]; then
76+
echo "==> running the automated Forge update test"
77+
pnpm exec playwright test --config=tests/e2e/playwright.update-forge.config.ts
78+
exit $?
79+
fi
80+
81+
PORT="${PORT:-8788}"
82+
node scripts/dev-update/serve.mjs out/dev-update-feed "$PORT" &
83+
SERVE_PID=$!
84+
85+
APP_LOG="out/run-from-ci-forge-app.log"
86+
echo
87+
echo "==> launching Forge PostHog Code 1.0.0 (feed http://127.0.0.1:$PORT)"
88+
echo " Its built-in Squirrel.Mac client downloads 2.0.0 in the background."
89+
echo " When prompted, click Restart (or quit and reopen) to apply the swap."
90+
echo " It relaunches into 2.0.0. Quit the app (or Ctrl+C) to finish."
91+
echo " App output: $APP_LOG update log: ~/.posthog-code/logs/main.log"
92+
echo
93+
POSTHOG_E2E_UPDATE_HOST="http://127.0.0.1:$PORT" \
94+
"out/old-forge/PostHog Code.app/Contents/MacOS/PostHog Code" >"$APP_LOG" 2>&1 || true
95+
96+
echo "==> app exited; cleaning up"

docs/AUTO-UPDATE-TESTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ pnpm --filter code exec playwright test \
143143

144144
The old Forge app lands at `apps/code/out/old-forge/PostHog Code.app`. The spec copies it to a disposable run dir, so a rerun starts from `1.0.0` again without rebuilding.
145145

146+
### Or: against the CI-signed Forge build (no local signing, no 9-minute build)
147+
148+
The Forge counterpart to `run-from-ci.sh`. It pulls the CI-signed Forge `1.0.0` app and the `2.0.0` feed from the latest green run, verifies the signature survived transport, serves the feed, and launches the Forge app so its built-in Squirrel.Mac client drives the real update:
149+
150+
```bash
151+
bash apps/code/scripts/dev-update/run-from-ci-forge.sh
152+
# a specific run: bash apps/code/scripts/dev-update/run-from-ci-forge.sh <run-id>
153+
# automated spec instead: AUTOMATED=1 bash apps/code/scripts/dev-update/run-from-ci-forge.sh
154+
```
155+
156+
Needs `gh` authenticated and your normal PostHog Code quit. The CI job uploads the Forge app as a `ditto` zip (`update-old-forge-build-1.0.0`) so its symlinks and code signature survive the artifact round-trip; a plain directory upload would break both and the swap would fail.
157+
146158
## CI
147159

148160
`code-update-e2e.yml` runs both specs nightly on `macos-15` with the real signing secrets, and on demand:

0 commit comments

Comments
 (0)