Skip to content

Commit ca288d4

Browse files
authored
ci: label-gated signed macOS test build for PRs (#3244)
1 parent e5ce2e3 commit ca288d4

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
name: PR Build Installer
2+
3+
# On-demand, signed macOS test builds for a pull request. Add the
4+
# `build-installer` label to package apps/code and get a downloadable .dmg/.zip
5+
# attached to the run, with a sticky comment on the PR linking to it. Testers
6+
# download the artifact and run the app locally.
7+
#
8+
# This deliberately does NOT publish a GitHub release or notarize the build. It
9+
# signs (so the app launches after a right-click → Open) but skips notarization,
10+
# which lives in the real release pipeline (code-release.yml, on v* tags).
11+
#
12+
# Gating: only same-repo PRs run — fork PRs never receive the signing/AWS
13+
# secrets, so building them would red spuriously.
14+
15+
on:
16+
pull_request:
17+
types: [opened, synchronize, reopened, labeled]
18+
19+
concurrency:
20+
group: pr-build-installer-${{ github.head_ref || github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
build-macos:
25+
# Run on a same-repo PR that carries the `build-installer` label (either
26+
# just added, or already present on a push).
27+
if: >-
28+
github.event.pull_request.head.repo.full_name == github.repository &&
29+
(
30+
(github.event.action == 'labeled' && github.event.label.name == 'build-installer') ||
31+
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'build-installer'))
32+
)
33+
runs-on: macos-15
34+
timeout-minutes: 45
35+
permissions:
36+
id-token: write
37+
contents: read
38+
env:
39+
NODE_OPTIONS: "--max-old-space-size=8192"
40+
NODE_ENV: production
41+
npm_config_arch: arm64
42+
npm_config_platform: darwin
43+
VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }}
44+
VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }}
45+
POSTHOG_SOURCEMAP_API_KEY: ${{ secrets.POSTHOG_SOURCEMAP_API_KEY }}
46+
POSTHOG_ENV_ID: ${{ secrets.POSTHOG_ENV_ID }}
47+
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
48+
CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }}
49+
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }}
50+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
51+
# Sign but do not notarize: a signed-but-unnotarized app launches for a
52+
# tester after a right-click → Open. Notarization runs only in the tag
53+
# release (code-release.yml).
54+
SKIP_NOTARIZE: "1"
55+
steps:
56+
- name: Checkout
57+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
58+
with:
59+
fetch-depth: 0
60+
persist-credentials: false
61+
62+
- name: Setup pnpm
63+
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
64+
65+
- name: Setup Node.js
66+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
67+
with:
68+
node-version: 22
69+
cache: "pnpm"
70+
71+
- name: Install dependencies
72+
run: pnpm install --frozen-lockfile
73+
74+
- name: Configure AWS credentials
75+
uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6.2.1
76+
with:
77+
role-to-assume: ${{ secrets.AWS_TWIG_APP_ASSETS_ROLE_ARN }}
78+
aws-region: ${{ secrets.AWS_TWIG_APP_ASSETS_REGION }}
79+
mask-aws-account-id: true
80+
unset-current-credentials: true
81+
82+
- name: Download BerkeleyMono fonts from S3
83+
run: aws s3 cp s3://${{ secrets.AWS_TWIG_APP_ASSETS_BUCKET }}/fonts/BerkeleyMono/ apps/code/assets/fonts/BerkeleyMono/ --recursive
84+
85+
- name: Build workspace packages
86+
run: |
87+
pnpm --filter @posthog/electron-trpc run build
88+
pnpm --filter @posthog/platform run build
89+
pnpm --filter @posthog/shared run build
90+
pnpm --filter @posthog/git run build
91+
pnpm --filter @posthog/enricher run build
92+
pnpm --filter @posthog/agent run build
93+
94+
# build/Assets.car is gitignored; regenerate it so the packaged app ships
95+
# the liquid-glass icon (the script falls back to .icns if actool fails).
96+
- name: Compile macOS liquid-glass icon
97+
working-directory: apps/code
98+
run: bash scripts/compile-glass-icon.sh
99+
100+
- name: Build app
101+
working-directory: apps/code
102+
run: |
103+
pnpm exec electron-vite build
104+
pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.ts
105+
106+
- name: Upload installer artifact
107+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
108+
with:
109+
name: code-macos-arm64
110+
path: |
111+
apps/code/out/*.dmg
112+
apps/code/out/*-mac.zip
113+
retention-days: 7
114+
if-no-files-found: error
115+
116+
comment:
117+
# Update the sticky comment once the build finishes, on success OR failure,
118+
# so a failed re-run replaces a prior "ready" comment instead of leaving it
119+
# pointing at stale artifacts. Skipped only when build-macos itself was
120+
# skipped (no label / fork), where there is nothing to report.
121+
needs: build-macos
122+
if: ${{ !cancelled() && (needs.build-macos.result == 'success' || needs.build-macos.result == 'failure') }}
123+
runs-on: ubuntu-latest
124+
timeout-minutes: 5
125+
permissions:
126+
issues: write
127+
pull-requests: write
128+
steps:
129+
- name: Post build status on the PR
130+
env:
131+
GH_TOKEN: ${{ github.token }}
132+
PR_NUMBER: ${{ github.event.pull_request.number }}
133+
BUILD_RESULT: ${{ needs.build-macos.result }}
134+
run: |
135+
set -euo pipefail
136+
MARKER="<!-- pr-installer-build -->"
137+
RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
138+
if [ "${BUILD_RESULT}" = "success" ]; then
139+
{
140+
echo "$MARKER"
141+
echo "### 🖥️ macOS test build (Apple Silicon)"
142+
echo ""
143+
echo "A signed macOS build for this PR is ready to download and run."
144+
echo ""
145+
echo "**[⬇️ Open this run and download the \`code-macos-arm64\` artifact](${RUN_URL}#artifacts)** (Apple Silicon / M-series)."
146+
echo ""
147+
echo "Unzip it, then open the \`.dmg\` (or the \`.app\` inside the \`-mac.zip\`)."
148+
echo ""
149+
echo "> ℹ️ The build is **signed but not notarized**, so macOS may warn on first launch."
150+
echo "> Right-click the app → **Open**, or run \`xattr -dr com.apple.quarantine \"PostHog Code.app\"\`."
151+
echo ""
152+
echo "<sub>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.</sub>"
153+
} > comment-body.md
154+
else
155+
{
156+
echo "$MARKER"
157+
echo "### 🖥️ macOS test build failed"
158+
echo ""
159+
echo "The macOS build for this PR **failed**, so there is no downloadable artifact for the latest commit."
160+
echo ""
161+
echo "**[🔎 View the failed run](${RUN_URL})** for logs."
162+
echo ""
163+
echo "<sub>Failed on ${GITHUB_SHA} · rebuilds on each push while the \`build-installer\` label is present.</sub>"
164+
} > comment-body.md
165+
fi
166+
167+
COMMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --paginate \
168+
--jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | head -n1 || true)
169+
170+
if [ -n "${COMMENT_ID}" ]; then
171+
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${COMMENT_ID}" -F body=@comment-body.md
172+
else
173+
gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" -F body=@comment-body.md
174+
fi

0 commit comments

Comments
 (0)