mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ad3e7b947 | |||
| 525412f566 | |||
| 75b056ba64 |
@@ -18,7 +18,7 @@ on:
|
||||
- channel
|
||||
- stable
|
||||
channel:
|
||||
description: "Required when mode=channel. dist-tag name, e.g. mcp / plugin / release-test (lowercase, digits, dashes)"
|
||||
description: "Required when mode=channel. npm dist-tag only (lowercase, digits, dashes), e.g. mcp / plugin / sync-release. bailian-cli binary CDN always overwrites sync-release.json; knowledge-studio-cli is npm-only."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Require channel input
|
||||
if: ${{ inputs.channel == '' }}
|
||||
run: |
|
||||
echo "::error::mode=channel requires the workflow input \"channel\" (e.g. mcp, plugin, release-test). Leave mode=stable if you do not need a dist-tag."
|
||||
echo "::error::mode=channel requires the workflow input \"channel\" (npm dist-tag, e.g. mcp / plugin / sync-release). Leave mode=stable if you do not need a dist-tag."
|
||||
exit 1
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
+6
-2
@@ -8,7 +8,7 @@
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash
|
||||
```
|
||||
|
||||
可选:正式安装读 CDN 上的 `manifest.json`。预发 / channel 验证示例:
|
||||
可选:正式安装读 CDN 上的 `manifest.json`。预发 / channel 验证一律读 `sync-release.json`(bailian-cli 的 channel 发版都会覆盖它):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash -s -- --channel sync-release
|
||||
@@ -19,10 +19,14 @@ curl -fsSL https://bailian.aliyun.com/cli/install.sh | bash -s -- --channel sync
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
# 正式(manifest.json)
|
||||
irm https://bailian.aliyun.com/cli/install.ps1 | iex
|
||||
|
||||
# channel / 预发验证(sync-release.json)
|
||||
$env:BAILIAN_CHANNEL = 'sync-release'; irm 'https://bailian.aliyun.com/cli/install.ps1' | iex
|
||||
```
|
||||
|
||||
带参时先落盘再执行(`irm | iex` 不便传参),或使用仓外静态资源文档中的预发入口。
|
||||
带其它参时先落盘再执行(`irm | iex` 不便传参),或使用仓外静态资源文档中的预发入口。
|
||||
|
||||
二进制安装布局为 `versions/<ver>/` + `current` 指针;`bl update` 只切换指针并清理旧版本(保留当前与上一版)。更新进程退出后,下次执行 `bl` 即使用新版本(无需「重启应用」)。
|
||||
|
||||
|
||||
+18
-7
@@ -26,17 +26,28 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口
|
||||
|
||||
两种模式:
|
||||
|
||||
| 模式 | 用途 | 触发方式 |
|
||||
| ------- | -------------------------------------------------------- | -------------------------------------------------- |
|
||||
| channel | npm dist-tag + GitHub prerelease + 滚动 `channel-<name>` | 选 mode=channel,填 dist-tag 名称(如 mcp/plugin) |
|
||||
| stable | npm latest + GitHub Release `v<ver>`(含 install 脚本) | 选 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<ver>` + CDN **`manifest.json`**(及 `latest.json` 别名) | mode=stable,需 production environment 审批 |
|
||||
|
||||
可选 flag:`--skip-binary`(仅发 npm,紧急逃生)。
|
||||
|
||||
### CDN 滚动指针(bailian-cli)
|
||||
|
||||
| 发布模式 | CDN 指针 | 本机安装 / 更新 |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------------------- |
|
||||
| channel | 始终覆盖 `sync-release.json` | `BAILIAN_CHANNEL=sync-release` / `install --channel sync-release` |
|
||||
| stable | `manifest.json`(+ `latest.json`) | 默认安装 / `bl update`(无 channel) |
|
||||
|
||||
workflow 的 `channel` 输入**只决定 npm dist-tag**(如 `mcp` / `plugin` / `sync-release`),**不再**生成 `release-test.json` 这类旁路文件。
|
||||
|
||||
### channel 发布
|
||||
|
||||
1. 在 GitHub 触发 Publish workflow,package 选 `bailian-cli` 或 `knowledge-studio-cli`,mode 选 `channel`,channel 填 dist-tag 名(如 `mcp`)
|
||||
2. CI 自动:生成 `0.0.0-beta-<sha7>-<date>` → 临时 bump → 自检 → **npm 发到 dist-tag** → **Bun 编二进制并创建 GitHub prerelease + 滚动 channel manifest** → 还原 package.json
|
||||
1. 在 GitHub 触发 Publish workflow,mode 选 `channel`,channel 填 npm dist-tag 名:
|
||||
- **`bailian-cli`**:npm 发到该 tag;二进制同时刷新 CDN `sync-release.json`(与 tag 名无关)。本机验证:`BAILIAN_CHANNEL=sync-release`
|
||||
- **`knowledge-studio-cli`**:仅 npm(自动跳过 binary,不碰 `sync-release.json`)
|
||||
2. CI 自动:生成 `0.0.0-beta-<sha7>-<date>` → 临时 bump → 自检 → **npm 发到 dist-tag** →(bailian-cli)**Bun 编二进制 + GH prerelease + 覆盖 `sync-release.json`** → 还原 package.json
|
||||
3. 对应脚本:`tools/release/publish-channel.mjs`
|
||||
|
||||
### stable 发布
|
||||
@@ -44,7 +55,7 @@ publish-stable.mjs / publish-channel.mjs ← 唯一发版入口
|
||||
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** → 完成
|
||||
4. CI 自动:自检 → **npm 发到 latest** → **推送 git tag `v<ver>`** → **Bun 编二进制并创建/更新 GitHub Release** →(bailian-cli)维护 CDN **`manifest.json`** → 完成
|
||||
5. 如果所选发布集合的当前版本已全部存在于 npm,stable 发布会失败并提示先升级版本号;如果只有部分包已发布,CI 会继续补发缺失包
|
||||
6. 对应脚本:`tools/release/publish-stable.mjs`
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
* v<version>/<asset>.zip —— immutable per-version binaries + SHA256SUMS
|
||||
* manifest.json —— stable install/update pointer (rolling-manifest shape)
|
||||
* latest.json —— stable alias; same body as manifest.json
|
||||
* <channel>.json —— per-channel rolling manifests (betas / local verify)
|
||||
* sync-release.json —— official channel/verify rolling pointer (all bailian-cli
|
||||
* channel publishes overwrite this; npm dist-tag is separate)
|
||||
*
|
||||
* Legacy `{name}.json` files may still exist on CDN; install may resolve them, but
|
||||
* release tooling no longer creates per-dist-tag manifests.
|
||||
*
|
||||
* Override with `BAILIAN_CLI_CDN`.
|
||||
*/
|
||||
@@ -29,7 +33,8 @@ export function getCliCdnBase(): string {
|
||||
/**
|
||||
* Rolling manifest URL at the CDN base root.
|
||||
* Stable (`latest` / `stable` / empty) → `manifest.json`.
|
||||
* Named channel → `{channel}.json` (e.g. sync-release for local verify).
|
||||
* Official verify line → `sync-release.json` (`channel=sync-release`).
|
||||
* Other names still map to `{channel}.json` for backward compatibility only.
|
||||
* All share the same rolling-manifest shape from binary-build.
|
||||
*/
|
||||
export function channelManifestUrl(channel = "latest"): string {
|
||||
|
||||
@@ -269,9 +269,26 @@ export async function ensureBinaryPathEntries(version: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureWindowsBinJunction(binDir: string): Promise<void> {
|
||||
function errnoCode(error: unknown): string {
|
||||
if (error && typeof error === "object" && "code" in error) {
|
||||
return String((error as { code?: unknown }).code ?? "");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure `shareRoot/bin` is a junction → `current`.
|
||||
*
|
||||
* Install scripts / older layouts may leave a real `bin/` directory with
|
||||
* `bl.exe` inside. Deleting that directory fails with EACCES while this
|
||||
* process is the running image — rename-away first (Windows allows that),
|
||||
* then create the junction. Stale `bin.migrating-*` dirs are best-effort GC.
|
||||
*/
|
||||
export async function ensureWindowsBinJunction(binDir: string): Promise<void> {
|
||||
const currentPath = getBinaryCurrentPath();
|
||||
const { symlink } = await import("node:fs/promises");
|
||||
const { symlink, rename } = await import("node:fs/promises");
|
||||
|
||||
let migratedAside: string | null = null;
|
||||
|
||||
try {
|
||||
const stats = await lstat(binDir);
|
||||
@@ -288,18 +305,33 @@ async function ensureWindowsBinJunction(binDir: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
await unlink(binDir);
|
||||
await symlink(currentPath, binDir, "junction");
|
||||
return;
|
||||
} else if (stats.isDirectory()) {
|
||||
// Prefer rename over rm: a running bl.exe inside bin locks delete/rm,
|
||||
// but rename of the directory usually succeeds on Windows.
|
||||
migratedAside = `${binDir}.migrating.${process.pid}`;
|
||||
try {
|
||||
await rename(binDir, migratedAside);
|
||||
} catch (renameError) {
|
||||
// Fallback: empty / unlocked real dirs can still be removed.
|
||||
try {
|
||||
await rm(binDir, { recursive: true, force: true });
|
||||
migratedAside = null;
|
||||
} catch (rmError) {
|
||||
const code = errnoCode(renameError) || errnoCode(rmError) || "EACCES";
|
||||
throw new Error(
|
||||
`Failed to migrate ${binDir} to a junction pointing at current (${code}). ` +
|
||||
`Close other bl sessions and re-run update, or re-run the install script once.`,
|
||||
{ cause: rmError },
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await unlink(binDir).catch(() => rm(binDir, { recursive: true, force: true }));
|
||||
}
|
||||
// Real directory from older installs: replace with junction to current.
|
||||
// May fail if a running exe inside bin is locked — surface a clear error.
|
||||
await rm(binDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
const code =
|
||||
error && typeof error === "object" && "code" in error
|
||||
? String((error as { code?: unknown }).code)
|
||||
: "";
|
||||
const code = errnoCode(error);
|
||||
if (code && code !== "ENOENT") {
|
||||
if (error instanceof Error && error.message.includes("Failed to migrate")) throw error;
|
||||
throw new Error(
|
||||
`Failed to migrate ${binDir} to a junction pointing at current (${code}). ` +
|
||||
`Close other bl sessions and re-run update, or re-run the install script once.`,
|
||||
@@ -310,6 +342,11 @@ async function ensureWindowsBinJunction(binDir: string): Promise<void> {
|
||||
|
||||
await mkdir(dirname(binDir), { recursive: true });
|
||||
await symlink(currentPath, binDir, "junction");
|
||||
|
||||
if (migratedAside) {
|
||||
// Best-effort: locked exes may keep the aside dir until process exit.
|
||||
await rm(migratedAside, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import {
|
||||
ensureBinaryPathEntries,
|
||||
ensureWindowsBinJunction,
|
||||
getBinaryBinRoot,
|
||||
getBinaryCurrentPath,
|
||||
getBinaryShareRoot,
|
||||
@@ -143,3 +144,22 @@ test("ensureBinaryPathEntries wires PATH entries through current", async () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("ensureWindowsBinJunction migrates a real bin directory via rename", async () => {
|
||||
await withTempBinaryRoots(async () => {
|
||||
seedVersion("3.0.0");
|
||||
await switchCurrentToVersion("3.0.0");
|
||||
|
||||
const binRoot = getBinaryBinRoot();
|
||||
mkdirSync(binRoot, { recursive: true });
|
||||
writeFileSync(join(binRoot, "bl.exe"), "old-copy");
|
||||
|
||||
await ensureWindowsBinJunction(binRoot);
|
||||
|
||||
const target = readlinkSync(binRoot);
|
||||
expect(target.replaceAll("\\", "/").toLowerCase()).toBe(
|
||||
getBinaryCurrentPath().replaceAll("\\", "/").toLowerCase(),
|
||||
);
|
||||
expect(existsSync(`${binRoot}.migrating.${process.pid}`)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
* node tools/release/lib/binary-build.mjs --mode stable --host
|
||||
*
|
||||
* Manifests:
|
||||
* --mode stable → writes latest.json (rolling manifest of the implicit
|
||||
* "latest" channel; OSS prefix root only, not a GH asset)
|
||||
* --mode channel → writes <channel>.json
|
||||
* --mode stable → writes latest.json (fed into OSS manifest.json + latest.json)
|
||||
* --mode channel → always writes sync-release.json (npm --channel is dist-tag only)
|
||||
*/
|
||||
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
@@ -17,7 +16,11 @@ import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { parseArgs } from "node:util";
|
||||
import { ROOT, readPackageJson, PACKAGES } from "./packages.mjs";
|
||||
import { channelManifestFileName, normalizeModeChannel } from "./binary-options.mjs";
|
||||
import {
|
||||
normalizeModeChannel,
|
||||
rollingManifestChannelId,
|
||||
rollingManifestFileName,
|
||||
} from "./binary-options.mjs";
|
||||
import { ensureZip, zipOne } from "./binary-zip.mjs";
|
||||
|
||||
const BINARY_COMPILE = fileURLToPath(new URL("./binary-compile.mjs", import.meta.url));
|
||||
@@ -157,8 +160,9 @@ function writeChecksums(outdir, artifacts) {
|
||||
writeFileSync(join(outdir, "SHA256SUMS"), `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
/** Write the rolling channel manifest (`latest.json` / `<channel>.json`) with per-platform zip file + sha256. */
|
||||
function writeChannelManifest(outdir, version, artifacts, channel) {
|
||||
/** Write the rolling channel manifest (`latest.json` / `sync-release.json`) with per-platform zip + sha256. */
|
||||
function writeChannelManifest(outdir, version, artifacts, mode) {
|
||||
const channel = rollingManifestChannelId(mode);
|
||||
const assets = Object.fromEntries(
|
||||
artifacts.map((item) => [
|
||||
`${item.os}-${item.arch}`,
|
||||
@@ -176,7 +180,7 @@ function writeChannelManifest(outdir, version, artifacts, channel) {
|
||||
releasedAt: new Date().toISOString(),
|
||||
assets,
|
||||
};
|
||||
const name = channelManifestFileName(channel);
|
||||
const name = rollingManifestFileName(mode);
|
||||
writeJson(join(outdir, name), manifest);
|
||||
return name;
|
||||
}
|
||||
@@ -222,9 +226,7 @@ export function buildBinaryArtifacts(rawOptions = {}) {
|
||||
writeChecksums(outdir, artifacts);
|
||||
|
||||
const extras = ["SHA256SUMS"];
|
||||
extras.push(
|
||||
writeChannelManifest(outdir, version, artifacts, mode === "channel" ? channel : "latest"),
|
||||
);
|
||||
extras.push(writeChannelManifest(outdir, version, artifacts, mode));
|
||||
|
||||
log(`\nBuilt ${artifacts.length} zip(s):`);
|
||||
for (const item of artifacts) {
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
/**
|
||||
* Shared mode / channel / manifest naming for binary-build and binary-release.
|
||||
*
|
||||
* Every mode writes a rolling channel manifest: stable writes `latest.json`
|
||||
* (uploaded to the OSS prefix root only, never attached to the GitHub Release),
|
||||
* channel writes `<channel>.json` (attached to the rolling `channel-<name>`
|
||||
* GitHub Release and uploaded to the OSS prefix root).
|
||||
* Rolling CDN pointers (bailian-cli binary):
|
||||
* - stable → build writes `latest.json`; OSS maintains `manifest.json` (+ `latest.json`)
|
||||
* - channel → always `sync-release.json` on OSS / GH rolling release `channel-sync-release`
|
||||
*
|
||||
* The workflow `--channel` value remains the npm dist-tag (and versioned GH release notes).
|
||||
* It does not choose the CDN rolling filename.
|
||||
*/
|
||||
import { assertChannel } from "./validate.mjs";
|
||||
|
||||
/** Official bailian-cli channel/verify rolling manifest name on CDN. */
|
||||
export const SYNC_RELEASE_CHANNEL = "sync-release";
|
||||
|
||||
/** Stable build-local rolling manifest (fed into maintainReleaseManifest). */
|
||||
export const STABLE_ROLLING_CHANNEL = "latest";
|
||||
|
||||
/**
|
||||
* @param {string} mode
|
||||
* @param {string | null | undefined} channel
|
||||
@@ -28,8 +36,40 @@ export function normalizeModeChannel(mode = "stable", channel = null) {
|
||||
return { mode: "stable", channel: null };
|
||||
}
|
||||
|
||||
/** Rolling-manifest basename: `latest.json` for stable, `<channel>.json` otherwise. */
|
||||
/**
|
||||
* Rolling-manifest basename for a logical channel id inside a build artifact.
|
||||
* Prefer {@link rollingManifestFileName} for mode-aware naming.
|
||||
*/
|
||||
export function channelManifestFileName(channel) {
|
||||
if (!channel) throw new Error("channelManifestFileName requires a channel name");
|
||||
return `${channel}.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rolling manifest basename written by binary-build / uploaded to OSS root.
|
||||
* - stable → `latest.json`
|
||||
* - channel → always `sync-release.json` (ignores npm dist-tag name)
|
||||
*
|
||||
* @param {"stable" | "channel"} mode
|
||||
* @returns {string}
|
||||
*/
|
||||
export function rollingManifestFileName(mode) {
|
||||
if (mode === "stable") return channelManifestFileName(STABLE_ROLLING_CHANNEL);
|
||||
if (mode === "channel") return channelManifestFileName(SYNC_RELEASE_CHANNEL);
|
||||
throw new Error(`rollingManifestFileName: unknown mode ${mode}`);
|
||||
}
|
||||
|
||||
/** Channel id embedded in the rolling manifest JSON body. */
|
||||
export function rollingManifestChannelId(mode) {
|
||||
if (mode === "stable") return STABLE_ROLLING_CHANNEL;
|
||||
if (mode === "channel") return SYNC_RELEASE_CHANNEL;
|
||||
throw new Error(`rollingManifestChannelId: unknown mode ${mode}`);
|
||||
}
|
||||
|
||||
/** GitHub rolling prerelease tag that holds only the CDN channel pointer. */
|
||||
export function rollingChannelReleaseTag(mode) {
|
||||
if (mode !== "channel") {
|
||||
throw new Error("rollingChannelReleaseTag is only valid for mode=channel");
|
||||
}
|
||||
return `channel-${SYNC_RELEASE_CHANNEL}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
channelManifestFileName,
|
||||
normalizeModeChannel,
|
||||
rollingChannelReleaseTag,
|
||||
rollingManifestChannelId,
|
||||
rollingManifestFileName,
|
||||
STABLE_ROLLING_CHANNEL,
|
||||
SYNC_RELEASE_CHANNEL,
|
||||
} from "./binary-options.mjs";
|
||||
|
||||
describe("rolling CDN manifest naming", () => {
|
||||
it("keeps sync-release and latest constants", () => {
|
||||
assert.equal(SYNC_RELEASE_CHANNEL, "sync-release");
|
||||
assert.equal(STABLE_ROLLING_CHANNEL, "latest");
|
||||
});
|
||||
|
||||
it("channel mode always rolls sync-release.json regardless of npm dist-tag", () => {
|
||||
const { mode, channel } = normalizeModeChannel("channel", "mcp");
|
||||
assert.equal(mode, "channel");
|
||||
assert.equal(channel, "mcp");
|
||||
assert.equal(rollingManifestFileName(mode), "sync-release.json");
|
||||
assert.equal(rollingManifestChannelId(mode), "sync-release");
|
||||
assert.equal(rollingChannelReleaseTag(mode), "channel-sync-release");
|
||||
});
|
||||
|
||||
it("stable mode rolls latest.json for maintainReleaseManifest input", () => {
|
||||
const { mode, channel } = normalizeModeChannel("stable", null);
|
||||
assert.equal(mode, "stable");
|
||||
assert.equal(channel, null);
|
||||
assert.equal(rollingManifestFileName(mode), "latest.json");
|
||||
assert.equal(rollingManifestChannelId(mode), "latest");
|
||||
});
|
||||
|
||||
it("channelManifestFileName still formats arbitrary names for helpers", () => {
|
||||
assert.equal(channelManifestFileName("release-test"), "release-test.json");
|
||||
assert.equal(channelManifestFileName(SYNC_RELEASE_CHANNEL), "sync-release.json");
|
||||
});
|
||||
});
|
||||
@@ -2,18 +2,19 @@
|
||||
* 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)
|
||||
* assets: bl-*.zip, 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)
|
||||
* + rolling prerelease tag `channel-<name>` holding only `<name>.json`
|
||||
* + rolling prerelease `channel-sync-release` holding only sync-release.json
|
||||
* OSS: always overwrite prefix-root sync-release.json
|
||||
*
|
||||
* Same commit/day channel publishes share one `v<betaVersion>` Release (identical
|
||||
* binaries); only the rolling `channel-<name>` manifest differs per channel.
|
||||
* Workflow `--channel` is the npm dist-tag (and versioned release notes); it does
|
||||
* not choose the CDN rolling filename. Same commit/day channel publishes share one
|
||||
* `v<betaVersion>` Release (identical binaries).
|
||||
*
|
||||
* Re-runs are idempotent via `gh release upload --clobber` (see gh-release.mjs).
|
||||
* After the GitHub upload the same assets are pushed straight to OSS from the
|
||||
* runner, HEAD-reconciled, and (stable only) release/manifest.json + latest.json
|
||||
* are rewritten with the SAME rolling-manifest body as channel `<channel>.json` —
|
||||
* all in-process, no external FC (see oss-direct-upload.mjs).
|
||||
* runner and HEAD-reconciled — all in-process, no external FC (see oss-direct-upload.mjs).
|
||||
*
|
||||
* Called by publish-stable.mjs / publish-channel.mjs.
|
||||
* Debug:
|
||||
@@ -26,7 +27,12 @@ import { fileURLToPath } from "node:url";
|
||||
import { parseArgs as parseCliArgs } from "node:util";
|
||||
import { ROOT, readPackageJson, PACKAGES } from "./packages.mjs";
|
||||
import { buildBinaryArtifacts, matrixAssetNames } from "./binary-build.mjs";
|
||||
import { channelManifestFileName, normalizeModeChannel } from "./binary-options.mjs";
|
||||
import {
|
||||
normalizeModeChannel,
|
||||
rollingChannelReleaseTag,
|
||||
rollingManifestFileName,
|
||||
SYNC_RELEASE_CHANNEL,
|
||||
} from "./binary-options.mjs";
|
||||
import { ensureGh, GITHUB_REPOSITORY, upsertRelease } from "./gh-release.mjs";
|
||||
import {
|
||||
maintainReleaseManifest,
|
||||
@@ -79,22 +85,23 @@ function uploadStable({ dir, version, files, dryRun }) {
|
||||
}
|
||||
|
||||
function uploadChannel({ dir, version, channel, files, dryRun }) {
|
||||
// Versioned tag is shared across channels built from the same beta version string.
|
||||
// Versioned tag is shared across npm dist-tags built from the same beta version.
|
||||
upsertRelease({
|
||||
tag: `v${version}`,
|
||||
title: `v${version}`,
|
||||
prerelease: true,
|
||||
notes: `Beta build for the \`${channel}\` channel.`,
|
||||
notes: `Beta build (npm dist-tag \`${channel}\`). CDN rolling pointer: ${SYNC_RELEASE_CHANNEL}.json.`,
|
||||
assets: versionBinaryAssets(dir, version, files),
|
||||
dryRun,
|
||||
});
|
||||
|
||||
const rollingTag = rollingChannelReleaseTag("channel");
|
||||
upsertRelease({
|
||||
tag: `channel-${channel}`,
|
||||
title: `channel: ${channel}`,
|
||||
tag: rollingTag,
|
||||
title: `channel: ${SYNC_RELEASE_CHANNEL}`,
|
||||
prerelease: true,
|
||||
notes: `Rolling manifest for the \`${channel}\` channel. Latest beta: ${version}.`,
|
||||
assets: [join(dir, channelManifestFileName(channel))],
|
||||
notes: `Rolling CDN manifest (${SYNC_RELEASE_CHANNEL}.json). Latest beta: ${version} (npm dist-tag \`${channel}\`).`,
|
||||
assets: [join(dir, rollingManifestFileName("channel"))],
|
||||
dryRun,
|
||||
});
|
||||
}
|
||||
@@ -117,16 +124,16 @@ function planDryRunWithoutArtifacts({ version, mode, channel }) {
|
||||
tag: `v${version}`,
|
||||
title: `v${version}`,
|
||||
prerelease: true,
|
||||
notes: `Beta build for the \`${channel}\` channel.`,
|
||||
notes: `Beta build (npm dist-tag \`${channel}\`). CDN rolling pointer: ${SYNC_RELEASE_CHANNEL}.json.`,
|
||||
assets: [...matrix, "SHA256SUMS"],
|
||||
dryRun: true,
|
||||
});
|
||||
upsertRelease({
|
||||
tag: `channel-${channel}`,
|
||||
title: `channel: ${channel}`,
|
||||
tag: rollingChannelReleaseTag("channel"),
|
||||
title: `channel: ${SYNC_RELEASE_CHANNEL}`,
|
||||
prerelease: true,
|
||||
notes: `Rolling manifest for the \`${channel}\` channel. Latest beta: ${version}.`,
|
||||
assets: [channelManifestFileName(channel)],
|
||||
notes: `Rolling CDN manifest (${SYNC_RELEASE_CHANNEL}.json). Latest beta: ${version} (npm dist-tag \`${channel}\`).`,
|
||||
assets: [rollingManifestFileName("channel")],
|
||||
dryRun: true,
|
||||
});
|
||||
}
|
||||
@@ -136,15 +143,14 @@ function planDryRunWithoutArtifacts({ version, mode, channel }) {
|
||||
* upload. `files == null` means dry-run planning without artifacts on disk,
|
||||
* so bare basenames stand in for real paths.
|
||||
*/
|
||||
function ossMirrorPlans({ dir, version, mode, channel, files }) {
|
||||
function ossMirrorPlans({ dir, version, mode, files }) {
|
||||
const paths = files
|
||||
? versionBinaryAssets(dir, version, files)
|
||||
: [...matrixAssetNames(version), "SHA256SUMS"];
|
||||
const plans = [{ tag: `v${version}`, paths }];
|
||||
if (mode === "channel") {
|
||||
const manifest = channelManifestFileName(channel);
|
||||
// Rolling channel manifest lives at the OSS prefix root, next to
|
||||
// manifest.json / latest.json — one flat json per channel.
|
||||
const manifest = rollingManifestFileName("channel");
|
||||
// Always sync-release.json at the OSS prefix root (next to manifest.json).
|
||||
plans.push({ tag: "", paths: [files ? join(dir, manifest) : manifest] });
|
||||
}
|
||||
return plans;
|
||||
@@ -183,7 +189,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
|
||||
if (dryRun && !existsSync(dir)) {
|
||||
process.stdout.write(`[dry-run] ${dir} missing; planning expected assets\n`);
|
||||
planDryRunWithoutArtifacts({ version, mode, channel });
|
||||
const plans = ossMirrorPlans({ dir, version, mode, channel, files: null });
|
||||
const plans = ossMirrorPlans({ dir, version, mode, files: null });
|
||||
await syncStaticFilesToOss({
|
||||
filePaths: [join(ROOT, "CHANGELOG.md"), join(ROOT, "CHANGELOG.zh.md")],
|
||||
dryRun: true,
|
||||
@@ -209,10 +215,10 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
|
||||
if (!files.includes("SHA256SUMS")) {
|
||||
throw new Error(`Missing SHA256SUMS in ${dir}`);
|
||||
}
|
||||
const rollingManifest = channelManifestFileName(mode === "channel" ? channel : "latest");
|
||||
const rollingManifest = rollingManifestFileName(mode);
|
||||
if (!files.includes(rollingManifest)) {
|
||||
throw new Error(
|
||||
`Missing ${rollingManifest} in ${dir}. Rebuild with matching --mode/--channel (found: ${files.join(", ") || "(empty)"}).`,
|
||||
`Missing ${rollingManifest} in ${dir}. Rebuild with matching --mode (found: ${files.join(", ") || "(empty)"}).`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,7 +248,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
|
||||
// Push the exact Release assets straight to OSS from the runner, then
|
||||
// HEAD-reconcile. Stable releases additionally maintain release/manifest.json
|
||||
// (newer-version guard). Throws on failure — CI is the only OSS writer.
|
||||
const plans = ossMirrorPlans({ dir, version, mode, channel, files });
|
||||
const plans = ossMirrorPlans({ dir, version, mode, files });
|
||||
const mirror = await mirrorReleaseAssetsToOss({ plans, dryRun });
|
||||
if (mode === "stable" && !mirror.skipped) {
|
||||
await maintainReleaseManifest({
|
||||
|
||||
@@ -4,15 +4,14 @@
|
||||
* No external FC is involved anymore; CI is the single writer.
|
||||
*
|
||||
* Flow:
|
||||
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>`; rolling
|
||||
* channel manifests (`<channel>.json`) go to the prefix root (empty tag).
|
||||
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>`; channel
|
||||
* mode also uploads `sync-release.json` to the prefix root (empty tag).
|
||||
* - After upload, every object is HEAD-verified against the local byte size
|
||||
* (reconciliation — the runner has the ground-truth artifacts on disk).
|
||||
* - Stable only: when the tag is a NEWER version than the current manifest
|
||||
* (compareVersions), rewrite `<prefix>/manifest.json` and the rolling
|
||||
* `<prefix>/latest.json` — both carry the SAME rolling-manifest body
|
||||
* written by binary-build.mjs, so manifest.json shares the channel
|
||||
* `<channel>.json` shape. Channel/prerelease never touches either.
|
||||
* written by binary-build.mjs. Channel mode never touches those two files.
|
||||
*
|
||||
* Zero-dependency: OSS V1 header signature (HMAC-SHA1) over plain fetch.
|
||||
*
|
||||
|
||||
@@ -35,7 +35,9 @@ const { values } = parseArgs({
|
||||
const channel = values.channel;
|
||||
const dryRun = values["dry-run"];
|
||||
const knowledge = values.knowledge;
|
||||
const skipBinary = values["skip-binary"];
|
||||
// knowledge-studio-cli channel publishes are npm-only: binary artifacts are `bl`
|
||||
// and must not overwrite CDN sync-release.json used by bailian-cli install verify.
|
||||
const skipBinary = values["skip-binary"] || knowledge;
|
||||
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
|
||||
assertChannel(channel);
|
||||
|
||||
@@ -89,18 +91,22 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// 2) binary GitHub Release — must run before finally restores package.json versions
|
||||
// 2) binary GitHub Release — must run before finally restores package.json versions.
|
||||
// Channel binary always refreshes OSS sync-release.json (npm tag is independent).
|
||||
if (skipBinary) {
|
||||
log("\n[skip-binary] skipping binary GitHub Release");
|
||||
const reason = knowledge
|
||||
? "[knowledge] skipping binary (npm-only; does not touch sync-release.json)"
|
||||
: "[skip-binary] skipping binary GitHub Release";
|
||||
log(`\n${reason}`);
|
||||
} else {
|
||||
step(
|
||||
`publish binary GitHub Release (mode=channel, channel=${channel}, version=${betaVersion})`,
|
||||
`publish binary GitHub Release (mode=channel, npm-tag=${channel}, CDN=sync-release, version=${betaVersion})`,
|
||||
);
|
||||
await releaseBinaryArtifacts({ mode: "channel", channel, dryRun });
|
||||
}
|
||||
|
||||
const parts = ["npm"];
|
||||
if (!skipBinary) parts.push("binary");
|
||||
if (!skipBinary) parts.push("binary/sync-release");
|
||||
log(`\nchannel release complete: ${channel}@${betaVersion} (${parts.join(" + ")})`);
|
||||
} catch (error) {
|
||||
process.stderr.write(`\nrelease publish-channel failed: ${error.message}\n`);
|
||||
|
||||
Reference in New Issue
Block a user