Files
openwork/apps/desktop/scripts/electron-after-sign.cjs

104 lines
3.8 KiB
JavaScript

const { spawnSync } = require("node:child_process");
const { existsSync, mkdtempSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const path = require("node:path");
const computerUseHelperAppName = "OpenWork Computer Use.app";
function run(command, args) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status !== 0) {
throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`);
}
}
async function runWithRetry(command, args, attempts, baseDelayMs = 30_000) {
for (let attempt = 1; ; attempt++) {
const result = spawnSync(command, args, { stdio: "inherit" });
if (result.status === 0) return;
if (attempt >= attempts) {
throw new Error(`${command} ${args.join(" ")} failed with status ${result.status} after ${attempts} attempts`);
}
const delayMs = baseDelayMs * attempt;
console.warn(
`[electron-after-sign] ${command} ${args.join(" ")} failed with status ${result.status}; retrying in ${delayMs / 1000}s (attempt ${attempt}/${attempts}).`,
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required to notarize the Electron macOS app`);
}
return value;
}
function computerUseHelperPath(appPath) {
return path.join(appPath, "Contents", "Resources", "helpers", computerUseHelperAppName);
}
function verifyComputerUseHelper(appPath, requireDistributionSignature) {
const helperPath = computerUseHelperPath(appPath);
if (!existsSync(helperPath)) {
throw new Error(`Computer Use helper app is missing from packaged app: ${helperPath}`);
}
run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", helperPath]);
if (!requireDistributionSignature) return;
const result = spawnSync("codesign", ["--display", "--verbose=4", helperPath], { encoding: "utf8" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`codesign --display failed for Computer Use helper with status ${result.status}`);
}
if (result.stderr.includes("Signature=adhoc")) {
throw new Error("Computer Use helper app is ad-hoc signed; notarized builds require a Developer ID signature.");
}
}
async function afterSign(context) {
if (context.electronPlatformName !== "darwin") return;
if (process.env.MACOS_NOTARIZE !== "true") {
console.warn("[electron-after-sign] MACOS_NOTARIZE is not true; skipping notarization.");
return;
}
const appName = `${context.packager.appInfo.productFilename}.app`;
const appPath = path.join(context.appOutDir, appName);
verifyComputerUseHelper(appPath, process.env.MACOS_NOTARIZE === "true");
const notaryTempDir = mkdtempSync(path.join(tmpdir(), "openwork-electron-notary-"));
const notaryZipPath = path.join(notaryTempDir, `${context.packager.appInfo.productFilename}-notary.zip`);
const keyPath = requireEnv("APPLE_API_KEY_PATH");
const keyId = requireEnv("APPLE_API_KEY");
const issuer = requireEnv("APPLE_API_ISSUER");
try {
run("ditto", ["-c", "-k", "--keepParent", appPath, notaryZipPath]);
run("xcrun", [
"notarytool",
"submit",
notaryZipPath,
"--key",
keyPath,
"--key-id",
keyId,
"--issuer",
issuer,
"--wait",
]);
// Notarization tickets can take minutes to propagate to Apple's CDN after acceptance; stapler can transiently fail with status 65 ("CloudKit query failed").
await runWithRetry("xcrun", ["stapler", "staple", appPath], 5);
run("xcrun", ["stapler", "validate", appPath]);
} finally {
rmSync(notaryTempDir, { recursive: true, force: true });
}
}
module.exports = afterSign;
module.exports.default = afterSign;
module.exports.runWithRetry = runWithRetry;