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
30 changes: 30 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ jobs:
- name: Ensure zip and tar (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip

# bun build --compile leaves an invalid darwin linker signature. rcodesign
# re-signs those Mach-O files on the Linux runner (binary-codesign.mjs).
- name: Install rcodesign (ad-hoc sign darwin binaries)
run: |
set -euo pipefail
RCODESIGN_VERSION=0.29.0
asset="apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl.tar.gz"
curl -fsSL -o "/tmp/${asset}" \
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign/${RCODESIGN_VERSION}/${asset}"
tar -xzf "/tmp/${asset}" -C /tmp
sudo install -m 755 \
"/tmp/apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl/rcodesign" \
/usr/local/bin/rcodesign
rcodesign --version

- run: pnpm install --frozen-lockfile

# Binary compile uses `bun build --compile` CLI (not Bun.build API).
Expand Down Expand Up @@ -117,6 +132,21 @@ jobs:
- name: Ensure zip and tar (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip

# bun build --compile leaves an invalid darwin linker signature. rcodesign
# re-signs those Mach-O files on the Linux runner (binary-codesign.mjs).
- name: Install rcodesign (ad-hoc sign darwin binaries)
run: |
set -euo pipefail
RCODESIGN_VERSION=0.29.0
asset="apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl.tar.gz"
curl -fsSL -o "/tmp/${asset}" \
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign/${RCODESIGN_VERSION}/${asset}"
tar -xzf "/tmp/${asset}" -C /tmp
sudo install -m 755 \
"/tmp/apple-codesign-${RCODESIGN_VERSION}-x86_64-unknown-linux-musl/rcodesign" \
/usr/local/bin/rcodesign
rcodesign --version

- run: pnpm install --frozen-lockfile

# Binary compile uses `bun build --compile` CLI (not Bun.build API).
Expand Down
2 changes: 2 additions & 0 deletions docs/agents/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
- **GitHub Release**:`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`(stable / channel 均需)
- **Node 版本**:24(npm 11.5+ 才支持 OIDC token 交换)
- **Bun**:`oven-sh/setup-bun`,版本钉死在 workflow 中
- **darwin 签名**:编完后 `binary-codesign.mjs` 对 darwin Mach-O 做 ad-hoc 重签,并按 Apple 的方式重算每一页 SHA-256(末页不补零)。对不上就中止发布。macOS runner 额外跑 `codesign --verify --strict`。Linux runner 用 `rcodesign 0.29.0 sign`;不要用 `rcodesign verify`(会拒绝 Apple 已通过的 ad-hoc 签名)。漏签时 Apple Silicon 会在启动时 SIGKILL
- **Actions 版本**:checkout/setup-node/pnpm-action 均为 v6(Node 24 兼容)
- **npm 配置**:当前 release tooling 发布的包(`bailian-cli-core` / `bailian-cli-runtime` / `bailian-cli-commands` / `bailian-cli` / `knowledge-studio-cli`)的 Trusted Publisher 指向 `modelstudioai/cli` 的 `publish.yml`;新增发布包时同步 npm Trusted Publisher

Expand Down Expand Up @@ -160,4 +161,5 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run
| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 |
| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 |
| channel job 缺少 `contents: write` | `gh release create` 失败 |
| darwin 二进制编完后没有 ad-hoc 重签 | Apple Silicon / macOS 27 对失效的 linker 签名直接 SIGKILL(`Killed: 9`) |
| stable 未先推 tag 就建 Release | `--verify-tag` 失败 |
3 changes: 3 additions & 0 deletions tools/release/lib/binary-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
rollingManifestChannelId,
rollingManifestFileName,
} from "./binary-options.mjs";
import { signDarwinAdhoc } from "./binary-codesign.mjs";
import { ensureTar, tarOne } from "./binary-tar.mjs";
import { ensureZip, zipOne } from "./binary-zip.mjs";

Expand Down Expand Up @@ -166,6 +167,8 @@ function compileOne({ bunTarget, os, arch, exe }, version, outdir, entry) {
if (result.stderr) process.stderr.write(result.stderr);
// Bun 1.2.19 writes windows-x64 .exe with mode 000 on Unix hosts (oven-sh/bun#21308).
chmodSync(innerPath, 0o755);
// Sign after chmod. Bun's linker signature does not match the final Mach-O bytes.
if (os === "darwin") signDarwinAdhoc(innerPath, { log });
return { innerName, innerPath, os, arch, exe };
}

Expand Down
162 changes: 162 additions & 0 deletions tools/release/lib/binary-codesign.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Ad-hoc re-sign Bun-compiled darwin Mach-O binaries.
*
* `bun build --compile` leaves a linker signature (`Identifier=a.out`,
* `adhoc,linker-signed`) whose page hashes do not match the final file.
* macOS 27 SIGKILLs that binary at launch (exit 137). Re-signing after
* compile covers the bytes that actually ship.
*
* darwin hosts use `codesign`, then `codesign --verify --strict`.
* Linux/Windows release runners use `rcodesign sign`, then check each
* CodeDirectory page hash in-process. `rcodesign verify` is not a gate:
* 0.29.0 rejects ad-hoc signatures Apple accepts (`CMS error`).
*/
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";

export const RCODESIGN_VERSION = "0.29.0";

function defaultLog(message = "") {
process.stdout.write(`${message}\n`);
}

/** Command used to replace the linker signature. `platform` is the build host, not the target. */
export function adhocSignPlan(platform = process.platform) {
if (platform === "darwin") {
return {
command: "codesign",
args: ["--force", "--sign", "-", "--identifier", "bl"],
};
}
return {
command: "rcodesign",
args: ["sign"],
};
}

const LC_CODE_SIGNATURE = 0x1d;
const CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0;
const CSMAGIC_CODEDIRECTORY = 0xfade0c02;
const CSSLOT_CODEDIRECTORY = 0;
const CDHASH_SHA256 = 2;

/**
* Recompute CodeDirectory page hashes the way Apple does for ad-hoc Mach-O:
* each 4KiB page is SHA-256 of the bytes that exist, with no zero padding.
* Bun's linker signature matches the padded last page and fails this check.
*
* @param {string} binaryPath
*/
export function verifyAdhocPageHashes(binaryPath) {
const data = readFileSync(binaryPath);
const magic = data.readUInt32LE(0);
const littleEndian = magic === 0xfeedfacf;
if (!littleEndian && magic !== 0xcffaedfe) {
throw new Error(`${binaryPath} is not a thin 64-bit Mach-O`);
}
const read32 = littleEndian
? (offset) => data.readUInt32LE(offset)
: (offset) => data.readUInt32BE(offset);
const commandCount = read32(16);
let commandOffset = 32;
let signatureOffset = 0;
for (let commandIndex = 0; commandIndex < commandCount; commandIndex++) {
const command = read32(commandOffset);
const commandSize = read32(commandOffset + 4);
if (command === LC_CODE_SIGNATURE) signatureOffset = read32(commandOffset + 8);
commandOffset += commandSize;
}
if (!signatureOffset) throw new Error(`${binaryPath} has no LC_CODE_SIGNATURE`);
const readBlob32 = (offset) => data.readUInt32BE(signatureOffset + offset);
if (readBlob32(0) !== CSMAGIC_EMBEDDED_SIGNATURE) {
throw new Error(`${binaryPath} code signature superblob is invalid`);
}
const blobCount = readBlob32(8);
let directoryOffset = 0;
for (let blobIndex = 0; blobIndex < blobCount; blobIndex++) {
const blobType = readBlob32(12 + blobIndex * 8);
const blobOffset = readBlob32(12 + blobIndex * 8 + 4);
if (blobType === CSSLOT_CODEDIRECTORY) directoryOffset = blobOffset;
}
if (!directoryOffset) throw new Error(`${binaryPath} has no CodeDirectory`);
const directory = signatureOffset + directoryOffset;
if (data.readUInt32BE(directory) !== CSMAGIC_CODEDIRECTORY) {
throw new Error(`${binaryPath} CodeDirectory magic is invalid`);
}
const version = data.readUInt32BE(directory + 8);
const hashOffset = data.readUInt32BE(directory + 16);
const codeSlotCount = data.readUInt32BE(directory + 28);
let codeLimit = data.readUInt32BE(directory + 32);
const hashSize = data[directory + 36];
const hashType = data[directory + 37];
const pageShift = data[directory + 39];
if (version >= 0x20300) {
const codeLimit64 = data.readBigUInt64BE(directory + 48);
if (codeLimit64 > 0n) codeLimit = Number(codeLimit64);
}
if (hashType !== CDHASH_SHA256 || hashSize !== 32) {
throw new Error(`${binaryPath} code signature is not SHA-256`);
}
if (codeLimit > data.length) {
throw new Error(`${binaryPath} codeLimit ${codeLimit} exceeds file length ${data.length}`);
}
const pageSize = pageShift === 0 ? codeLimit : 1 << pageShift;
for (let slot = 0; slot < codeSlotCount; slot++) {
const start = slot * pageSize;
const end = Math.min(start + pageSize, codeLimit);
const recorded = data.subarray(
directory + hashOffset + slot * hashSize,
directory + hashOffset + (slot + 1) * hashSize,
);
const actual = createHash("sha256").update(data.subarray(start, end)).digest();
if (!recorded.equals(actual)) {
throw new Error(
`${binaryPath} code signature page ${slot} does not match the file (stale ad-hoc signature)`,
);
}
}
}

/**
* Replace the Mach-O ad-hoc signature in place.
* Then recompute page hashes so a stale linker signature fails the build on Linux.
* On darwin, also run `codesign --verify --strict`.
*
* @param {string} binaryPath
* @param {{ log?: (message?: string) => void, platform?: NodeJS.Platform }} [options]
*/
export function signDarwinAdhoc(
binaryPath,
{ log = defaultLog, platform = process.platform } = {},
) {
const plan = adhocSignPlan(platform);
log(`adhoc sign via ${plan.command}`);
const result = spawnSync(plan.command, [...plan.args, binaryPath], { encoding: "utf-8" });
if (result.error?.code === "ENOENT") {
const installHint =
platform === "darwin"
? "codesign ships with the Xcode command line tools."
: `Install rcodesign ${RCODESIGN_VERSION} (apple-codesign) and retry.`;
throw new Error(
`${plan.command} not found on PATH. Darwin binaries need an ad-hoc signature before packaging. ${installHint}`,
);
}
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.status !== 0) {
throw new Error(`ad-hoc sign failed for ${binaryPath}`);
}
verifyAdhocPageHashes(binaryPath);
if (platform !== "darwin") return;

const verify = spawnSync("codesign", ["--verify", "--strict", binaryPath], { encoding: "utf-8" });
if (verify.stdout) process.stdout.write(verify.stdout);
if (verify.stderr) process.stderr.write(verify.stderr);
if (verify.status !== 0) {
const detail = (verify.stderr || verify.stdout || "").trim();
throw new Error(
`codesign --verify --strict failed for ${binaryPath}${detail ? `: ${detail}` : ""}`,
);
}
}
17 changes: 17 additions & 0 deletions tools/release/lib/binary-codesign.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vite-plus/test";
import { adhocSignPlan, RCODESIGN_VERSION } from "./binary-codesign.mjs";

describe("darwin ad-hoc sign plan", () => {
test("darwin hosts use codesign and replace the linker signature", () => {
expect(adhocSignPlan("darwin")).toEqual({
command: "codesign",
args: ["--force", "--sign", "-", "--identifier", "bl"],
});
});

test("non-darwin hosts use rcodesign so Linux CI can sign Mach-O", () => {
expect(RCODESIGN_VERSION).toBe("0.29.0");
expect(adhocSignPlan("linux")).toEqual({ command: "rcodesign", args: ["sign"] });
expect(adhocSignPlan("win32")).toEqual({ command: "rcodesign", args: ["sign"] });
});
});
Loading