fix: verify macOS entitlements before release

This commit is contained in:
Neil
2026-05-14 19:02:37 -07:00
parent 8a0b9b6bc5
commit 2d9cf72084
5 changed files with 146 additions and 2 deletions
+3
View File
@@ -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
+6
View File
@@ -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
@@ -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]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<key\b[^>]*>([\s\S]*?)<\/key>|<(\/?)dict\b[^>]*>/g
for (const match of xml.matchAll(tokenPattern)) {
if (match[0].startsWith('<!--') || match[0].startsWith('<![CDATA[')) {
continue
}
if (match[0].startsWith('<key')) {
const currentDict = dictStack.at(-1)
if (!currentDict) {
continue
}
const key = decodeXmlEntities(match[1].trim())
const duplicateLine = lineNumberForIndex(lineStarts, match.index)
const firstLine = currentDict.keys.get(key)
if (firstLine) {
problems.push({ key, firstLine, duplicateLine })
} else {
currentDict.keys.set(key, duplicateLine)
}
continue
}
if (match[2] === '/') {
dictStack.pop()
} else {
dictStack.push({ keys: new Map() })
}
}
return problems
}
function buildLineStarts(text) {
const starts = [0]
for (let index = 0; index < text.length; index += 1) {
if (text[index] === '\n') {
starts.push(index + 1)
}
}
return starts
}
function lineNumberForIndex(lineStarts, targetIndex) {
let low = 0
let high = lineStarts.length - 1
while (low <= high) {
const mid = Math.floor((low + high) / 2)
if (lineStarts[mid] <= targetIndex) {
low = mid + 1
} else {
high = mid - 1
}
}
return high + 1
}
function decodeXmlEntities(value) {
return value.replace(/&(#x?[0-9a-fA-F]+|amp|lt|gt|apos|quot);/g, (entity, body) => {
if (body === 'amp') {
return '&'
}
if (body === 'lt') {
return '<'
}
if (body === 'gt') {
return '>'
}
if (body === 'apos') {
return "'"
}
if (body === 'quot') {
return '"'
}
if (body.startsWith('#x')) {
return String.fromCodePoint(Number.parseInt(body.slice(2), 16))
}
if (body.startsWith('#')) {
return String.fromCodePoint(Number.parseInt(body.slice(1), 10))
}
return entity
})
}
+1
View File
@@ -15,6 +15,7 @@
"prepare": "husky",
"test": "vitest run --config config/vitest.config.ts",
"check:feature-wall-assets": "node config/scripts/check-feature-wall-assets.mjs",
"verify:macos-entitlements": "node config/scripts/verify-macos-entitlements.mjs",
"vendor:feature-wall-assets": "node config/scripts/vendor-feature-wall-assets.mjs",
"tc:node": "tsgo --noEmit -p config/tsconfig.node.json",
"tc:cli": "tsgo --noEmit -p config/tsconfig.tc.cli.json",
-2
View File
@@ -20,7 +20,5 @@
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>