Sign and notarize the desktop DMG (AINFRA-2457)#363
Open
mokagio wants to merge 17 commits into
Open
Conversation
Adds the `set_up_signing` fastlane lane (match, readonly, `developer_id`, team `PZYM8XX95Q`, S3 `a8c-fastlane-match`) under `apps/desktop`, so a build agent fetches the Developer ID Application cert into its keychain before electron-builder runs. Cert delivery is `match` for every Automattic signing path, electron-builder included. Locked against Ruby 3.3.4 (the agent ceiling) with fastlane `2.236`, which re-bundles `multi_json` — so the earlier `<= 2.235` workaround gem is unneeded. Verified by running `bundle exec fastlane set_up_signing`: it pulled the match repo from S3 and installed the real Developer ID cert. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces `mac.identity: null` with the Automattic Developer ID Application identity, enables hardened runtime, and turns on electron-builder's built-in notarization (`mac.notarize.teamId`, driven by `APPLE_API_*` at build time). The bundled `runtime/bin/php` is listed in `mac.binaries`: electron-builder does not auto-sign Mach-O files dropped into `Contents/Resources` via `extraResources`, so an unsigned `php` would otherwise fail notarization. Entitlements grant only what hardened runtime needs here: JIT and unsigned-executable-memory for Electron's V8, and disabled library validation so the app can spawn the separately-signed bundled `php`. They live in `signing/` because the repo gitignores `build/`. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves the release build to the Buildkite macOS queue, where the signing secrets live (GitHub Actions cannot reach them). Mirrors the GHA `release-desktop.yml` build sequence, then runs `set_up_signing` (match) before `npm run dist` so electron-builder signs from the keychain cert and notarizes, and verifies the result with `codesign` + `stapler`. The existing GHA workflow stays as the unsigned/dry path; cutting the release over to this pipeline is a separate human decision. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mokagio
commented
Jun 10, 2026
mokagio
commented
Jun 10, 2026
mokagio
commented
Jun 10, 2026
Co-authored-by: Gio Lodi <giovanni.lodi42@gmail.com>
`shared-pipeline-vars` defines `CI_TOOLKIT_PLUGIN`, `NVM_PLUGIN`, and `IMAGE_ID`, which `pipeline.yml` references; without it the plugin entries render blank and the pipeline upload is rejected. `.xcode-version`/`.nvmrc`/`.ruby-version` pin the agent toolchain. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
electron-builder's `mac.notarize.teamId` is the source of truth for the team. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The xcode-* image has no composer; without it the build dies at status 127. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`spc` source-builds libraries without a pre-built binary (libxml2 hit `cmake` not found); the minimal agent image carries none of the toolchain. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This electron-builder version rejects the `{ teamId }` object form; it takes a
boolean and reads the credentials from `APPLE_API_*` at build time.
---
Generated with the help of Claude Code, https://claude.com/claude-code
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`APPLE_API_KEY` must point at a valid PEM; the secret stores it with newlines as literal `\n`. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`mac.notarize` is now a boolean, so it no longer carries `teamId`; the identity string is the field in electron-builder's config that still embeds it. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
electron-builder picks the certificate type itself and rejects an identity that carries the `Developer ID Application:` prefix. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
electron-builder signs, notarizes and staples `Cortext.app`, then wraps it in an unsigned `.dmg`; verifying the dmg's signature failed the build even though the app was correctly notarized. --- Generated with the help of Claude Code, https://claude.com/claude-code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mokagio
commented
Jun 11, 2026
Comment on lines
+46
to
+57
| # electron-builder signs from the match-installed keychain cert (mac.identity) | ||
| # and notarizes via its built-in @electron/notarize, driven by APPLE_API_*. | ||
| # APPLE_API_KEY must be a path to the .p8, so materialize the key the agent | ||
| # carries as APP_STORE_CONNECT_API_KEY_KEY into a temp file. | ||
| apple_api_key_path="$(mktemp -t cortext_asc).p8" | ||
| trap 'rm -f "$apple_api_key_path"' EXIT | ||
| # The secret stores the .p8 with newlines as literal \n; %b turns them back into | ||
| # real newlines so the file is a valid PEM (a no-op if they are already real). | ||
| printf '%b' "$APP_STORE_CONNECT_API_KEY_KEY" > "$apple_api_key_path" | ||
| export APPLE_API_KEY="$apple_api_key_path" | ||
| export APPLE_API_KEY_ID="$APP_STORE_CONNECT_API_KEY_KEY_ID" | ||
| export APPLE_API_ISSUER="$APP_STORE_CONNECT_API_KEY_ISSUER_ID" |
Author
There was a problem hiding this comment.
This could be hidden away in CI toolkit if it proves a pattern used by other apps.
I considered adding the env vars directly in CI, but I think it's cleaner to have only the APP_STORE_CONNECT_API_KEY_ definitions there.
There was a problem hiding this comment.
Pull request overview
This PR introduces a signing + notarization path for the macOS desktop DMG, moving the “release-grade” build to Buildkite so Developer ID certificates and App Store Connect API credentials can be used securely on macOS agents.
Changes:
- Adds macOS hardened-runtime entitlements and updates electron-builder config to enable signing + notarization.
- Introduces a Fastlane lane to fetch Developer ID signing certs via
match(S3-backed). - Adds a Buildkite pipeline + release script to build, sign, notarize, verify, and (for tags) upload DMGs to GitHub Releases; also pins toolchain versions (Node/Ruby/Xcode).
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/desktop/signing/entitlements.mac.plist | Adds hardened runtime entitlements for Electron + bundled runtime. |
| apps/desktop/package.json | Enables macOS signing identity, hardened runtime, entitlements, extra binaries signing, and notarization. |
| apps/desktop/Gemfile.lock | Adds pinned Ruby gem dependency lock for Fastlane tooling. |
| apps/desktop/Gemfile | Introduces Fastlane + wpmreleasetoolkit plugin dependencies. |
| apps/desktop/fastlane/Fastfile | Adds set_up_signing lane to fetch Developer ID certs via match. |
| apps/desktop/fastlane/.gitignore | Ignores generated Fastlane artifacts. |
| apps/desktop/.bundle/config | Bundler configuration for vendored gems/platform behavior. |
| .xcode-version | Pins Xcode version for Buildkite image selection. |
| .ruby-version | Pins Ruby version for Fastlane execution. |
| .nvmrc | Pins Node major/minor used by CI. |
| .buildkite/shared-pipeline-vars | Exports shared Buildkite variables (plugins/image id). |
| .buildkite/pipeline.yml | Adds Buildkite mac pipeline definition for signing/notarization. |
| .buildkite/commands/release-desktop.sh | Buildkite command script to build, sign, notarize, verify, and upload DMGs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+36
to
+38
| # `set_up` is needed even with no `.env` file so EnvManager has a configured | ||
| # instance; on CI the vars come from the Buildkite agent. | ||
| EnvManager.set_up(env_file_name: 'cortext-desktop.env') |
Comment on lines
+57
to
+66
| "identity": "Automattic, Inc. (PZYM8XX95Q)", | ||
| "category": "public.app-category.productivity", | ||
| "minimumSystemVersion": "12.0.0" | ||
| "minimumSystemVersion": "12.0.0", | ||
| "hardenedRuntime": true, | ||
| "entitlements": "signing/entitlements.mac.plist", | ||
| "entitlementsInherit": "signing/entitlements.mac.plist", | ||
| "binaries": [ | ||
| "Contents/Resources/runtime/bin/php" | ||
| ], | ||
| "notarize": true |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
priethor
reviewed
Jun 11, 2026
The desktop release workflow should publish for the repo's existing tag convention, which omits the leading v. Keep accepting v-prefixed tags so existing release inputs remain compatible. --- Generated with the help of Codex, https://openai.com/codex Co-Authored-By: Codex GPT-5 <noreply@openai.com>
The desktop release workflow should match the repo's tag convention exactly. Tags with a leading v should stay on the artifact-only path. --- Generated with the help of Codex, https://openai.com/codex Co-Authored-By: Codex GPT-5 <noreply@openai.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Part of AINFRA-2457. Adds the scripts and CI configuration to build and sign the macOS app.
Notice the introduction of Buildkite for this step instead of GitHub Actions. Buildkite is a CI provider we can better control than GHA when it comes to macOS builds and security.
You can download the DMG from here.
AI-generated details below. Fun fact, this was all automated based on a prompt +
/goal open PR with green CI and notarized artifactin Claude CodeWhy
The desktop DMG ships unsigned today (
mac.identity: null), so users hit Gatekeeper's "damaged"/quarantine prompt and run anxattrworkaround.docs/desktop-decisions.mddeferred signing until we had an Apple Developer ID — we do (PZYM8XX95Q), and signing also unblocks in-place updates. This wires up Developer ID signing + notarization for the arm64 build.Approach
match(the org standard for every signing path): a newapps/desktopfastlaneset_up_signinglane fetches the Developer ID Application cert into the build keychain. electron-builder then signs from the keychain — noCSC_LINK.identity, hardened runtime, notarization (mac.notarize.teamId+APPLE_API_*), and the bundledruntime/bin/phpadded tomac.binaries(electron-builder doesn't auto-sign Mach-O files underextraResources).queue: mac): GitHub Actions can't reach the signing secrets. The new pipeline mirrors the GHA build, signs, notarizes, and verifies. The existing GHA workflow stays as the unsigned/dry path; the release cutover is a separate decision.Gotchas for the reviewer
apps/desktop/signing/, not the electron-builder defaultbuild/, because the repo gitignoresbuild/.matchlane is proven locally (it pulled the cert from S3 and installed it); the full signed build is not yet validated end-to-end — it needs the Buildkite pipeline registered +APP_STORE_CONNECT_API_KEY_*on the agents (see below).How to test
Cert delivery:
cd apps/desktop && bundle exec fastlane set_up_signing(needsMATCH_*). Full build runs on the Buildkite pipeline once registered.🤖 Generated with Claude Code