CI: set up macOS build & publish pipeline #165

Open
opened 2026-04-15 17:32:13 +00:00 by talksik · 0 comments
talksik commented 2026-04-15 17:32:13 +00:00 (Migrated from github.com)

Context

Windows builds are now automated via .github/workflows/build-windows.yml (signed with Azure Trusted Signing, published to GCS). macOS releases are still cut manually from a dev machine (yarn publish:mac), which relies on:

  • A Developer ID Application certificate in the local Keychain
  • A notarytool keychain profile named default

We should bring macOS to parity with Windows so releases are reproducible, don't depend on one laptop, and survive cert/profile changes.

Scope

  • New workflow .github/workflows/build-mac.yml running on macos-latest for both arm64 and x64.
  • Import Developer ID cert into a temporary keychain at job start (e.g. apple-actions/import-codesign-certs@v3).
  • Notarize via App Store Connect API key (.p8 + Key ID + Issuer) — env-var-based, not keychainProfile.
  • GCP auth via existing PROD_GKE_SERVICE_ACCOUNT_KEY secret; publish via @electron-forge/publisher-gcs.
  • Invalidate RELEASES.json cache for darwin/arm64 and darwin/x64 after publish.

Changes required

js/forge.config.ts

Switch osxNotarize to prefer env-var-based API-key credentials when present, falling back to the local keychainProfile: 'default' so yarn publish:mac from a dev machine keeps working:

osxNotarize: process.env.APPLE_API_KEY ? {
  appleApiKey: process.env.APPLE_API_KEY,
  appleApiKeyId: process.env.APPLE_API_KEY_ID!,
  appleApiIssuer: process.env.APPLE_API_ISSUER!,
} : { keychainProfile: 'default' }

osxSign: {} auto-discovers from the keychain and needs no change — it works in both the local keychain and a temp CI keychain.

js/package.json

Current publish:mac has an interactive read -r answer version-bump prompt that will hang CI. Open question — pick one:

  • A — drop the prompt; trust devs to bump package.json before pushing
  • B — trigger only on v* tags and assert the tag matches package.json version in CI
  • C — keep interactive publish:mac for local; add non-interactive publish:mac:ci

New GitHub secrets

  • APPLE_CERT_P12_BASE64 — base64 of the exported Developer ID .p12
  • APPLE_CERT_PASSWORD — the .p12 export password
  • APPLE_API_KEY_P8_BASE64 — base64 of the App Store Connect API key .p8
  • APPLE_API_KEY_ID — 10-char key ID
  • APPLE_API_ISSUER — issuer UUID

Gotchas

  • Notarization queue time is 2–15 min of billed runner time per arch.
  • .p12 export must include the private key, not just the cert.
  • If the API key is ever rotated, all secrets above change together.

Out of scope

  • Switching publisher-gcs → a signed-URL-based CDN invalidation strategy. The current gsutil setmeta approach is fine.
  • Staged rollouts — not supported by Squirrel.Mac either.
## Context Windows builds are now automated via `.github/workflows/build-windows.yml` (signed with Azure Trusted Signing, published to GCS). macOS releases are still cut manually from a dev machine (`yarn publish:mac`), which relies on: - A Developer ID Application certificate in the local Keychain - A `notarytool` keychain profile named `default` We should bring macOS to parity with Windows so releases are reproducible, don't depend on one laptop, and survive cert/profile changes. ## Scope - New workflow `.github/workflows/build-mac.yml` running on `macos-latest` for both `arm64` and `x64`. - Import Developer ID cert into a temporary keychain at job start (e.g. `apple-actions/import-codesign-certs@v3`). - Notarize via App Store Connect API key (`.p8` + Key ID + Issuer) — env-var-based, not `keychainProfile`. - GCP auth via existing `PROD_GKE_SERVICE_ACCOUNT_KEY` secret; publish via `@electron-forge/publisher-gcs`. - Invalidate `RELEASES.json` cache for `darwin/arm64` and `darwin/x64` after publish. ## Changes required ### `js/forge.config.ts` Switch `osxNotarize` to prefer env-var-based API-key credentials when present, falling back to the local `keychainProfile: 'default'` so `yarn publish:mac` from a dev machine keeps working: ```ts osxNotarize: process.env.APPLE_API_KEY ? { appleApiKey: process.env.APPLE_API_KEY, appleApiKeyId: process.env.APPLE_API_KEY_ID!, appleApiIssuer: process.env.APPLE_API_ISSUER!, } : { keychainProfile: 'default' } ``` `osxSign: {}` auto-discovers from the keychain and needs no change — it works in both the local keychain and a temp CI keychain. ### `js/package.json` Current `publish:mac` has an interactive `read -r answer` version-bump prompt that will hang CI. Open question — pick one: - **A** — drop the prompt; trust devs to bump `package.json` before pushing - **B** — trigger only on `v*` tags and assert the tag matches `package.json` version in CI - **C** — keep interactive `publish:mac` for local; add non-interactive `publish:mac:ci` ### New GitHub secrets - `APPLE_CERT_P12_BASE64` — base64 of the exported Developer ID `.p12` - `APPLE_CERT_PASSWORD` — the `.p12` export password - `APPLE_API_KEY_P8_BASE64` — base64 of the App Store Connect API key `.p8` - `APPLE_API_KEY_ID` — 10-char key ID - `APPLE_API_ISSUER` — issuer UUID ## Gotchas - Notarization queue time is 2–15 min of billed runner time per arch. - `.p12` export must include the private key, not just the cert. - If the API key is ever rotated, all secrets above change together. ## Out of scope - Switching publisher-gcs → a signed-URL-based CDN invalidation strategy. The current `gsutil setmeta` approach is fine. - Staged rollouts — not supported by Squirrel.Mac either.
Sign in to join this conversation.