Batch macOS signing calls for adhoc releases (#31340)

This commit is contained in:
Neil
2026-10-20 20:16:08 -07:00
committed by GitHub
parent b99552cabc
commit 411b5c024a
5 changed files with 152 additions and 0 deletions
+1
View File
@@ -640,6 +640,7 @@ module.exports = {
icon: 'resources/build/icon.icns',
entitlements: 'resources/build/entitlements.mac.plist',
entitlementsInherit: 'resources/build/entitlements.mac.plist',
...(isMacAdhoc ? { sign: resolve(__dirname, 'scripts/sign-adhoc-mac-app.mjs') } : {}),
signIgnore: [
...bundledRipgrepMacSignIgnore,
...orcadTemplateMacSignIgnore,
+22
View File
@@ -0,0 +1,22 @@
import { createRequire } from 'node:module'
export function signAdhocMacApp(options, { sign, retry }) {
// A custom signer bypasses electron-builder's retry wrapper.
return retry(() => sign({ ...options, batchCodesignCalls: true }), {
retries: 3,
interval: 5000,
backoff: 5000
})
}
export default async function sign(options) {
const { sign: signApplication } = await import('@electron/osx-sign')
const builderRequire = createRequire(createRequire(import.meta.url).resolve('electron-builder'))
const require = createRequire(builderRequire.resolve('app-builder-lib'))
const { retry } = require('builder-util')
const started = performance.now()
await signAdhocMacApp(options, { sign: signApplication, retry })
console.log(
`[mac-sign] batched signing completed in ${Math.round(performance.now() - started)}ms`
)
}
+111
View File
@@ -0,0 +1,111 @@
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import { expect, it } from 'vitest'
import { signAdhocMacApp } from './sign-adhoc-mac-app.mjs'
const require = createRequire(import.meta.url)
const builderRequire = createRequire(require.resolve('electron-builder'))
const appBuilderRequire = createRequire(builderRequire.resolve('app-builder-lib'))
const { resolveFunction } = appBuilderRequire('app-builder-lib/out/util/resolve')
it('preserves file-specific signing policy and retries with fresh options', async () => {
const calls = []
const failure = new Error('timestamp service unavailable')
const options = {
app: '/fixture/Orca.app',
platform: 'darwin',
type: 'distribution',
identity: 'SIGNING_IDENTITY',
identityValidation: false,
keychain: '/fixture/signing.keychain',
binaries: ['/fixture/extra-helper'],
optionsForFile: (path) => ({ entitlements: `${path}.plist`, hardenedRuntime: true }),
ignore: [(path) => path.includes('/orcad-template/')],
preAutoEntitlements: false
}
let policy
const result = await signAdhocMacApp(options, {
async retry(task, retryOptions) {
policy = retryOptions
try {
return await task()
} catch (error) {
expect(error).toBe(failure)
return task()
}
},
async sign(received) {
calls.push(received)
if (calls.length === 1) {
throw failure
}
return 'signed'
}
})
expect(result).toBe('signed')
expect(policy).toEqual({ retries: 3, interval: 5000, backoff: 5000 })
expect(calls).toHaveLength(2)
expect(calls[0]).not.toBe(calls[1])
for (const received of calls) {
expect(received).toEqual({ ...options, batchCodesignCalls: true })
expect(received.optionsForFile).toBe(options.optionsForFile)
expect(received.optionsForFile('/fixture/helper')).toEqual({
entitlements: '/fixture/helper.plist',
hardenedRuntime: true
})
expect(received.ignore[0]('/fixture/orcad-template/helper')).toBe(true)
}
expect(options).not.toHaveProperty('batchCodesignCalls')
})
it('propagates signing failure after the retry policy is exhausted', async () => {
const failure = new Error('signature verification failed')
await expect(
signAdhocMacApp(
{},
{
retry: (task) => task(),
sign: async () => {
throw failure
}
}
)
).rejects.toBe(failure)
})
it.each([
{ ORCA_MAC_ADHOC: '1' },
{},
{ ORCA_MAC_RELEASE: '1' },
{ ORCA_MAC_HOURLY: '1' },
{ ORCA_MAC_DAILY: '1' },
{ ORCA_WIN_ADHOC: '1' }
])('only selects the batched signer for adhoc macOS builds: %j', async (env) => {
const configPath = resolve('config/electron-builder.config.cjs')
const saved = { ...process.env }
try {
for (const name of Object.keys(process.env)) {
if (name.startsWith('ORCA_')) {
delete process.env[name]
}
}
Object.assign(process.env, env)
delete require.cache[configPath]
const config = require(configPath)
if (env.ORCA_MAC_ADHOC === '1') {
expect(config.mac.sign).toBe(resolve('config/scripts/sign-adhoc-mac-app.mjs'))
const hook = await resolveFunction(undefined, config.mac.sign, 'sign', process.cwd())
expect(typeof hook).toBe('function')
expect(hook.name).toBe('sign')
expect(config.mac.signIgnore).toContain('/orcad-template/')
expect(config.forceCodeSigning).toBe(true)
expect(config.mac.hardenedRuntime).toBe(true)
expect(config.mac.notarize).toBe(true)
} else {
expect(config.mac.sign).toBeUndefined()
}
} finally {
process.env = saved
delete require.cache[configPath]
}
})
+1
View File
@@ -226,6 +226,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron/osx-sign": "2.7.0",
"@electron/rebuild": "^4.2.1",
"@monaco-editor/react": "^4.7.0",
"@noble/hashes": "1.8.0",
+17
View File
@@ -287,6 +287,9 @@ importers:
'@electron-toolkit/tsconfig':
specifier: ^2.0.0
version: 2.0.0(@types/node@24.19.2)
'@electron/osx-sign':
specifier: 2.7.0
version: 2.7.0(supports-color@7.2.0)
'@electron/rebuild':
specifier: ^4.2.1
version: 4.2.1(supports-color@7.2.0)
@@ -1041,6 +1044,11 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
'@electron/osx-sign@2.7.0':
resolution: {integrity: sha512-9DGhNqKMl6ibkhUoXbN7OHX2gZznfY10L3ZwG0u6r667Kfb6kec4JEfFTXftoqzmOfZ+OzwDbr4p/nKBMHnz0g==}
engines: {node: '>=22.12.0'}
hasBin: true
'@electron/rebuild@4.2.1':
resolution: {integrity: sha512-IS4pG3BysRrCLJ8xTAfvg+IQDbiUoAzdJy2SfjcYKnLDy5DpCdjnPdhS5Zf5UMfrkB24xu0KC+TqcOxg+eG+6w==}
engines: {node: '>=22.12.0'}
@@ -8944,6 +8952,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@electron/osx-sign@2.7.0(supports-color@7.2.0)':
dependencies:
debug: 4.4.3(supports-color@7.2.0)
isbinaryfile: 4.0.10
plist: 3.1.0
semver: 7.8.1
transitivePeerDependencies:
- supports-color
'@electron/rebuild@4.2.1(supports-color@7.2.0)':
dependencies:
'@malept/cross-spawn-promise': 2.0.0