Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- Personal sidebar channel order now follows your account across browsers and devices, with the existing local cache retained for offline use. Thanks @isaiahknight-va.
- Signed and notarized macOS server archives with the Foundation Developer ID, alongside the desktop installers, and verify their notarization tickets on a clean runner before publication.
- Allow macOS release automation to select its credential keychain explicitly on headless hosts.
- Updated SQLite, virtual scrolling, Wrangler, and Oxc tooling while preserving runtime minimums and the 48-hour dependency release-age gate.
- Updated DOMPurify, Vite, Electron 43, desktop packaging, Wrangler, Node.js types, and pnpm while preserving runtime minimums and the 48-hour dependency release-age gate; Docker builds now include every workspace manifest for frozen-lockfile validation.
- Corrected the source-install pnpm version and quickstart caller-authentication count to match the current toolchain and authentication guide. Thanks @KrasimirKralev.
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/scripts/after-sign.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async function verifyApp(script, appPath, requireNotarized = false) {

export async function notarizeArchive(submission, options = {}) {
const profile = options.profile || process.env.NOTARYTOOL_KEYCHAIN_PROFILE;
const keychainPath = options.keychainPath || process.env.NOTARYTOOL_KEYCHAIN_PATH;
if (!profile) throw new Error("official macOS packaging requires NOTARYTOOL_KEYCHAIN_PROFILE");
const delay =
options.delay ||
Expand All @@ -74,6 +75,7 @@ export async function notarizeArchive(submission, options = {}) {
submission,
"--keychain-profile",
profile,
...(keychainPath ? ["--keychain", keychainPath] : []),
"--no-s3-acceleration",
"--wait",
"--output-format",
Expand Down Expand Up @@ -102,7 +104,11 @@ export async function notarizeApp(appPath, options = {}) {
await verifyApp(script, appPath);
await run("ditto", ["-c", "-k", "--sequesterRsrc", "--keepParent", appPath, submission]);

await notarizeArchive(submission, { profile, delay: options.delay });
await notarizeArchive(submission, {
profile,
delay: options.delay,
keychainPath: options.keychainPath,
});

await run("xcrun", ["stapler", "staple", appPath]);
await verifyApp(script, appPath, true);
Expand Down
43 changes: 42 additions & 1 deletion apps/desktop/scripts/macos-signing.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { execFileSync } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -63,6 +65,45 @@ test("notarization accepts only Apple's accepted response shape", () => {
);
});

test(
"notarization forwards a credential-keychain path as one argument",
{ skip: process.platform === "win32" },
async () => {
const scratch = await mkdtemp(path.join(os.tmpdir(), "clickclack-notary-test-"));
try {
const argsFile = path.join(scratch, "args.txt");
await writeFile(
path.join(scratch, "xcrun"),
`#!/bin/sh\nprintf '%s\\n' "$@" > "$NOTARY_TEST_ARGS"\nprintf '%s\\n' '${accepted}'\n`,
{ mode: 0o755 },
);
const keychainPath = path.join(scratch, "release credentials.keychain-db");
const moduleURL = new URL("./after-sign.mjs", import.meta.url).href;
execFileSync(
process.execPath,
[
"--input-type=module",
"-e",
`import { notarizeArchive } from ${JSON.stringify(moduleURL)}; await notarizeArchive("synthetic.zip");`,
],
{
env: {
PATH: scratch,
NOTARY_TEST_ARGS: argsFile,
NOTARYTOOL_KEYCHAIN_PROFILE: "synthetic-profile",
NOTARYTOOL_KEYCHAIN_PATH: keychainPath,
},
},
);
const args = (await readFile(argsFile, "utf8")).trim().split("\n");
assert.equal(args[args.indexOf("--keychain") + 1], keychainPath);
assert.equal(args[args.indexOf("--keychain-profile") + 1], "synthetic-profile");
} finally {
await rm(scratch, { recursive: true, force: true });
}
},
);

test("macOS release policy is fail-closed and Foundation-scoped", async () => {
const [config, packageScript, verifier, workflow] = await Promise.all([
readFile(path.join(root, "electron-builder.yml"), "utf8"),
Expand Down
5 changes: 3 additions & 2 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ is published only after all of those jobs pass.
## Build the macOS release candidates

Create and verify the signed tag, then check it out in a clean repository. The
notary profile must already be stored in the login keychain; its credentials do
not belong in the repository.
notary profile must already be stored in an accessible keychain; its credentials
do not belong in the repository. Set `NOTARYTOOL_KEYCHAIN_PATH` to select a managed
release keychain explicitly on a headless host with a locked login keychain.

```sh
git tag -s v0.7.0 -m "Release v0.7.0"
Expand Down
Loading