-
Notifications
You must be signed in to change notification settings - Fork 46
test(updates): macOS auto-update E2E test pipeline with real install + relaunch #2828
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
charlesvien
wants to merge
14
commits into
feat/auto-update-ux
Choose a base branch
from
test/macos-auto-update-e2e
base: feat/auto-update-ux
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
6627f78
add macos auto-update e2e harness and ci
charlesvien 9794ad2
pass --publish never in e2e build-pair
charlesvien dd307d6
gate update e2e spec behind RUN_UPDATE_E2E
charlesvien 5daa6dc
add local auto-update testing doc
charlesvien a6466f4
harden update e2e checks and evidence
charlesvien 76c3c60
fix update e2e stats report path
charlesvien 4f670eb
assert Squirrel auto-relaunch in update e2e
charlesvien 9dd7aad
add update proof artifact and per-build uploads
charlesvien f792a7f
document testing with CI-signed builds
charlesvien 42eff6c
add run-from-ci local update test script
charlesvien f0439b8
scope update e2e push trigger to code changes
charlesvien af369c9
fix suffix range offsets in update feed server
charlesvien 1a807ea
fix updates service token in e2e hook
charlesvien cb8861a
close electron apps in update e2e teardown
charlesvien File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| name: Code Update E2E (macOS) | ||
|
|
||
| # Real macOS auto-update end to end: build a signed old (1.0.0) app and a signed | ||
| # new (2.0.0) feed, serve the feed on localhost, then drive a packaged build | ||
| # through download -> install -> Squirrel.Mac swap -> relaunch and assert the | ||
| # installed app became 2.0.0. Nightly and on demand only, never on PRs: it builds | ||
| # twice and exercises a real install, so it is too slow and too flaky for the gate. | ||
|
|
||
| on: | ||
| # Temporary: also run on this branch so the harness can be exercised on the PR. | ||
| # Remove this push trigger once merged; nightly + dispatch is the steady state. | ||
| # Docs and the local-only run-from-ci helper do not affect CI, so skip rebuilds | ||
| # for those and use workflow_dispatch / the nightly schedule on demand instead. | ||
| push: | ||
| branches: | ||
| - test/macos-auto-update-e2e | ||
|
charlesvien marked this conversation as resolved.
|
||
| paths-ignore: | ||
| - "docs/**" | ||
| - "**/*.md" | ||
| - "apps/code/scripts/dev-update/run-from-ci.sh" | ||
| schedule: | ||
| - cron: "0 7 * * *" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: code-update-e2e-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| update-e2e-macos: | ||
| runs-on: macos-15 | ||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
| env: | ||
| NODE_OPTIONS: "--max-old-space-size=8192" | ||
| NODE_ENV: production | ||
| npm_config_arch: arm64 | ||
| npm_config_platform: darwin | ||
| VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }} | ||
| VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }} | ||
| # Sign both builds with the same identity so Squirrel.Mac accepts the swap. | ||
| # Notarization is skipped: it is a Gatekeeper concern, not what the in-place | ||
| # update verifies, and locally built bundles carry no quarantine attribute. | ||
| CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }} | ||
| CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }} | ||
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | ||
| SKIP_NOTARIZE: "1" | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | ||
| with: | ||
| node-version: 22 | ||
| cache: "pnpm" | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Configure AWS credentials | ||
| uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 | ||
| with: | ||
| role-to-assume: ${{ secrets.AWS_TWIG_APP_ASSETS_ROLE_ARN }} | ||
| aws-region: ${{ secrets.AWS_TWIG_APP_ASSETS_REGION }} | ||
| mask-aws-account-id: true | ||
| unset-current-credentials: true | ||
|
|
||
| - name: Download BerkeleyMono fonts from S3 | ||
| run: aws s3 cp s3://${{ secrets.AWS_TWIG_APP_ASSETS_BUCKET }}/fonts/BerkeleyMono/ apps/code/assets/fonts/BerkeleyMono/ --recursive | ||
|
|
||
| - name: Build workspace packages | ||
| run: | | ||
| pnpm --filter @posthog/electron-trpc run build | ||
| pnpm --filter @posthog/platform run build | ||
| pnpm --filter @posthog/shared run build | ||
| pnpm --filter @posthog/git run build | ||
| pnpm --filter @posthog/enricher run build | ||
| pnpm --filter @posthog/agent run build | ||
|
|
||
| - name: Build old + new update pair | ||
| working-directory: apps/code | ||
| run: bash scripts/dev-update/build-pair.sh | ||
|
|
||
| - name: Install Playwright | ||
| run: pnpm --filter code exec playwright install | ||
|
|
||
| - name: Run macOS update E2E | ||
| working-directory: apps/code | ||
| env: | ||
| PLAYWRIGHT_JSON_OUTPUT_NAME: ${{ github.workspace }}/apps/code/out/update-report.json | ||
| run: | | ||
| pnpm exec playwright test --config=tests/e2e/playwright.update.config.ts | ||
| node -e ' | ||
| const r = require(process.env.GITHUB_WORKSPACE + "/apps/code/out/update-report.json"); | ||
| const s = r.stats || {}; | ||
| console.log("update e2e stats:", JSON.stringify(s)); | ||
| if (s.expected !== 1 || s.skipped || s.unexpected || s.flaky) { | ||
| console.error("FAIL: expected exactly one passing update test"); | ||
| process.exit(1); | ||
| } | ||
| ' | ||
|
|
||
| - name: Render update proof summary | ||
| if: always() | ||
| working-directory: apps/code | ||
| run: | | ||
| node -e ' | ||
| const fs = require("fs"); | ||
| const p = "out/update-proof/proof.json"; | ||
| if (!fs.existsSync(p)) { console.log("no proof file was written"); process.exit(0); } | ||
| const d = JSON.parse(fs.readFileSync(p, "utf8")); | ||
| const cell = (v) => v === undefined || v === null ? "-" : String(v).replace(/\|/g, "\\|").replace(/\n/g, " "); | ||
| const rows = [ | ||
| ["Result", d.result], | ||
| ["Old version", d.oldVersion], | ||
| ["New version", d.newVersion], | ||
| ["Booted on", d.bootedOn], | ||
| ["Feed offered", d.feedAvailableVersion], | ||
| ["Downloaded", d.downloaded], | ||
| ["Bundle after swap", d.bundleVersionAfterSwap], | ||
| ["Auto-relaunched exe", d.autoRelaunchedExecutable], | ||
| ["Fresh launch version", d.freshLaunchVersion], | ||
| ["ShipIt cache", d.shipItExists ? (d.shipItEntries || []).join(", ") : "missing"], | ||
| ["Failed step", d.failedStep], | ||
| ["Error", d.error], | ||
| ["Finished", d.finishedAt], | ||
| ]; | ||
| const md = ["## macOS auto-update proof: " + d.result, "", "| Check | Value |", "| --- | --- |"] | ||
| .concat(rows.map((r) => "| " + r[0] + " | " + cell(r[1]) + " |")) | ||
| .join("\n"); | ||
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md + "\n"); | ||
| console.log(md); | ||
| ' | ||
|
|
||
| - name: Upload proof, report and logs | ||
| if: always() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: update-e2e-macos | ||
| path: | | ||
| apps/code/out/update-proof/ | ||
| apps/code/out/update-report.json | ||
| apps/code/playwright-results/ | ||
| /Users/runner/.posthog-code/logs/ | ||
| /Users/runner/Library/Caches/com.posthog.array.ShipIt/ | ||
| if-no-files-found: ignore | ||
| retention-days: 7 | ||
|
|
||
| - name: Upload old build (1.0.0) | ||
| if: always() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: update-old-build-1.0.0 | ||
| path: | | ||
| apps/code/out/PostHog-Code-1.0.0-arm64-mac.zip | ||
| apps/code/out/PostHog-Code-1.0.0-arm64-mac.zip.blockmap | ||
| if-no-files-found: warn | ||
| retention-days: 7 | ||
|
|
||
| - name: Upload new build feed (2.0.0) | ||
| if: always() | ||
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||
| with: | ||
| name: update-new-build-2.0.0 | ||
| path: apps/code/out/dev-update-feed/ | ||
| if-no-files-found: warn | ||
| retention-days: 7 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| #!/usr/bin/env bash | ||
| # Build a signed OLD (1.0.0) app to run plus a signed NEW (2.0.0) feed for the | ||
| # macOS auto-update E2E. Real signing needs CSC_LINK (set in CI); locally it uses | ||
| # whatever identity electron-builder finds. Run from apps/code. | ||
| set -euo pipefail | ||
|
|
||
| cd "$(dirname "$0")/../.." | ||
|
|
||
| OLD_VERSION="${OLD_VERSION:-1.0.0}" | ||
| NEW_VERSION="${NEW_VERSION:-2.0.0}" | ||
| FEED_DIR="out/dev-update-feed" | ||
|
|
||
| export SKIP_NOTARIZE="${SKIP_NOTARIZE:-1}" | ||
|
|
||
| echo "==> electron-vite build" | ||
| pnpm exec electron-vite build | ||
|
|
||
| echo "==> build NEW $NEW_VERSION (feed artifacts)" | ||
| pnpm exec electron-builder build --mac zip --arm64 --publish never \ | ||
| -c.extraMetadata.version="$NEW_VERSION" --config electron-builder.ts | ||
|
|
||
| rm -rf "$FEED_DIR" | ||
| mkdir -p "$FEED_DIR" | ||
| cp "out/PostHog-Code-${NEW_VERSION}-arm64-mac.zip" "$FEED_DIR/" | ||
| cp "out/PostHog-Code-${NEW_VERSION}-arm64-mac.zip.blockmap" "$FEED_DIR/" | ||
| cp "out/latest-mac.yml" "$FEED_DIR/" | ||
|
|
||
| echo "==> build OLD $OLD_VERSION (runnable app left in out/mac-arm64)" | ||
| pnpm exec electron-builder build --mac zip --arm64 --publish never \ | ||
| -c.extraMetadata.version="$OLD_VERSION" --config electron-builder.ts | ||
|
|
||
| echo "==> feed=$FEED_DIR" | ||
| echo "==> app=out/mac-arm64/PostHog Code.app ($OLD_VERSION)" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| #!/usr/bin/env bash | ||
| # One command to try the macOS auto-update flow locally against the CI-signed | ||
| # builds, no local signing. It downloads the old (1.0.0) app and the new (2.0.0) | ||
| # feed from the latest green Code Update E2E run, serves the feed, and opens the | ||
| # old app pointed at it. In the app: open the update banner, click Download, then | ||
| # Restart, and watch the real Squirrel swap + relaunch into 2.0.0. | ||
| # | ||
| # Squirrel verifies signatures cryptographically, so the CI-signed pair updates | ||
| # here without the cert being in your keychain. | ||
| # | ||
| # Usage: | ||
| # bash scripts/dev-update/run-from-ci.sh [run-id] | ||
| # AUTOMATED=1 bash scripts/dev-update/run-from-ci.sh # run the Playwright spec instead | ||
| set -euo pipefail | ||
|
|
||
| cd "$(dirname "$0")/../.." | ||
|
|
||
| command -v gh >/dev/null || { | ||
| echo "gh (GitHub CLI) is required and must be authenticated" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| if pgrep -x "PostHog Code" >/dev/null; then | ||
| echo "PostHog Code is already running. Quit it first; the test build shares its single-instance lock and data dir." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| RUN_ID="${1:-$(gh run list --workflow=code-update-e2e.yml --status success -L 1 --json databaseId -q '.[0].databaseId')}" | ||
| [[ -n "$RUN_ID" ]] || { | ||
| echo "no successful Code Update E2E run found; pass a run id explicitly" >&2 | ||
| exit 1 | ||
| } | ||
| echo "==> using CI run $RUN_ID" | ||
|
|
||
| TMP="$(mktemp -d)" | ||
| cleanup() { | ||
| [[ -n "${SERVE_PID:-}" ]] && kill "$SERVE_PID" 2>/dev/null || true | ||
| rm -rf "$TMP" | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| echo "==> downloading signed builds from CI" | ||
| gh run download "$RUN_ID" -n update-old-build-1.0.0 -D "$TMP/old" | ||
| gh run download "$RUN_ID" -n update-new-build-2.0.0 -D "$TMP/new" | ||
|
|
||
| OLD_ZIP="$(find "$TMP/old" -name 'PostHog-Code-*-arm64-mac.zip' | head -1)" | ||
| FEED_YML="$(find "$TMP/new" -name latest-mac.yml | head -1)" | ||
| [[ -n "$OLD_ZIP" ]] || { | ||
| echo "old build zip not found in artifact" >&2 | ||
| exit 1 | ||
| } | ||
| [[ -n "$FEED_YML" ]] || { | ||
| echo "latest-mac.yml not found in new build artifact" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| echo "==> old 1.0.0 app -> out/mac-arm64" | ||
| rm -rf out/mac-arm64 && mkdir -p out/mac-arm64 | ||
| ditto -x -k "$OLD_ZIP" out/mac-arm64 | ||
| xattr -dr com.apple.quarantine "out/mac-arm64/PostHog Code.app" 2>/dev/null || true | ||
|
|
||
| echo "==> new 2.0.0 feed -> out/dev-update-feed" | ||
| rm -rf out/dev-update-feed && mkdir -p out/dev-update-feed | ||
| cp "$(dirname "$FEED_YML")"/* out/dev-update-feed/ | ||
|
|
||
| if [[ "${AUTOMATED:-}" == "1" ]]; then | ||
| echo "==> running the automated update test" | ||
| pnpm exec playwright test --config=tests/e2e/playwright.update.config.ts | ||
| exit $? | ||
| fi | ||
|
|
||
| PORT="${PORT:-8788}" | ||
| node scripts/dev-update/serve.mjs out/dev-update-feed "$PORT" & | ||
| SERVE_PID=$! | ||
|
|
||
| APP_LOG="out/run-from-ci-app.log" | ||
| echo | ||
| echo "==> launching PostHog Code 1.0.0 (feed http://127.0.0.1:$PORT)" | ||
| echo " In the app: open the update banner, click Download, then Restart." | ||
| echo " It swaps and relaunches into 2.0.0. Quit the app (or Ctrl+C) to finish." | ||
| echo " App output: $APP_LOG update log: ~/.posthog-code/logs/main.log" | ||
| echo | ||
| POSTHOG_E2E_UPDATE_FEED="http://127.0.0.1:$PORT" \ | ||
| "out/mac-arm64/PostHog Code.app/Contents/MacOS/PostHog Code" >"$APP_LOG" 2>&1 || true | ||
|
|
||
| echo "==> app exited; cleaning up" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| #!/usr/bin/env node | ||
| // Dependency-free static file server for the auto-update feed. Serves a directory | ||
| // (latest-mac.yml + zip + blockmap) over HTTP with Range support, which the macOS | ||
| // updater needs. Used by the update E2E and for local manual testing. | ||
| // | ||
| // Usage: node serve.mjs <dir> [port] | ||
| import { createReadStream, statSync } from "node:fs"; | ||
| import { createServer } from "node:http"; | ||
| import { extname, join, normalize } from "node:path"; | ||
|
|
||
| const root = process.argv[2]; | ||
| const port = Number(process.argv[3] ?? 8080); | ||
|
|
||
| if (!root) { | ||
| console.error("Usage: serve.mjs <dir> [port]"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const CONTENT_TYPES = { | ||
| ".yml": "text/yaml; charset=utf-8", | ||
| ".yaml": "text/yaml; charset=utf-8", | ||
| ".zip": "application/zip", | ||
| ".blockmap": "application/octet-stream", | ||
| ".json": "application/json; charset=utf-8", | ||
| }; | ||
|
|
||
| const server = createServer((req, res) => { | ||
| const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]); | ||
| const safePath = normalize(urlPath).replace(/^(\.\.[/\\])+/, ""); | ||
| const filePath = join(root, safePath); | ||
|
|
||
| let stat; | ||
| try { | ||
| stat = statSync(filePath); | ||
| } catch { | ||
| res.writeHead(404); | ||
| res.end("Not found"); | ||
| return; | ||
| } | ||
| if (!stat.isFile()) { | ||
| res.writeHead(404); | ||
| res.end("Not found"); | ||
| return; | ||
| } | ||
|
|
||
| const type = CONTENT_TYPES[extname(filePath)] ?? "application/octet-stream"; | ||
| const range = req.headers.range; | ||
|
|
||
| if (range) { | ||
| const match = /bytes=(\d*)-(\d*)/.exec(range); | ||
| let start = 0; | ||
| let end = stat.size - 1; | ||
| if (match?.[1]) { | ||
| start = Number(match[1]); | ||
| if (match[2]) end = Number(match[2]); | ||
| } else if (match?.[2]) { | ||
| // Suffix range (bytes=-N): the last N bytes of the file. | ||
| start = Math.max(0, stat.size - Number(match[2])); | ||
| } | ||
| res.writeHead(206, { | ||
| "Content-Type": type, | ||
| "Content-Range": `bytes ${start}-${end}/${stat.size}`, | ||
| "Accept-Ranges": "bytes", | ||
| "Content-Length": end - start + 1, | ||
| }); | ||
| createReadStream(filePath, { start, end }).pipe(res); | ||
| return; | ||
| } | ||
|
|
||
| res.writeHead(200, { | ||
| "Content-Type": type, | ||
| "Content-Length": stat.size, | ||
| "Accept-Ranges": "bytes", | ||
| }); | ||
| createReadStream(filePath).pipe(res); | ||
| }); | ||
|
|
||
| server.listen(port, "127.0.0.1", () => { | ||
| console.log(`update feed: http://127.0.0.1:${port} serving ${root}`); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so we need to remove this then right?