Skip to content

Sign and notarize the desktop DMG (AINFRA-2457)#363

Open
mokagio wants to merge 17 commits into
mainfrom
mokagio/desktop-code-signing
Open

Sign and notarize the desktop DMG (AINFRA-2457)#363
mokagio wants to merge 17 commits into
mainfrom
mokagio/desktop-code-signing

Conversation

@mokagio

@mokagio mokagio commented Jun 10, 2026

Copy link
Copy Markdown

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.

Screenshot 2026-06-11 at 2 43 46 pm Screenshot 2026-06-11 at 2 43 52 pm

AI-generated details below. Fun fact, this was all automated based on a prompt + /goal open PR with green CI and notarized artifact in Claude Code


Why

The desktop DMG ships unsigned today (mac.identity: null), so users hit Gatekeeper's "damaged"/quarantine prompt and run an xattr workaround. docs/desktop-decisions.md deferred 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

  • Cert delivery via match (the org standard for every signing path): a new apps/desktop fastlane set_up_signing lane fetches the Developer ID Application cert into the build keychain. electron-builder then signs from the keychain — no CSC_LINK.
  • electron-builder config: real identity, hardened runtime, notarization (mac.notarize.teamId + APPLE_API_*), and the bundled runtime/bin/php added to mac.binaries (electron-builder doesn't auto-sign Mach-O files under extraResources).
  • Build moves to Buildkite (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

  • Entitlements live in apps/desktop/signing/, not the electron-builder default build/, because the repo gitignores build/.
  • The match lane 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).
  • The mac agent's PHP/composer availability for the snapshot build is unconfirmed — first thing to watch on a real CI run.

How to test

Cert delivery: cd apps/desktop && bundle exec fastlane set_up_signing (needs MATCH_*). Full build runs on the Buildkite pipeline once registered.


🤖 Generated with Claude Code

mokagio and others added 3 commits June 10, 2026 12:35
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 mokagio added the type: tooling CI, release, packaging, scripts, dependencies, and repo automation. label Jun 10, 2026
@mokagio mokagio self-assigned this Jun 10, 2026
Comment thread .buildkite/commands/release-desktop.sh Outdated
Comment thread .buildkite/commands/release-desktop.sh Outdated
Comment thread .buildkite/pipeline.yml Outdated
mokagio and others added 10 commits June 10, 2026 14:54
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>
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"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mokagio mokagio marked this pull request as ready for review June 11, 2026 04:43
Copilot AI review requested due to automatic review settings June 11, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thread .buildkite/pipeline.yml
Comment thread .buildkite/commands/release-desktop.sh Outdated
Comment thread apps/desktop/fastlane/Fastfile Outdated
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 thread apps/desktop/package.json
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
mokagio and others added 2 commits June 11, 2026 15:42
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 priethor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @mokagio !

Comment thread .buildkite/commands/release-desktop.sh Outdated
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: tooling CI, release, packaging, scripts, dependencies, and repo automation.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants