From 2d9cf720845e6e94829d0feebec1f87f1488e918 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 14 May 2026 19:02:37 -0700 Subject: [PATCH] fix: verify macOS entitlements before release --- .github/workflows/pr.yml | 3 + .github/workflows/release-cut.yml | 6 + config/scripts/verify-macos-entitlements.mjs | 136 +++++++++++++++++++ package.json | 1 + resources/build/entitlements.mac.plist | 2 - 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 config/scripts/verify-macos-entitlements.mjs diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b70a92d35dc..22b332c1a21 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -71,6 +71,9 @@ jobs: - name: Check feature wall asset budget run: pnpm check:feature-wall-assets + - name: Verify macOS entitlements + run: pnpm verify:macos-entitlements + - name: Typecheck run: pnpm typecheck diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 0f2dc8ba6c0..5128f74d079 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -634,6 +634,12 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Why: `plutil -lint` accepts duplicate plist keys, but `codesign` + # rejects duplicate entitlements after the expensive app build. + - name: Verify macOS entitlements + if: matrix.platform == 'mac' + run: pnpm verify:macos-entitlements + # Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`) # requires the build identity to be the literal string `stable` or `rc`, # substituted by electron-vite's `define` block at build time. Derive diff --git a/config/scripts/verify-macos-entitlements.mjs b/config/scripts/verify-macos-entitlements.mjs new file mode 100644 index 00000000000..20bb7230510 --- /dev/null +++ b/config/scripts/verify-macos-entitlements.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const repoRoot = resolve(__dirname, '../..') + +const defaultPlists = [ + 'resources/build/entitlements.mac.plist', + 'resources/build/entitlements.computer-use.mac.plist' +] + +const plistPaths = process.argv.slice(2) +const pathsToCheck = plistPaths.length > 0 ? plistPaths : defaultPlists + +let failed = false + +for (const plistPath of pathsToCheck) { + const absolutePath = resolve(repoRoot, plistPath) + const xml = readFileSync(absolutePath, 'utf8') + const problems = findDuplicateDictKeys(xml) + + if (problems.length === 0) { + console.log(`${plistPath}: OK`) + continue + } + + failed = true + console.error(`${plistPath}: duplicate plist dict keys found`) + for (const problem of problems) { + console.error( + `- ${problem.key} first appears on line ${problem.firstLine}, duplicated on line ${problem.duplicateLine}` + ) + } +} + +if (failed) { + process.exit(1) +} + +function findDuplicateDictKeys(xml) { + const problems = [] + const lineStarts = buildLineStarts(xml) + const dictStack = [] + // Why: `plutil -lint` accepts duplicate keys, but `codesign` rejects + // duplicate entitlements during release signing. + const tokenPattern = + /||]*>([\s\S]*?)<\/key>|<(\/?)dict\b[^>]*>/g + + for (const match of xml.matchAll(tokenPattern)) { + if (match[0].startsWith('