diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 26be12291b..bdbeb9f139 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -135,6 +135,32 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Backport electron-builder#10172 to the pinned build tool only. + # Remove this step when app-builder-lib is upgraded to a release containing it. + - name: Repair temporary keychain password in signing tool + shell: bash + run: | + node <<'KEYCHAIN_FIX' + const fs = require('node:fs'); + const path = require('node:path'); + const manifestPath = require.resolve('app-builder-lib/package.json'); + if (JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version !== '26.8.1') { + throw new Error('Reassess/remove the signing backport for the new app-builder-lib version'); + } + const sourcePath = path.join(path.dirname(manifestPath), 'out/codeSign/macCodeSign.js'); + let source = fs.readFileSync(sourcePath, 'utf8'); + const replacements = [ + ['importCerts(keychainFile, certPaths, cscPasswords)', 'importCerts(keychainFile, certPaths, cscPasswords, keychainPassword)'], + ['importCerts(keychainFile, paths, keyPasswords)', 'importCerts(keychainFile, paths, keyPasswords, keychainPassword)'], + ['["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]', '["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]'], + ]; + for (const [before, after] of replacements) { + if (source.split(before).length !== 2) throw new Error('Unexpected signing tool source; refusing to patch'); + source = source.replace(before, after); + } + fs.writeFileSync(sourcePath, source); + KEYCHAIN_FIX + - name: Set desktop package version from tag shell: bash run: | diff --git a/scripts/ci-workflow.test.mjs b/scripts/ci-workflow.test.mjs index 73a9003312..381f826540 100644 --- a/scripts/ci-workflow.test.mjs +++ b/scripts/ci-workflow.test.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { readFileSync, readdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { readFileSync, readdirSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { spawnSync } from "node:child_process"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -679,3 +679,53 @@ test("macOS jobs use latest runners while preserving release architectures", () // GitHub has no standard macos-latest-intel alias; use the newest Intel image. assert.match(release, /runner: macos-26-intel\n\s+electron_arch: x64/); }); + +test("macOS signing repair keeps certificate and keychain passwords separate", () => { + const workflow = readFileSync(new URL(".github/workflows/desktop-release.yml", repoRoot), "utf8"); + const match = workflow.match(/<<'KEYCHAIN_FIX'\n([\s\S]*?)\n KEYCHAIN_FIX/); + assert.ok(match, "the release workflow repairs the pinned signing tool"); + const script = match[1].replace(/^ /gm, ""); + const original = [ + "return await importCerts(keychainFile, certPaths, cscPasswords);", + "async function importCerts(keychainFile, paths, keyPasswords) {", + '["import", paths[i], "-P", password]', + '["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]', + "}", + ].join("\n"); + const directory = mkdtempSync(join(tmpdir(), "signing-repair-")); + try { + const packageDirectory = join(directory, "node_modules/app-builder-lib"); + mkdirSync(join(packageDirectory, "out/codeSign"), { recursive: true }); + const manifest = join(packageDirectory, "package.json"); + const sourcePath = join(packageDirectory, "out/codeSign/macCodeSign.js"); + for (const [version, source, succeeds] of [ + ["26.8.1", original, true], + ["26.8.1", original.replace("cscPasswords);", "differentArguments);"), false], + ["26.18.0", original, false], + ]) { + writeFileSync(manifest, JSON.stringify({ name: "app-builder-lib", version })); + writeFileSync(sourcePath, source); + const result = spawnSync(process.execPath, ["-e", script], { + cwd: directory, + encoding: "utf8", + }); + const actual = readFileSync(sourcePath, "utf8"); + if (succeeds) { + assert.equal(result.status, 0, result.stderr); + assert.ok( + actual.includes("importCerts(keychainFile, certPaths, cscPasswords, keychainPassword)"), + ); + assert.ok( + actual.includes("importCerts(keychainFile, paths, keyPasswords, keychainPassword)"), + ); + assert.ok(actual.includes('["import", paths[i], "-P", password]')); + assert.ok(actual.includes('"-k", keychainPassword, keychainFile]')); + } else { + assert.notEqual(result.status, 0); + assert.equal(actual, source, "unexpected tool inputs must fail without a partial write"); + } + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } +});