From a5629b8c77372442141fe4da40198f39e283d8f8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 17:36:01 +0100 Subject: [PATCH] chore: add macOS notarization support for DMG builds (#944) --- .github/workflows/platform-builds.yml | 3 + NOTARIZATION.md | 143 ++++++++++++++++++++++++++ Readme.md | 4 + package.json | 7 +- package.ts | 12 +++ res/entitlements.mac.inherit.plist | 12 +++ res/entitlements.mac.plist | 14 +++ scripts/notarize.ts | 49 +++++++++ 8 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 NOTARIZATION.md create mode 100644 res/entitlements.mac.inherit.plist create mode 100644 res/entitlements.mac.plist create mode 100644 scripts/notarize.ts diff --git a/.github/workflows/platform-builds.yml b/.github/workflows/platform-builds.yml index c657727..82e8389 100644 --- a/.github/workflows/platform-builds.yml +++ b/.github/workflows/platform-builds.yml @@ -50,4 +50,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} \ No newline at end of file diff --git a/NOTARIZATION.md b/NOTARIZATION.md new file mode 100644 index 0000000..22bd64a --- /dev/null +++ b/NOTARIZATION.md @@ -0,0 +1,143 @@ +# macOS Notarization Setup + +This document explains how to set up notarization for macOS builds of MQTT Explorer. + +## Overview + +macOS notarization is a security feature required by Apple for all software distributed outside the Mac App Store. Starting with macOS 10.15 (Catalina), all software must be notarized to run without warnings on macOS. + +## Prerequisites + +1. An active Apple Developer account +2. Xcode command line tools installed on the build machine +3. An app-specific password for notarization + +## Setup Steps + +### 1. Create an App-Specific Password + +1. Sign in to [appleid.apple.com](https://appleid.apple.com) +2. Navigate to "Sign-In and Security" section +3. Under "App-Specific Passwords", click "Generate an app-specific password" +4. Enter a descriptive name (e.g., "MQTT Explorer Notarization") +5. Copy the generated password (you won't be able to see it again) + +### 2. Find Your Team ID + +1. Sign in to [developer.apple.com](https://developer.apple.com/account) +2. Navigate to "Membership Details" +3. Copy your Team ID (a 10-character alphanumeric string) + +### 3. Configure GitHub Secrets + +Add the following secrets to your GitHub repository: + +- `APPLE_ID`: Your Apple ID email address (e.g., `your.email@example.com`) +- `APPLE_APP_SPECIFIC_PASSWORD`: The app-specific password created in step 1 +- `APPLE_TEAM_ID`: Your Team ID from step 2 + +To add secrets: +1. Go to your repository on GitHub +2. Navigate to Settings → Secrets and variables → Actions +3. Click "New repository secret" for each of the above + +## How It Works + +### Build Configuration + +The notarization process is configured in `package.json`: + +```json +{ + "build": { + "mac": { + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "res/entitlements.mac.plist", + "entitlementsInherit": "res/entitlements.mac.inherit.plist" + }, + "afterSign": "./dist/scripts/notarize.js" + } +} +``` + +### Notarization Script + +The `scripts/notarize.ts` script handles the notarization process: + +1. Checks if the build is for macOS +2. Verifies that required environment variables are set +3. Submits the app to Apple's notarization service +4. Waits for notarization to complete +5. Staples the notarization ticket to the app + +### Entitlements + +Different entitlements are used for different build types: + +- **DMG builds** (regular distribution): + - `res/entitlements.mac.plist` - Main entitlements + - `res/entitlements.mac.inherit.plist` - Inherited entitlements for child processes + +- **MAS builds** (Mac App Store): + - `res/entitlements.mas.plist` - App Store specific entitlements + +### CI/CD Integration + +The GitHub Actions workflow (`platform-builds.yml`) automatically: + +1. Builds the macOS app when code is pushed to `release` or `beta` branches +2. Signs the app with the developer certificate +3. Notarizes the app using the configured secrets +4. Publishes the notarized app to GitHub releases + +## Troubleshooting + +### Notarization Fails + +If notarization fails, check: + +1. **Credentials**: Ensure all three secrets are correctly set in GitHub +2. **App-specific password**: Verify it hasn't expired or been revoked +3. **Team ID**: Confirm it matches your developer account +4. **Entitlements**: Ensure the entitlements files are valid and appropriate for your app + +### Checking Notarization Status + +You can check the notarization status of a built app: + +```bash +# Check if an app is notarized +spctl -a -vv /path/to/MQTT\ Explorer.app + +# Check notarization history +xcrun notarytool history --apple-id your.email@example.com --team-id YOUR_TEAM_ID +``` + +### Local Testing + +To test notarization locally: + +```bash +# Set environment variables +export APPLE_ID="your.email@example.com" +export APPLE_APP_SPECIFIC_PASSWORD="your-app-specific-password" +export APPLE_TEAM_ID="YOUR_TEAM_ID" + +# Build and notarize +yarn build +yarn package mac +``` + +## Security Considerations + +- Never commit Apple credentials to the repository +- Use app-specific passwords, not your main Apple ID password +- Rotate app-specific passwords periodically +- Limit access to GitHub secrets to trusted maintainers only + +## References + +- [Apple Notarization Documentation](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution) +- [electron-builder Code Signing](https://www.electron.build/code-signing) +- [@electron/notarize](https://github.com/electron/notarize) diff --git a/Readme.md b/Readme.md index 9b7720f..6923282 100644 --- a/Readme.md +++ b/Readme.md @@ -116,6 +116,10 @@ yarn ui-test Create a PR to `release` branch. There needs to be a "feat: some new feature" or "fix: some bugfix" commit for a new release to be created +### macOS Notarization + +macOS builds are automatically notarized during the release process. To set up notarization credentials, see [NOTARIZATION.md](NOTARIZATION.md). + ## Create a beta release Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version. diff --git a/package.json b/package.json index 999e6f8..da5fa1e 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,10 @@ "appId": "de.t7n.apps.mqtt-explorer", "category": "public.app-category.developer-tools", "hardenedRuntime": true, + "gatekeeperAssess": false, "publish": [ "github" - ], - "entitlements": "res/entitlements.mas.plist" + ] }, "linux": { "category": "Development", @@ -76,7 +76,8 @@ "buildResources": "res", "output": "build" }, - "afterPack": "./dist/scripts/afterPack.js" + "afterPack": "./dist/scripts/afterPack.js", + "afterSign": "./dist/scripts/notarize.js" }, "author": "Thomas Nordquist", "email": "xxnerowingerxx@gmail.com", diff --git a/package.ts b/package.ts index db06d65..9e44d07 100644 --- a/package.ts +++ b/package.ts @@ -117,6 +117,18 @@ async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInf ? 'res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile' : 'res/MQTTExplorerdmg.provisionprofile' dotProp.set(packageJson, 'build.mac.provisioningProfile', provisioningProfile) + + // Set different entitlements for MAS vs DMG builds + if (buildInfo.package === 'mas') { + // MAS builds use the same sandboxed entitlements for parent and child processes + dotProp.set(packageJson, 'build.mac.entitlements', 'res/entitlements.mas.plist') + dotProp.set(packageJson, 'build.mac.entitlementsInherit', 'res/entitlements.mas.plist') + } else { + // DMG builds use different entitlements for notarization + // Parent app has network permissions, child processes have minimal permissions + dotProp.set(packageJson, 'build.mac.entitlements', 'res/entitlements.mac.plist') + dotProp.set(packageJson, 'build.mac.entitlementsInherit', 'res/entitlements.mac.inherit.plist') + } } try { diff --git a/res/entitlements.mac.inherit.plist b/res/entitlements.mac.inherit.plist new file mode 100644 index 0000000..38c887b --- /dev/null +++ b/res/entitlements.mac.inherit.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/res/entitlements.mac.plist b/res/entitlements.mac.plist new file mode 100644 index 0000000..52b3fc5 --- /dev/null +++ b/res/entitlements.mac.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + + diff --git a/scripts/notarize.ts b/scripts/notarize.ts new file mode 100644 index 0000000..4b7d9cd --- /dev/null +++ b/scripts/notarize.ts @@ -0,0 +1,49 @@ +import { notarize } from '@electron/notarize' +import * as path from 'path' + +interface Context { + electronPlatformName: string + appOutDir: string + packager: { + appInfo: { + productFilename: string + } + } +} + +export default async function notarizing(context: Context) { + const { electronPlatformName, appOutDir } = context + + // Only notarize macOS builds + if (electronPlatformName !== 'darwin') { + return + } + + // Check for required environment variables + const appleId = process.env.APPLE_ID + const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD + const teamId = process.env.APPLE_TEAM_ID + + if (!appleId || !appleIdPassword || !teamId) { + console.warn('Skipping notarization: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, or APPLE_TEAM_ID not set') + return + } + + const appName = context.packager.appInfo.productFilename + const appPath = path.join(appOutDir, `${appName}.app`) + + console.log(`Notarizing ${appPath}...`) + + try { + await notarize({ + appPath, + appleId, + appleIdPassword, + teamId, + }) + console.log('Notarization successful!') + } catch (error) { + console.error('Notarization failed:', error) + throw error + } +}