diff --git a/.github/workflows/pr-build-installer.yml b/.github/workflows/pr-build-installer.yml
new file mode 100644
index 0000000000..20c9099125
--- /dev/null
+++ b/.github/workflows/pr-build-installer.yml
@@ -0,0 +1,174 @@
+name: PR Build Installer
+
+# On-demand, signed macOS test builds for a pull request. Add the
+# `build-installer` label to package apps/code and get a downloadable .dmg/.zip
+# attached to the run, with a sticky comment on the PR linking to it. Testers
+# download the artifact and run the app locally.
+#
+# This deliberately does NOT publish a GitHub release or notarize the build. It
+# signs (so the app launches after a right-click â Open) but skips notarization,
+# which lives in the real release pipeline (code-release.yml, on v* tags).
+#
+# Gating: only same-repo PRs run â fork PRs never receive the signing/AWS
+# secrets, so building them would red spuriously.
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, labeled]
+
+concurrency:
+ group: pr-build-installer-${{ github.head_ref || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-macos:
+ # Run on a same-repo PR that carries the `build-installer` label (either
+ # just added, or already present on a push).
+ if: >-
+ github.event.pull_request.head.repo.full_name == github.repository &&
+ (
+ (github.event.action == 'labeled' && github.event.label.name == 'build-installer') ||
+ (github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'build-installer'))
+ )
+ runs-on: macos-15
+ timeout-minutes: 45
+ 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 }}
+ POSTHOG_SOURCEMAP_API_KEY: ${{ secrets.POSTHOG_SOURCEMAP_API_KEY }}
+ POSTHOG_ENV_ID: ${{ secrets.POSTHOG_ENV_ID }}
+ POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
+ CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }}
+ CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ # Sign but do not notarize: a signed-but-unnotarized app launches for a
+ # tester after a right-click â Open. Notarization runs only in the tag
+ # release (code-release.yml).
+ 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@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
+
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.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@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.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
+
+ # build/Assets.car is gitignored; regenerate it so the packaged app ships
+ # the liquid-glass icon (the script falls back to .icns if actool fails).
+ - name: Compile macOS liquid-glass icon
+ working-directory: apps/code
+ run: bash scripts/compile-glass-icon.sh
+
+ - name: Build app
+ working-directory: apps/code
+ run: |
+ pnpm exec electron-vite build
+ pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.ts
+
+ - name: Upload installer artifact
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: code-macos-arm64
+ path: |
+ apps/code/out/*.dmg
+ apps/code/out/*-mac.zip
+ retention-days: 7
+ if-no-files-found: error
+
+ comment:
+ # Update the sticky comment once the build finishes, on success OR failure,
+ # so a failed re-run replaces a prior "ready" comment instead of leaving it
+ # pointing at stale artifacts. Skipped only when build-macos itself was
+ # skipped (no label / fork), where there is nothing to report.
+ needs: build-macos
+ if: ${{ !cancelled() && (needs.build-macos.result == 'success' || needs.build-macos.result == 'failure') }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ issues: write
+ pull-requests: write
+ steps:
+ - name: Post build status on the PR
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ BUILD_RESULT: ${{ needs.build-macos.result }}
+ run: |
+ set -euo pipefail
+ MARKER=""
+ RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+ if [ "${BUILD_RESULT}" = "success" ]; then
+ {
+ echo "$MARKER"
+ echo "### đĨī¸ macOS test build (Apple Silicon)"
+ echo ""
+ echo "A signed macOS build for this PR is ready to download and run."
+ echo ""
+ echo "**[âŦī¸ Open this run and download the \`code-macos-arm64\` artifact](${RUN_URL}#artifacts)** (Apple Silicon / M-series)."
+ echo ""
+ echo "Unzip it, then open the \`.dmg\` (or the \`.app\` inside the \`-mac.zip\`)."
+ echo ""
+ echo "> âšī¸ The build is **signed but not notarized**, so macOS may warn on first launch."
+ echo "> Right-click the app â **Open**, or run \`xattr -dr com.apple.quarantine \"PostHog Code.app\"\`."
+ echo ""
+ echo "Built from ${GITHUB_SHA} ¡ rebuilds on each push while the \`build-installer\` label is present. Downloading requires being signed in to GitHub with repo access."
+ } > comment-body.md
+ else
+ {
+ echo "$MARKER"
+ echo "### đĨī¸ macOS test build failed"
+ echo ""
+ echo "The macOS build for this PR **failed**, so there is no downloadable artifact for the latest commit."
+ echo ""
+ echo "**[đ View the failed run](${RUN_URL})** for logs."
+ echo ""
+ echo "Failed on ${GITHUB_SHA} ¡ rebuilds on each push while the \`build-installer\` label is present."
+ } > comment-body.md
+ fi
+
+ COMMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
+ --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -n1 || true)
+
+ if [ -n "${COMMENT_ID}" ]; then
+ gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" -F body=@comment-body.md
+ else
+ gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F body=@comment-body.md
+ fi