From a3c00e5e27abcf33ef8ed1caf5f5a1b664a5376e Mon Sep 17 00:00:00 2001 From: rendianmeng Date: Fri, 18 Sep 2026 13:40:38 +0800 Subject: [PATCH 1/3] fix: ship unix binary tar.gz alongside zip for slim Linux installs Channel/stable publishes keep zip for Windows and bl update, and add tar.gz so install.sh can extract without unzip. Co-authored-by: Cursor --- .github/workflows/publish.yml | 4 +- INSTALL.md | 2 +- docs/agents/publish.md | 4 +- packages/core/src/install/cdn.ts | 13 +++- packages/core/src/install/index.ts | 1 + packages/core/tests/install-method.test.ts | 7 ++ skills/bailian-protocol/assets/setup.md | 2 + tools/release/lib/binary-archive.test.mjs | 59 +++++++++++++++++ tools/release/lib/binary-build.mjs | 77 +++++++++++++++++----- tools/release/lib/binary-release.mjs | 4 +- tools/release/lib/binary-tar.mjs | 55 ++++++++++++++++ tools/release/lib/binary-zip.mjs | 6 +- tools/release/lib/oss-direct-upload.mjs | 1 + 13 files changed, 209 insertions(+), 26 deletions(-) create mode 100644 tools/release/lib/binary-archive.test.mjs create mode 100644 tools/release/lib/binary-tar.mjs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fcb0c6c6b..2f4430e4c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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 @@ -116,7 +116,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 diff --git a/INSTALL.md b/INSTALL.md index 6a8f6c8df..24e2c079e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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`。 --- diff --git a/docs/agents/publish.md b/docs/agents/publish.md index b83f6c647..f0919079c 100644 --- a/docs/agents/publish.md +++ b/docs/agents/publish.md @@ -49,7 +49,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--`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json 3. 对应脚本:`tools/release/publish-channel.mjs` @@ -59,7 +59,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`** → **Bun 编二进制并创建/更新 GitHub Release** →(bailian-cli)维护 CDN **`manifest.json`** → 完成 +4. CI 自动:自检 → **npm 发到 latest** → **推送 git tag `v`** → **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` diff --git a/packages/core/src/install/cdn.ts b/packages/core/src/install/cdn.ts index 491b16879..71a3aee4d 100644 --- a/packages/core/src/install/cdn.ts +++ b/packages/core/src/install/cdn.ts @@ -4,7 +4,9 @@ * no external FC is involved. * * Layout under the base: - * v/.zip —— immutable per-version binaries + SHA256SUMS + * v/.zip —— per-version zip (Windows installer, `bl update`, old Unix install.sh) + * v/.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 @@ -96,6 +98,15 @@ export function binaryAssetFileName( return `bl-${version}-${os}-${arch}.zip`; } +/** + * Unix install.sh archive: `bl---.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, diff --git a/packages/core/src/install/index.ts b/packages/core/src/install/index.ts index db33309a2..9e9d8ea87 100644 --- a/packages/core/src/install/index.ts +++ b/packages/core/src/install/index.ts @@ -14,6 +14,7 @@ export { DEFAULT_INSTALL_SCRIPT_URL, GITHUB_RELEASES_BASE, binaryAssetFileName, + binaryTarFileName, binaryInnerFileName, channelManifestUrl, detectBinaryPlatform, diff --git a/packages/core/tests/install-method.test.ts b/packages/core/tests/install-method.test.ts index 559d07b8f..731f07fe8 100644 --- a/packages/core/tests/install-method.test.ts +++ b/packages/core/tests/install-method.test.ts @@ -9,6 +9,7 @@ import { getUpdateInstallMethod, isCompiledBinary, binaryAssetFileName, + binaryTarFileName, binaryInnerFileName, writeInstallMethodSync, } from "../src/install/index.ts"; @@ -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"); diff --git a/skills/bailian-protocol/assets/setup.md b/skills/bailian-protocol/assets/setup.md index 8c97458e5..8ea2694c4 100644 --- a/skills/bailian-protocol/assets/setup.md +++ b/skills/bailian-protocol/assets/setup.md @@ -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`) diff --git a/tools/release/lib/binary-archive.test.mjs b/tools/release/lib/binary-archive.test.mjs new file mode 100644 index 000000000..40226d884 --- /dev/null +++ b/tools/release/lib/binary-archive.test.mjs @@ -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 }); + } + }); +}); diff --git a/tools/release/lib/binary-build.mjs b/tools/release/lib/binary-build.mjs index ea0108c83..6e9b1247e 100644 --- a/tools/release/lib/binary-build.mjs +++ b/tools/release/lib/binary-build.mjs @@ -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: @@ -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)); @@ -42,14 +43,27 @@ export function binaryInnerName(version, { os, arch, exe }) { return `bl-${version}-${os}-${arch}${exe ? ".exe" : ""}`; } -/** Release asset basename: `bl---.zip`. */ +/** Release zip basename: `bl---.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---.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 = "") { @@ -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", @@ -203,7 +227,28 @@ 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; @@ -211,6 +256,7 @@ export function buildBinaryArtifacts(rawOptions = {}) { ensureZip(); const version = cliVersion(); const targets = resolveTargets(options); + if (targets.some((target) => unixTarTarget(target))) ensureTar(); mkdirSync(outdir, { recursive: true }); log(`bun ${bunVersion}`); @@ -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"]; @@ -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 { diff --git a/tools/release/lib/binary-release.mjs b/tools/release/lib/binary-release.mjs index 0a8e0b8a4..2d50fba38 100644 --- a/tools/release/lib/binary-release.mjs +++ b/tools/release/lib/binary-release.mjs @@ -2,9 +2,9 @@ * Publish bailian-cli binary assets to GitHub Releases. * * stable: release `v` (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` (assets: bl-*.zip, SHA256SUMS) + * channel: versioned prerelease `v` (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 * diff --git a/tools/release/lib/binary-tar.mjs b/tools/release/lib/binary-tar.mjs new file mode 100644 index 000000000..7acb86c07 --- /dev/null +++ b/tools/release/lib/binary-tar.mjs @@ -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---.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), + }; +} diff --git a/tools/release/lib/binary-zip.mjs b/tools/release/lib/binary-zip.mjs index d054ba847..4fde68cdc 100644 --- a/tools/release/lib/binary-zip.mjs +++ b/tools/release/lib/binary-zip.mjs @@ -28,9 +28,9 @@ export function ensureZip() { * Pack compiled binary into `zipFileName` under `outdir` and remove the raw file. * * @param {{ innerName: string, innerPath: string, os: string, arch: string }} compiled - * @param {{ outdir: string, zipFileName: string, log?: (message?: string) => void }} options + * @param {{ outdir: string, zipFileName: string, log?: (message?: string) => void, removeInner?: boolean }} options */ -export function zipOne(compiled, { outdir, zipFileName, log = defaultLog }) { +export function zipOne(compiled, { outdir, zipFileName, log = defaultLog, removeInner = true }) { const zipPath = join(outdir, zipFileName); log(`zip ${compiled.innerName} → ${zipFileName}`); @@ -43,7 +43,7 @@ export function zipOne(compiled, { outdir, zipFileName, log = defaultLog }) { process.stderr.write(result.stderr || result.stdout || ""); throw new Error(`zip failed for ${compiled.innerName}`); } - unlinkSync(compiled.innerPath); + if (removeInner) unlinkSync(compiled.innerPath); return { fileName: zipFileName, outfile: zipPath, diff --git a/tools/release/lib/oss-direct-upload.mjs b/tools/release/lib/oss-direct-upload.mjs index 6e3caf9e0..6467f60a4 100644 --- a/tools/release/lib/oss-direct-upload.mjs +++ b/tools/release/lib/oss-direct-upload.mjs @@ -73,6 +73,7 @@ function ossHost(cfg) { function contentTypeFor(name) { if (name.endsWith(".md")) return "text/markdown; charset=utf-8"; + if (name.endsWith(".tar.gz") || name.endsWith(".tgz")) return "application/gzip"; if (name.endsWith(".zip")) return "application/zip"; if (name.endsWith(".json")) return "application/json"; return "application/octet-stream"; From 8c3ae9a167e423a690bd9e59d390c80d2291bd67 Mon Sep 17 00:00:00 2001 From: rendianmeng Date: Fri, 18 Sep 2026 14:14:08 +0800 Subject: [PATCH 2/3] fix: inject production OSS secrets into channel publishes Channel jobs skipped OSS when BAILIAN_OSS_* lived only on the production environment, so sync-release.json stayed stale. Co-authored-by: Cursor --- .github/workflows/publish.yml | 3 ++ docs/agents/publish.md | 42 +++++++++++++++------------- tools/release/lib/binary-release.mjs | 7 +++++ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f4430e4c..993904470 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -87,6 +87,9 @@ jobs: if: inputs.mode == 'channel' && inputs.package != 'bailian-kb-dsh' name: publish channel (${{ inputs.package }}) to npm + binary runs-on: ubuntu-latest + # Same environment as stable so BAILIAN_OSS_* (environment secrets) inject. + # Without this, channel silently skips OSS and install.sh keeps the old CDN pointer. + environment: production permissions: contents: write # create prerelease GitHub Release with binary assets id-token: write # OIDC for npm Trusted Publishing + provenance diff --git a/docs/agents/publish.md b/docs/agents/publish.md index f0919079c..3ae4f863b 100644 --- a/docs/agents/publish.md +++ b/docs/agents/publish.md @@ -30,10 +30,10 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口 两种模式: -| 模式 | 用途 | 触发方式 | -| ------- | --------------------------------------------------------------------------------------- | -------------------------------------------- | -| channel | npm dist-tag +(仅 bailian-cli)二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channel,channel 填 **npm dist-tag** 名 | -| stable | npm latest + GitHub Release `v` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 | +| 模式 | 用途 | 触发方式 | +| ------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| channel | npm dist-tag +(仅 bailian-cli)二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channel,channel 填 **npm dist-tag** 名;**与 stable 同样走 production environment**(OSS 密钥) | +| stable | npm latest + GitHub Release `v` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 | 可选 flag:`--skip-binary`(仅发 npm,紧急逃生)。 @@ -48,10 +48,10 @@ workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` / ### channel 发布 -1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名: +1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名。`publish-channel` 与 stable 一样走 **production** environment(OSS 密钥;若该 environment 开了 Required Reviewers,测试包也要审批): - **`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--`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json +2. CI 自动:生成 `0.0.0-beta--`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 OSS `sync-release.json`** → 还原 package.json。OSS 密钥未注入时 channel 失败,不会只更新 GitHub。 3. 对应脚本:`tools/release/publish-channel.mjs` ### stable 发布 @@ -90,6 +90,7 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run - **认证**:npm OIDC Trusted Publishing(无 token),需要 `id-token: write` 权限 - **GitHub Release**:`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`(stable / channel 均需) +- **OSS**:`BAILIAN_OSS_*` 配在 **production** environment;`publish-stable` 与 `publish-channel` 都挂该 environment,否则 channel 拿不到密钥、公网 `sync-release.json` 不滚动 - **Node 版本**:24(npm 11.5+ 才支持 OIDC token 交换) - **Bun**:`oven-sh/setup-bun`,版本钉死在 workflow 中 - **Actions 版本**:checkout/setup-node/pnpm-action 均为 v6(Node 24 兼容) @@ -125,17 +126,18 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run ## 常见漏点(基于历史踩坑) -| 漏点 | 后果 | -| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli | -| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 | -| cli 升版号但 core 没升 | check.mjs 会拦下 | -| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 | -| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 | -| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` | -| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17`) | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 | -| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 | -| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 | -| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 | -| channel job 缺少 `contents: write` | `gh release create` 失败 | -| stable 未先推 tag 就建 Release | `--verify-tag` 失败 | +| 漏点 | 后果 | +| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli | +| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 | +| cli 升版号但 core 没升 | check.mjs 会拦下 | +| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 | +| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 | +| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` | +| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17`) | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 | +| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 | +| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 | +| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 | +| channel job 未挂 `environment: production` | `BAILIAN_OSS_*` 只配在 production 时 channel 拿不到密钥;现已挂上。密钥仍缺则 channel 失败,避免只更新 GitHub | +| channel job 缺少 `contents: write` | `gh release create` 失败 | +| stable 未先推 tag 就建 Release | `--verify-tag` 失败 | diff --git a/tools/release/lib/binary-release.mjs b/tools/release/lib/binary-release.mjs index 2d50fba38..2977ff79a 100644 --- a/tools/release/lib/binary-release.mjs +++ b/tools/release/lib/binary-release.mjs @@ -250,6 +250,13 @@ export async function releaseBinaryArtifacts(rawOptions = {}) { // (newer-version guard). Throws on failure — CI is the only OSS writer. const plans = ossMirrorPlans({ dir, version, mode, files }); const mirror = await mirrorReleaseAssetsToOss({ plans, dryRun }); + if (mode === "channel" && mirror.skipped && !dryRun) { + throw new Error( + "Channel publish must update OSS sync-release.json, but BAILIAN_OSS_AK/SK are unset. " + + "Attach the production environment (or repository OSS secrets) to the channel job; " + + "do not treat a GitHub-only Release as a successful CDN publish.", + ); + } if (mode === "stable" && !mirror.skipped) { await maintainReleaseManifest({ tag: `v${version}`, From 195d71becfa23389470a662e2a4e92627471719e Mon Sep 17 00:00:00 2001 From: rendianmeng Date: Fri, 18 Sep 2026 14:38:40 +0800 Subject: [PATCH 3/3] revert: drop OSS secret gate from channel sync-release publishes Channel jobs skip OSS when keys are unset instead of failing, matching the previous GitHub-only release path. Co-authored-by: Cursor --- .github/workflows/publish.yml | 3 -- docs/agents/publish.md | 42 +++++++++++++--------------- tools/release/lib/binary-release.mjs | 7 ----- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 993904470..2f4430e4c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -87,9 +87,6 @@ jobs: if: inputs.mode == 'channel' && inputs.package != 'bailian-kb-dsh' name: publish channel (${{ inputs.package }}) to npm + binary runs-on: ubuntu-latest - # Same environment as stable so BAILIAN_OSS_* (environment secrets) inject. - # Without this, channel silently skips OSS and install.sh keeps the old CDN pointer. - environment: production permissions: contents: write # create prerelease GitHub Release with binary assets id-token: write # OIDC for npm Trusted Publishing + provenance diff --git a/docs/agents/publish.md b/docs/agents/publish.md index 3ae4f863b..f0919079c 100644 --- a/docs/agents/publish.md +++ b/docs/agents/publish.md @@ -30,10 +30,10 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口 两种模式: -| 模式 | 用途 | 触发方式 | -| ------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| channel | npm dist-tag +(仅 bailian-cli)二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channel,channel 填 **npm dist-tag** 名;**与 stable 同样走 production environment**(OSS 密钥) | -| stable | npm latest + GitHub Release `v` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 | +| 模式 | 用途 | 触发方式 | +| ------- | --------------------------------------------------------------------------------------- | -------------------------------------------- | +| channel | npm dist-tag +(仅 bailian-cli)二进制 + CDN **一律**覆盖 `sync-release.json` | mode=channel,channel 填 **npm dist-tag** 名 | +| stable | npm latest + GitHub Release `v` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 | 可选 flag:`--skip-binary`(仅发 npm,紧急逃生)。 @@ -48,10 +48,10 @@ workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` / ### channel 发布 -1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名。`publish-channel` 与 stable 一样走 **production** environment(OSS 密钥;若该 environment 开了 Required Reviewers,测试包也要审批): +1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名: - **`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--`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 OSS `sync-release.json`** → 还原 package.json。OSS 密钥未注入时 channel 失败,不会只更新 GitHub。 +2. CI 自动:生成 `0.0.0-beta--`(UTC 到分钟;同 commit 同分钟重跑会覆盖同号)→ 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json 3. 对应脚本:`tools/release/publish-channel.mjs` ### stable 发布 @@ -90,7 +90,6 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run - **认证**:npm OIDC Trusted Publishing(无 token),需要 `id-token: write` 权限 - **GitHub Release**:`contents: write` + `GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}`(stable / channel 均需) -- **OSS**:`BAILIAN_OSS_*` 配在 **production** environment;`publish-stable` 与 `publish-channel` 都挂该 environment,否则 channel 拿不到密钥、公网 `sync-release.json` 不滚动 - **Node 版本**:24(npm 11.5+ 才支持 OIDC token 交换) - **Bun**:`oven-sh/setup-bun`,版本钉死在 workflow 中 - **Actions 版本**:checkout/setup-node/pnpm-action 均为 v6(Node 24 兼容) @@ -126,18 +125,17 @@ node tools/release/publish-channel.mjs --channel test --knowledge --dry-run ## 常见漏点(基于历史踩坑) -| 漏点 | 后果 | -| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli | -| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 | -| cli 升版号但 core 没升 | check.mjs 会拦下 | -| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 | -| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 | -| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` | -| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17`) | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 | -| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 | -| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 | -| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 | -| channel job 未挂 `environment: production` | `BAILIAN_OSS_*` 只配在 production 时 channel 拿不到密钥;现已挂上。密钥仍缺则 channel 失败,避免只更新 GitHub | -| channel job 缺少 `contents: write` | `gh release create` 失败 | -| stable 未先推 tag 就建 Release | `--verify-tag` 失败 | +| 漏点 | 后果 | +| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| 只升部分包,漏升 runtime/commands/kscli | 当前 check.mjs 按所选发布集合校验,但未选择 `knowledge-studio-cli` 时不会覆盖 kscli | +| 新增发布包但没加 `tools/release/lib/packages.mjs` | CI 不会 bump/publish/校验该包 | +| cli 升版号但 core 没升 | check.mjs 会拦下 | +| 发版漏更 CHANGELOG,或分类写成规范外的 `优化`/`Improved` | 用户看不到本次变更,分类与历史不一致 | +| `1.0.0` 当 beta 直接发 | 占了 `latest` tag,所有用户被强升,撤回成本极高 | +| README 写的 bin 名实际 `package.json.bin` 没注册 | 用户复制命令报 `command not found` | +| Node 徽章与 `cli/package.json.engines` 不一致(当前应为 `>=18.17`) | 用户在声明外的 Node 上 `npm i` 被 engine 警告或直接失败 | +| npm Trusted Publisher 的 workflow filename 改了没同步 | OIDC 匹配不上,publish 报 404 | +| CI 用 Node 22(npm 10)跑 publish | npm 10 不支持 OIDC token 交换,publish 报 404 | +| stable 发布前没有升级版本号 | 所选发布集合的版本已全部存在于 npm,CI 明确报错并要求先升级版本号 | +| channel job 缺少 `contents: write` | `gh release create` 失败 | +| stable 未先推 tag 就建 Release | `--verify-tag` 失败 | diff --git a/tools/release/lib/binary-release.mjs b/tools/release/lib/binary-release.mjs index 2977ff79a..2d50fba38 100644 --- a/tools/release/lib/binary-release.mjs +++ b/tools/release/lib/binary-release.mjs @@ -250,13 +250,6 @@ export async function releaseBinaryArtifacts(rawOptions = {}) { // (newer-version guard). Throws on failure — CI is the only OSS writer. const plans = ossMirrorPlans({ dir, version, mode, files }); const mirror = await mirrorReleaseAssetsToOss({ plans, dryRun }); - if (mode === "channel" && mirror.skipped && !dryRun) { - throw new Error( - "Channel publish must update OSS sync-release.json, but BAILIAN_OSS_AK/SK are unset. " + - "Attach the production environment (or repository OSS secrets) to the channel job; " + - "do not treat a GitHub-only Release as a successful CDN publish.", - ); - } if (mode === "stable" && !mirror.skipped) { await maintainReleaseManifest({ tag: `v${version}`,