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
66 changes: 65 additions & 1 deletion apps/desktop/scripts/macos-signing.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
Expand Down Expand Up @@ -125,3 +126,66 @@ test("macOS release policy is fail-closed and Foundation-scoped", async () => {
assert.match(verifier, /FWJYW4S8P8/);
assert.match(workflow, /Verify signed macOS release assets/);
});

test(
"server verification accepts long tool output without SIGPIPE",
{ skip: process.platform === "win32" },
async () => {
const scratch = await mkdtemp(path.join(os.tmpdir(), "clickclack-verifier-test-"));
try {
const bin = path.join(scratch, "bin");
const payload = path.join(scratch, "payload");
const release = path.join(scratch, "release");
await Promise.all([bin, payload, release].map((directory) => mkdir(directory)));
await writeFile(
path.join(payload, "clickclack"),
"#!/bin/sh\necho 'clickclack 9.9.9 (synthetic)'\n",
{ mode: 0o755 },
);
await writeFile(
path.join(bin, "lipo"),
'#!/bin/sh\ncase "$2" in */aarch64/*) echo arm64;; *) echo x86_64;; esac\n',
{ mode: 0o755 },
);
await writeFile(
path.join(bin, "codesign"),
'#!/usr/bin/env python3\nimport sys\nif "-dvvv" in sys.argv:\n print("CodeDirectory v=20400 flags=0x10000(runtime)")\n print("padding\\n" * 200000)\n',
{ mode: 0o755 },
);
await writeFile(
path.join(bin, "otool"),
'#!/usr/bin/env python3\nprint("cmd LC_BUILD_VERSION\\nminos 13.0")\nprint("padding\\n" * 200000)\n',
{ mode: 0o755 },
);
const checksums = [];
for (const arch of ["amd64", "aarch64"]) {
const name = `clickclack_9.9.9_darwin_${arch}.tar.gz`;
const archive = path.join(release, name);
execFileSync("tar", ["-czf", archive, "-C", payload, "clickclack"]);
checksums.push(
`${createHash("sha256")
.update(await readFile(archive))
.digest("hex")} ${name}`,
);
}
await writeFile(
path.join(release, "ClickClack-9.9.9-mac-server-SHA256SUMS.txt"),
`${checksums.join("\n")}\n`,
);
const output = execFileSync(
"bash",
[path.join(root, "scripts", "verify-macos-server-release.sh"), "v9.9.9", release],
{
env: { PATH: `${bin}${path.delimiter}${process.env.PATH}`, TMPDIR: scratch },
encoding: "utf8",
},
);
assert.match(
output,
/verified signed and notarized ClickClack 9\.9\.9 macOS server archives/,
);
} finally {
await rm(scratch, { recursive: true, force: true });
}
},
);
6 changes: 4 additions & 2 deletions apps/desktop/scripts/verify-macos-server-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ PY
test "$(lipo -archs "$binary")" = "$expected_arch"
codesign --verify --strict -R="$requirement" --verbose=2 "$binary"
codesign --verify --strict --check-notarization -R=notarized --verbose=2 "$binary"
codesign -dvvv "$binary" 2>&1 | grep -Eq '^CodeDirectory .*flags=.*\([^)]*runtime[^)]*\)'
minos=$(otool -l "$binary" | awk '/LC_BUILD_VERSION/{build=1;next} build && /minos/{print $2;exit}')
signature=$(codesign -dvvv "$binary" 2>&1)
grep -Eq '^CodeDirectory .*flags=.*\([^)]*runtime[^)]*\)' <<<"$signature"
load_commands=$(otool -l "$binary")
minos=$(awk '/LC_BUILD_VERSION/{build=1;next} build && /minos/{print $2;exit}' <<<"$load_commands")
test "$minos" = 13.0
if [[ "$(uname -m)" = "$expected_arch" ]]; then
"$binary" version | grep -F "$version"
Expand Down
15 changes: 15 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,18 @@ attached and the draft is published.
GoReleaser reuses the existing draft and replaces matching server assets, while
the desktop uploader replaces matching Windows and Linux assets. This makes a
failed draft release safe to retry without making published releases mutable.

If local packaging produced all archives but a verifier defect stopped completion,
preserve those candidates and fix the verifier through normal review and CI on
protected `main`. Verify the preserved artifacts with that trusted checkout,
then finish the desktop checksum manifest before creating the draft:

```sh
"$VERIFY_TREE/apps/desktop/scripts/verify-macos-release.sh" v0.7.0 "$RELEASE_DIR"
node "$VERIFY_TREE/apps/desktop/scripts/release-artifacts.mjs" mac 0.7.0 "$RELEASE_DIR"
```

`VERIFY_TREE` is the clean, reviewed `main` checkout and `RELEASE_DIR` contains the
artifacts already built from the signed tag. Every signature, notarization, version,
and architecture check must pass. The signed tag and built code remain unchanged;
the workflow independently repeats verification from protected `main`.
Loading