Skip to content

Commit f156bb1

Browse files
committed
Fix(release): macOS signing survives electron-builder's keychain password bug
The macOS release job died inside code signing: electron-builder creates a temporary keychain with a random password, then runs `security set-key-partition-list -k` with the certificate's import password instead. Older macOS let that slide on an already-unlocked keychain, and 2.43.0 signed fine two days ago on the same electron-builder 26.15.7; the macOS 26.6 runner image now verifies the password and refuses ("SecKeychainUnlock: The user name or passphrase you entered is not correct"). Upstream fixed it in electron-builder #10101 (issue #10066), but the fix is missing from 26.16.0 and the v26 backport is unreleased. Until a release carries it, the macOS job applies the same two-line change to the installed app-builder-lib through a small script. The script is idempotent, becomes a no-op once the installed copy has the fix, and fails loudly if the file stops looking like either version so it gets dropped rather than silently skipped. Local builds are untouched: they sign from the login keychain and never enter this code path. Claude-Session: https://claude.ai/code/session_015HNdWonTE8g6dPY2SkdsRS
1 parent e56f9ec commit f156bb1

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ jobs:
7777
- name: Install dependencies
7878
run: npm ci
7979

80+
- name: Carry the electron-builder keychain fix
81+
if: matrix.name == 'macOS'
82+
# electron-builder 26.15 and 26.16 hand `security set-key-partition-list`
83+
# the certificate's import password instead of the temporary keychain's
84+
# own password; the macOS 26.6 runner image verifies it and signing
85+
# dies with "SecKeychainUnlock: The user name or passphrase you entered
86+
# is not correct". The fix is merged upstream (electron-builder #10101)
87+
# but no v26 release carries it yet, so apply it to the installed copy.
88+
# The script is a no-op once the installed version has the fix.
89+
run: node tooling/scripts/patch-electron-builder-keychain.mjs
90+
8091
- name: Verify mac signing secrets
8192
if: matrix.name == 'macOS'
8293
shell: bash
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Carry electron-builder's macOS keychain fix until a v26 release ships it.
4+
*
5+
* app-builder-lib 26.15 and 26.16 create a temporary signing keychain with a
6+
* random password, then hand `security set-key-partition-list -k` the
7+
* certificate's import password instead. Older macOS ignored the mismatch on
8+
* an already-unlocked keychain; the macOS 26.6 runner image verifies it and
9+
* signing dies with "SecKeychainUnlock: The user name or passphrase you
10+
* entered is not correct" (electron-builder #10066). The fix (#10101) is
11+
* merged upstream and backported to v26 but not published, so the release
12+
* workflow applies the same two-line change to the installed copy. The script
13+
* is idempotent and exits 0 without touching a copy that already carries the
14+
* fix, so it can stay in the workflow past the upgrade; it fails loudly if the
15+
* file no longer looks like either version, which is the cue to drop it.
16+
*
17+
* Usage: node tooling/scripts/patch-electron-builder-keychain.mjs [path/to/macCodeSign.js]
18+
*/
19+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
20+
import path from 'node:path'
21+
import { fileURLToPath } from 'node:url'
22+
23+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
24+
const candidates = [
25+
path.join(repoRoot, 'apps', 'desktop', 'node_modules', 'app-builder-lib', 'out', 'codeSign', 'macCodeSign.js'),
26+
path.join(repoRoot, 'node_modules', 'app-builder-lib', 'out', 'codeSign', 'macCodeSign.js')
27+
]
28+
const target = process.argv[2] ?? candidates.find((candidate) => existsSync(candidate))
29+
if (!target || !existsSync(target)) {
30+
console.error('patch-electron-builder-keychain: app-builder-lib macCodeSign.js not found')
31+
process.exit(1)
32+
}
33+
34+
const source = readFileSync(target, 'utf8')
35+
const fixedCall = 'return await importCerts(keychainFile, certPaths, cscPasswords, keychainPassword);'
36+
if (source.includes(fixedCall)) {
37+
console.log(`patch-electron-builder-keychain: already fixed, nothing to do (${target})`)
38+
process.exit(0)
39+
}
40+
41+
const replacements = [
42+
['return await importCerts(keychainFile, certPaths, cscPasswords);', fixedCall],
43+
[
44+
'async function importCerts(keychainFile, paths, keyPasswords) {',
45+
'async function importCerts(keychainFile, paths, keyPasswords, keychainPassword) {'
46+
],
47+
[
48+
'["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]',
49+
'["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]'
50+
]
51+
]
52+
let patched = source
53+
for (const [from, to] of replacements) {
54+
const occurrences = patched.split(from).length - 1
55+
if (occurrences !== 1) {
56+
console.error(
57+
`patch-electron-builder-keychain: expected exactly one occurrence of ${JSON.stringify(from)}, found ${occurrences}; the installed app-builder-lib no longer matches the known bug, review and drop this script`
58+
)
59+
process.exit(1)
60+
}
61+
patched = patched.replace(from, to)
62+
}
63+
writeFileSync(target, patched)
64+
console.log(`patch-electron-builder-keychain: patched ${target}`)

0 commit comments

Comments
 (0)