chore: add macOS notarization support for DMG builds (#944)

This commit is contained in:
Copilot
2025-12-21 17:36:01 +01:00
committed by GitHub
parent da122e06f1
commit a5629b8c77
8 changed files with 241 additions and 3 deletions

View File

@@ -50,4 +50,7 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} 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 }}

143
NOTARIZATION.md Normal file
View File

@@ -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)

View File

@@ -116,6 +116,10 @@ yarn ui-test
Create a PR to `release` branch. 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 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 beta release
Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version. Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version.

View File

@@ -49,10 +49,10 @@
"appId": "de.t7n.apps.mqtt-explorer", "appId": "de.t7n.apps.mqtt-explorer",
"category": "public.app-category.developer-tools", "category": "public.app-category.developer-tools",
"hardenedRuntime": true, "hardenedRuntime": true,
"gatekeeperAssess": false,
"publish": [ "publish": [
"github" "github"
], ]
"entitlements": "res/entitlements.mas.plist"
}, },
"linux": { "linux": {
"category": "Development", "category": "Development",
@@ -76,7 +76,8 @@
"buildResources": "res", "buildResources": "res",
"output": "build" "output": "build"
}, },
"afterPack": "./dist/scripts/afterPack.js" "afterPack": "./dist/scripts/afterPack.js",
"afterSign": "./dist/scripts/notarize.js"
}, },
"author": "Thomas Nordquist", "author": "Thomas Nordquist",
"email": "xxnerowingerxx@gmail.com", "email": "xxnerowingerxx@gmail.com",

View File

@@ -117,6 +117,18 @@ async function buildWithOptions(options: builder.CliOptions, buildInfo: BuildInf
? 'res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile' ? 'res/MQTT_Explorer_Store_Distribution_Profile.provisionprofile'
: 'res/MQTTExplorerdmg.provisionprofile' : 'res/MQTTExplorerdmg.provisionprofile'
dotProp.set(packageJson, 'build.mac.provisioningProfile', provisioningProfile) 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 { try {

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

49
scripts/notarize.ts Normal file
View File

@@ -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
}
}