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
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
| sudo tar -xz -C /usr/local/bin gitleaks
gitleaks version

- name: Ensure zip (per-platform binary archives)
- name: Ensure zip and tar (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip

- run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -114,7 +114,7 @@ jobs:
| sudo tar -xz -C /usr/local/bin gitleaks
gitleaks version

- name: Ensure zip (per-platform binary archives)
- name: Ensure zip and tar (per-platform binary archives)
run: sudo apt-get update && sudo apt-get install -y zip

- run: pnpm install --frozen-lockfile
Expand Down
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ bl --version
which bl # Windows: where.exe bl
```

> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。
> CDN / GitHub Release 未就绪或下载失败时,若本机已有合格 Node,回退到上方 npm 安装。Unix 二进制安装优先解 `.tar.gz`(不依赖 `unzip`);Windows 仍使用 `.zip`。

---

Expand Down
4 changes: 2 additions & 2 deletions docs/agents/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` /
### channel 发布

1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名:
- **`bailian-cli`**:npm 发到该 tag;二进制同时刷新 CDN `sync-release.json`(与 tag 名无关)。本机验证:`BAILIAN_CHANNEL=sync-release`
- **`bailian-cli`**:npm 发到该 tag;二进制同时刷新 CDN `sync-release.json`(与 tag 名无关)。本机验证:`BAILIAN_CHANNEL=sync-release`。**先发二进制(zip+tar.gz 上齐)再发静态仓 `install.sh`**,避免新脚本去拉还不存在的 `.tar.gz`。
- **`knowledge-studio-cli`**:仅 npm(自动跳过 binary,不碰 `sync-release.json`)
2. CI 自动:生成 `0.0.0-beta-<sha7>-<YYYYMMDDHHMM>`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json
3. 对应脚本:`tools/release/publish-channel.mjs`
Expand All @@ -81,7 +81,7 @@ workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` /
1. 确保当前 release tooling 覆盖的包(`tools/release/lib/packages.mjs`)已升到目标版本且一致;当前基础集合为 `packages/core` / `packages/runtime` / `packages/commands` / `packages/cli`,`knowledge-studio-cli` 发布会额外包含 `packages/kscli`
2. 在 GitHub 触发 Publish workflow,package 选目标包集合,mode 选 `stable`
3. 需要 production environment 审批人批准
4. CI 自动:自检 → **npm 发到 latest** → **推送 git tag `v<ver>`** → **Bun 编二进制并创建/更新 GitHub Release** →(bailian-cli)维护 CDN **`manifest.json`** → 完成
4. CI 自动:自检 → **npm 发到 latest** → **推送 git tag `v<ver>`** → **Bun 编二进制并创建/更新 GitHub Release**(每平台 `.zip`,darwin/linux 额外 `.tar.gz`)→(bailian-cli)维护 CDN **`manifest.json`**(unix 资产含 `tar` / `tarSha256`,`file` 仍为 zip)→ 完成
5. 如果所选发布集合的当前版本已全部存在于 npm,stable 发布会失败并提示先升级版本号;如果只有部分包已发布,CI 会继续补发缺失包
6. 对应脚本:`tools/release/publish-stable.mjs`

Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/install/cdn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* FC-presigned URLs and holds no OSS credentials itself.
*
* Layout under the base:
* v<version>/<asset>.zip —— immutable per-version binaries + SHA256SUMS
* v<version>/<asset>.zip —— per-version zip (Windows installer, `bl update`, old Unix install.sh)
* v<version>/<asset>.tar.gz —— unix install.sh default (darwin / linux); same inner binary
* SHA256SUMS —— checksums for zip and tar.gz
* manifest.json —— stable install/update pointer (rolling-manifest shape)
* latest.json —— stable alias; same body as manifest.json
* sync-release.json —— official channel/verify rolling pointer (all bailian-cli
Expand Down Expand Up @@ -97,6 +99,15 @@ export function binaryAssetFileName(
return `bl-${version}-${os}-${arch}.zip`;
}

/**
* Unix install.sh archive: `bl-<ver>-<os>-<arch>.tar.gz`.
* Windows has no tar.gz (PowerShell Expand-Archive uses zip).
*/
export function binaryTarFileName(version: string, os: string, arch: string): string | undefined {
if (os === "windows") return undefined;
return `bl-${version}-${os}-${arch}.tar.gz`;
}

/** Uncompressed binary name inside the zip. */
export function binaryInnerFileName(
version: string,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/install/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
DEFAULT_INSTALL_SCRIPT_URL,
GITHUB_RELEASES_BASE,
binaryAssetFileName,
binaryTarFileName,
binaryInnerFileName,
channelManifestUrl,
detectBinaryPlatform,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/tests/install-method.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getUpdateInstallMethod,
isCompiledBinary,
binaryAssetFileName,
binaryTarFileName,
binaryInnerFileName,
writeInstallMethodSync,
} from "../src/install/index.ts";
Expand Down Expand Up @@ -103,6 +104,12 @@ test("binaryAssetFileName uses per-platform zip", () => {
expect(binaryAssetFileName("1.2.3", "darwin", "arm64", false)).toBe("bl-1.2.3-darwin-arm64.zip");
});

test("binaryTarFileName is unix-only", () => {
expect(binaryTarFileName("1.2.3", "darwin", "arm64")).toBe("bl-1.2.3-darwin-arm64.tar.gz");
expect(binaryTarFileName("1.2.3", "linux", "x64")).toBe("bl-1.2.3-linux-x64.tar.gz");
expect(binaryTarFileName("1.2.3", "windows", "x64")).toBeUndefined();
});

test("binaryInnerFileName keeps exe suffix inside zip", () => {
expect(binaryInnerFileName("1.2.3", "windows", "x64", true)).toBe("bl-1.2.3-windows-x64.exe");
expect(binaryInnerFileName("1.2.3", "darwin", "arm64", false)).toBe("bl-1.2.3-darwin-arm64");
Expand Down
2 changes: 2 additions & 0 deletions skills/bailian-protocol/assets/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash

Windows PowerShell: `irm https://bailian.aliyun.com/cli/install.ps1 | iex`

Unix `install.sh` prefers the `.tar.gz` asset and does not require `unzip`. Windows keeps `.zip`.

### Skills

- **Supported:** `bl skill init` (installs every `bailian-*`, including `bailian-protocol`)
Expand Down
59 changes: 59 additions & 0 deletions tools/release/lib/binary-archive.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { describe, expect, test } from "vite-plus/test";
import {
BINARY_TARGETS,
binaryTarAssetName,
matrixAssetNames,
unixTarTarget,
} from "./binary-build.mjs";
import { tarOne } from "./binary-tar.mjs";

describe("binary archive matrix", () => {
test("unix targets ship zip and tar.gz; windows is zip-only", () => {
const names = matrixAssetNames("1.2.3");
expect(names).toEqual([
"bl-1.2.3-darwin-arm64.zip",
"bl-1.2.3-darwin-arm64.tar.gz",
"bl-1.2.3-darwin-x64.zip",
"bl-1.2.3-darwin-x64.tar.gz",
"bl-1.2.3-linux-x64.zip",
"bl-1.2.3-linux-x64.tar.gz",
"bl-1.2.3-windows-x64.zip",
]);
expect(BINARY_TARGETS.filter((target) => unixTarTarget(target))).toHaveLength(3);
expect(binaryTarAssetName("1.2.3", { os: "linux", arch: "x64" })).toBe(
"bl-1.2.3-linux-x64.tar.gz",
);
});
});

describe("tarOne", () => {
test("tar.gz stores basename only and round-trips bytes", () => {
const outdir = mkdtempSync(join(tmpdir(), "bl-bin-tar-"));
try {
const innerName = "bl-9.9.9-linux-x64";
const innerPath = join(outdir, innerName);
writeFileSync(innerPath, "fake-binary-payload\n");
chmodSync(innerPath, 0o755);
const packed = tarOne(
{ innerName, innerPath, os: "linux", arch: "x64" },
{ outdir, tarFileName: "bl-9.9.9-linux-x64.tar.gz" },
);
expect(packed.fileName).toBe("bl-9.9.9-linux-x64.tar.gz");
expect(packed.sha256).toMatch(/^[a-f0-9]{64}$/);

const extractDir = join(outdir, "out");
mkdirSync(extractDir);
const extract = spawnSync("tar", ["-C", extractDir, "-xzf", packed.outfile, innerName], {
encoding: "utf-8",
});
expect(extract.status).toBe(0);
expect(readFileSync(join(extractDir, innerName), "utf-8")).toBe("fake-binary-payload\n");
} finally {
rmSync(outdir, { recursive: true, force: true });
}
});
});
77 changes: 62 additions & 15 deletions tools/release/lib/binary-build.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Build standalone `bl` binaries with Bun --compile, then pack each as a
* per-platform `.zip` via binary-zip.mjs (Release / OSS download asset).
* per-platform `.zip` (all OS) plus `.tar.gz` on darwin/linux (Release / OSS).
*
* Used by lib/binary-release.mjs (and publish-stable / publish-channel orchestrators).
* Debug:
Expand All @@ -21,6 +21,7 @@ import {
rollingManifestChannelId,
rollingManifestFileName,
} from "./binary-options.mjs";
import { ensureTar, tarOne } from "./binary-tar.mjs";
import { ensureZip, zipOne } from "./binary-zip.mjs";

const BINARY_COMPILE = fileURLToPath(new URL("./binary-compile.mjs", import.meta.url));
Expand All @@ -42,14 +43,27 @@ export function binaryInnerName(version, { os, arch, exe }) {
return `bl-${version}-${os}-${arch}${exe ? ".exe" : ""}`;
}

/** Release asset basename: `bl-<ver>-<os>-<arch>.zip`. */
/** Release zip basename: `bl-<ver>-<os>-<arch>.zip` (install.ps1 / bl update / old install.sh). */
export function binaryAssetName(version, { os, arch }) {
return `bl-${version}-${os}-${arch}.zip`;
}

/** Full matrix zip basenames for a version (order matches BINARY_TARGETS). */
/** Unix install.sh default archive: `bl-<ver>-<os>-<arch>.tar.gz`. */
export function binaryTarAssetName(version, { os, arch }) {
return `bl-${version}-${os}-${arch}.tar.gz`;
}

export function unixTarTarget(target) {
return target.os !== "windows";
}

/** Full matrix: zip for every target, plus tar.gz for darwin/linux. */
export function matrixAssetNames(version) {
return BINARY_TARGETS.map((target) => binaryAssetName(version, target));
return BINARY_TARGETS.flatMap((target) => {
const zipName = binaryAssetName(version, target);
if (!unixTarTarget(target)) return [zipName];
return [zipName, binaryTarAssetName(version, target)];
});
}

function log(message = "") {
Expand Down Expand Up @@ -156,22 +170,32 @@ function compileOne({ bunTarget, os, arch, exe }, version, outdir, entry) {
}

function writeChecksums(outdir, artifacts) {
const lines = artifacts.map((item) => `${item.sha256} ${item.fileName}`);
const lines = [];
for (const item of artifacts) {
lines.push(`${item.sha256} ${item.fileName}`);
if (item.tarFileName && item.tarSha256) {
lines.push(`${item.tarSha256} ${item.tarFileName}`);
}
}
writeFileSync(join(outdir, "SHA256SUMS"), `${lines.join("\n")}\n`);
}

/** Write the rolling channel manifest (`latest.json` / `sync-release.json`) with per-platform zip + sha256. */
/** Write the rolling channel manifest (`latest.json` / `sync-release.json`) with per-platform zip + optional tar.gz. */
function writeChannelManifest(outdir, version, artifacts, mode) {
const channel = rollingManifestChannelId(mode);
const assets = Object.fromEntries(
artifacts.map((item) => [
`${item.os}-${item.arch}`,
{
artifacts.map((item) => {
const asset = {
file: item.fileName,
sha256: item.sha256,
inner: item.innerName,
},
]),
};
if (item.tarFileName && item.tarSha256) {
asset.tar = item.tarFileName;
asset.tarSha256 = item.tarSha256;
}
return [`${item.os}-${item.arch}`, asset];
}),
);
const manifest = {
name: "bailian-cli",
Expand Down Expand Up @@ -203,14 +227,36 @@ function smokeTestHostBinary(compiled, outdir) {
}
}

/** Compile binaries into `outdir`, zip per platform, write checksums (+ channel manifest). */
function packOne(compiled, version, outdir) {
let tarMeta = null;
if (unixTarTarget(compiled)) {
tarMeta = tarOne(compiled, {
outdir,
tarFileName: binaryTarAssetName(version, compiled),
log,
});
}
const zipMeta = zipOne(compiled, {
outdir,
zipFileName: binaryAssetName(version, compiled),
log,
});
return {
...zipMeta,
tarFileName: tarMeta?.fileName,
tarSha256: tarMeta?.sha256,
};
}

/** Compile binaries into `outdir`, zip (+ unix tar.gz) per platform, write checksums (+ channel manifest). */
export function buildBinaryArtifacts(rawOptions = {}) {
const options = normalizeBuildOptions(rawOptions);
const { outdir, mode, channel } = options;
const bunVersion = ensureBun();
ensureZip();
const version = cliVersion();
const targets = resolveTargets(options);
if (targets.some((target) => unixTarTarget(target))) ensureTar();

mkdirSync(outdir, { recursive: true });
log(`bun ${bunVersion}`);
Expand All @@ -220,9 +266,7 @@ export function buildBinaryArtifacts(rawOptions = {}) {

const compiled = targets.map((target) => compileOne(target, version, outdir, CLI_ENTRY));
smokeTestHostBinary(compiled, outdir);
const artifacts = compiled.map((item) =>
zipOne(item, { outdir, zipFileName: binaryAssetName(version, item), log }),
);
const artifacts = compiled.map((item) => packOne(item, version, outdir));
writeChecksums(outdir, artifacts);

const extras = ["SHA256SUMS"];
Expand All @@ -231,6 +275,9 @@ export function buildBinaryArtifacts(rawOptions = {}) {
log(`\nBuilt ${artifacts.length} zip(s):`);
for (const item of artifacts) {
log(` ${item.fileName} ${item.sha256.slice(0, 12)}… (inner ${item.innerName})`);
if (item.tarFileName) {
log(` ${item.tarFileName} ${item.tarSha256.slice(0, 12)}…`);
}
}
log(`Also wrote ${extras.join(", ")}`);
return {
Expand Down
4 changes: 2 additions & 2 deletions tools/release/lib/binary-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
* Publish bailian-cli binary assets to GitHub Releases.
*
* stable: release `v<version>` (tag must already be on origin; --verify-tag)
* assets: bl-*.zip, SHA256SUMS (no latest.json on the GH release)
* assets: bl-*.zip, unix bl-*.tar.gz, SHA256SUMS (no latest.json on the GH release)
* OSS: rewrite release/manifest.json + latest.json from build latest.json
* channel: versioned prerelease `v<betaVersion>` (assets: bl-*.zip, SHA256SUMS)
* channel: versioned prerelease `v<betaVersion>` (assets: bl-*.zip, unix bl-*.tar.gz, SHA256SUMS)
* + rolling prerelease `channel-sync-release` holding only sync-release.json
* OSS: always overwrite prefix-root sync-release.json
*
Expand Down
55 changes: 55 additions & 0 deletions tools/release/lib/binary-tar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Pack a Bun-compiled Unix binary into a per-platform `.tar.gz` Release asset.
* Windows stays zip-only (install.ps1 / Expand-Archive).
*
* Called by binary-build.mjs after compile + smoke test, before zipOne removes
* the inner file. Naming (`bl-<ver>-<os>-<arch>.tar.gz`) stays in binary-build.
*/
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";

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

function sha256File(path) {
return createHash("sha256").update(readFileSync(path)).digest("hex");
}

export function ensureTar() {
const result = spawnSync("tar", ["--help"], { encoding: "utf-8" });
if (result.error?.code === "ENOENT") {
throw new Error("tar not found on PATH. A POSIX tar is required to pack unix .tar.gz assets.");
}
}

/**
* Pack compiled binary into `tarFileName` under `outdir`. Does not delete the inner file.
*
* @param {{ innerName: string, innerPath: string, os: string, arch: string }} compiled
* @param {{ outdir: string, tarFileName: string, log?: (message?: string) => void }} options
*/
export function tarOne(compiled, { outdir, tarFileName, log = defaultLog }) {
const tarPath = join(outdir, tarFileName);
log(`tar ${compiled.innerName} → ${tarFileName}`);

// Store basename only (no directory path). GNU / BSD / BusyBox: tar -czf archive file
const result = spawnSync("tar", ["-czf", tarFileName, compiled.innerName], {
cwd: outdir,
encoding: "utf-8",
});
if (result.status !== 0) {
process.stderr.write(result.stderr || result.stdout || "");
throw new Error(`tar failed for ${compiled.innerName}`);
}
return {
fileName: tarFileName,
outfile: tarPath,
innerName: compiled.innerName,
os: compiled.os,
arch: compiled.arch,
sha256: sha256File(tarPath),
};
}
Loading
Loading