feat: update manifest.json path

This commit is contained in:
故璃
2026-07-25 08:43:38 +08:00
parent 6bdc16597b
commit ebbd173b79
9 changed files with 114 additions and 52 deletions
+1
View File
@@ -25,6 +25,7 @@
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
"test:stress": "node packages/cli/tests/stress/run.mjs"
},
"dependencies": {},
"devDependencies": {
"tsx": "catalog:",
"vite-plus": "catalog:"
+2 -3
View File
@@ -2,6 +2,7 @@ import { execSync } from "child_process";
import { writeFileSync } from "fs";
import { join } from "path";
import {
DEFAULT_INSTALL_SCRIPT_URL,
defineCommand,
getConfigDir,
getInstallMethod,
@@ -101,9 +102,7 @@ export default defineCommand({
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`\nAutomatic binary update failed: ${message}\n`);
process.stderr.write("Re-run the install script:\n");
process.stderr.write(
" curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash\n\n",
);
process.stderr.write(` curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash\n\n`);
}
return;
}
+17 -9
View File
@@ -1,11 +1,17 @@
/**
* End-user binary download base (OSS mirror).
* GitHub Releases remain the publish source of truth; an external FC syncs assets here.
* Production install.sh / install.ps1 are maintained outside this repo and read OSS only.
* End-user binary download base (OSS). CI publishes release assets and rolling
* channel manifests here directly (tools/release/lib/oss-direct-upload.mjs);
* no external FC is involved.
*
* Layout under the base:
* v<version>/<asset>.zip —— immutable per-version binaries + SHA256SUMS
* manifest.json —— stable pointer { latest, releasedAt, assets }
* latest.json —— stable rolling manifest (os-arch keyed, sha256)
* <channel>.json —— per-channel rolling manifests (beta versions)
*
* Override with `BAILIAN_CLI_CDN`.
*/
export const DEFAULT_CLI_CDN_BASE = "https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli";
export const DEFAULT_CLI_CDN_BASE = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release";
/** GitHub Releases base — used when writing manifests attached to gh release assets. */
export const GITHUB_RELEASES_BASE = "https://github.com/modelstudioai/cli/releases";
@@ -20,16 +26,18 @@ export function getCliCdnBase(): string {
}
/**
* Channel manifest on OSS (after FC sync): `{base}/channels/{channel}.json`
* Formal installs still read `channels/latest.json` on OSS; this repo no longer
* attaches `latest.json` to GitHub Releases (FC / ops maintain OSS latest).
* Rolling channel manifest at the CDN base root: `{base}/{channel}.json`.
* `latest.json` (stable) and `<channel>.json` (betas) share the same shape;
* both are written by CI on publish.
*/
export function channelManifestUrl(channel = "latest"): string {
return `${getCliCdnBase()}/channels/${channel}.json`;
return `${getCliCdnBase()}/${channel}.json`;
}
/** Immutable per-version asset: `{base}/v{version}/{fileName}`. */
export function releaseAssetUrl(version: string, fileName: string): string {
return `${getCliCdnBase()}/releases/${version}/${fileName}`;
const tag = version.startsWith("v") ? version : `v${version}`;
return `${getCliCdnBase()}/${tag}/${fileName}`;
}
/** Platform triple used in asset names: `bl-<ver>-<os>-<arch>[.exe]`. */
+7 -2
View File
@@ -1,6 +1,11 @@
import { join } from "path";
import { readFileSync, writeFileSync } from "fs";
import { getConfigDir, trackingHeaders, getInstallMethod } from "bailian-cli-core";
import {
DEFAULT_INSTALL_SCRIPT_URL,
getConfigDir,
trackingHeaders,
getInstallMethod,
} from "bailian-cli-core";
export const NPM_REGISTRY = "https://registry.npmjs.org";
/** Default npm package; products override per-call via the `npmPackage` argument. */
@@ -257,7 +262,7 @@ export async function performAutoUpdate(
} catch (err) {
process.stderr.write(` ${yellow}⚠ Auto-update failed: ${errorMessage(err)}${reset}\n`);
process.stderr.write(
` ${yellow} Re-run:${reset} ${cyan}curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash${reset}\n\n`,
` ${yellow} Re-run:${reset} ${cyan}curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash${reset}\n\n`,
);
return false;
}
+7 -9
View File
@@ -7,8 +7,9 @@
* node tools/release/lib/binary-build.mjs --mode stable --host
*
* Manifests:
* --mode stable → no release manifest (latest.json removed)
* --mode channel → writes <channel>.json only
* --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
*/
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
@@ -156,10 +157,7 @@ function writeChecksums(outdir, artifacts) {
writeFileSync(join(outdir, "SHA256SUMS"), `${lines.join("\n")}\n`);
}
/**
* Channel-only: write `<channel>.json` with per-platform zip file + sha256.
* Stable no longer emits latest.json.
*/
/** Write the rolling channel manifest (`latest.json` / `<channel>.json`) with per-platform zip file + sha256. */
function writeChannelManifest(outdir, version, artifacts, channel) {
const assets = Object.fromEntries(
artifacts.map((item) => [
@@ -224,9 +222,9 @@ export function buildBinaryArtifacts(rawOptions = {}) {
writeChecksums(outdir, artifacts);
const extras = ["SHA256SUMS"];
if (mode === "channel") {
extras.push(writeChannelManifest(outdir, version, artifacts, channel));
}
extras.push(
writeChannelManifest(outdir, version, artifacts, mode === "channel" ? channel : "latest"),
);
log(`\nBuilt ${artifacts.length} zip(s):`);
for (const item of artifacts) {
+5 -4
View File
@@ -1,9 +1,10 @@
/**
* Shared mode / channel / manifest naming for binary-build and binary-release.
*
* Stable releases no longer write `latest.json` (OSS `channels/latest.json` is
* maintained outside this repo / by FC). Channel mode still writes `<channel>.json`
* onto the rolling `channel-<name>` GitHub 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).
*/
import { assertChannel } from "./validate.mjs";
@@ -27,7 +28,7 @@ export function normalizeModeChannel(mode = "stable", channel = null) {
return { mode: "stable", channel: null };
}
/** Channel rolling-manifest basename (`mcp.json`, …). Stable has none. */
/** Rolling-manifest basename: `latest.json` for stable, `<channel>.json` otherwise. */
export function channelManifestFileName(channel) {
if (!channel) throw new Error("channelManifestFileName requires a channel name");
return `${channel}.json`;
+10 -8
View File
@@ -138,7 +138,9 @@ function ossMirrorPlans({ dir, version, mode, channel, files }) {
const plans = [{ tag: `v${version}`, paths }];
if (mode === "channel") {
const manifest = channelManifestFileName(channel);
plans.push({ tag: `channel-${channel}`, paths: [files ? join(dir, manifest) : manifest] });
// Rolling channel manifest lives at the OSS prefix root, next to
// manifest.json / latest.json — one flat json per channel.
plans.push({ tag: "", paths: [files ? join(dir, manifest) : manifest] });
}
return plans;
}
@@ -182,6 +184,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
await maintainReleaseManifest({
tag: `v${version}`,
assetNames: plans[0].paths.map((path) => basename(path)),
channelJsonPath: null,
dryRun: true,
});
}
@@ -198,13 +201,11 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
if (!files.includes("SHA256SUMS")) {
throw new Error(`Missing SHA256SUMS in ${dir}`);
}
if (mode === "channel") {
const manifestName = channelManifestFileName(channel);
if (!files.includes(manifestName)) {
throw new Error(
`Missing ${manifestName} in ${dir}. Rebuild with matching --mode/--channel (found: ${files.join(", ") || "(empty)"}).`,
);
}
const rollingManifest = channelManifestFileName(mode === "channel" ? channel : "latest");
if (!files.includes(rollingManifest)) {
throw new Error(
`Missing ${rollingManifest} in ${dir}. Rebuild with matching --mode/--channel (found: ${files.join(", ") || "(empty)"}).`,
);
}
process.stdout.write(`artifacts in ${dir}:\n`);
@@ -234,6 +235,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
await maintainReleaseManifest({
tag: `v${version}`,
assetNames: plans[0].paths.map((path) => basename(path)),
channelJsonPath: join(dir, rollingManifest),
dryRun,
});
}
+55 -16
View File
@@ -4,13 +4,13 @@
* No external FC is involved anymore; CI is the single writer.
*
* Flow:
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>` (channel
* builds include the rolling `channel-<name>/<name>.json`).
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>`; rolling
* channel manifests (`<channel>.json`) go 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 manifest.latest
* (compareVersions), rewrite `<prefix>/manifest.json`. Channel/prerelease
* never touches it — same semantics the FC sync-release used to enforce.
* (compareVersions), rewrite `<prefix>/manifest.json` and the rolling
* `<prefix>/latest.json`. Channel/prerelease never touches either.
*
* Zero-dependency: OSS V1 header signature (HMAC-SHA1) over plain fetch.
*
@@ -132,12 +132,16 @@ function buildManifest(tag, releasedAt, assetNames, cfg) {
* Signed OSS request (V1 header signature). Keys here are [A-Za-z0-9._/-] only,
* so no URL encoding is needed and the signed resource matches the request path.
*/
async function ossRequest(method, key, { creds, cfg, body = null, contentType = "" }) {
async function ossRequest(
method,
key,
{ creds, cfg, body = null, contentType = "", extraHeaders = {} },
) {
const date = new Date().toUTCString();
const contentMd5 = body ? createHash("md5").update(body).digest("base64") : "";
const canonical = `${method}\n${contentMd5}\n${contentType}\n${date}\n/${cfg.bucket}/${key}`;
const signature = createHmac("sha1", creds.sk).update(canonical).digest("base64");
const headers = { Date: date, Authorization: `OSS ${creds.ak}:${signature}` };
const headers = { Date: date, Authorization: `OSS ${creds.ak}:${signature}`, ...extraHeaders };
if (contentType) headers["Content-Type"] = contentType;
if (contentMd5) headers["Content-MD5"] = contentMd5;
const options = { method, headers };
@@ -169,12 +173,23 @@ async function putWithRetry(params, attempts = 3) {
}
}
/** Remote object byte size; null when the object does not exist. */
/**
* Remote object byte size; null when the object does not exist.
* Forces the identity encoding: for compressible types (e.g. JSON) OSS gzips
* the transfer and undici then strips the content-length header, which would
* otherwise read as a bogus size 0 here.
*/
async function headObjectSize(key, creds, cfg) {
const res = await ossRequest("HEAD", key, { creds, cfg });
const res = await ossRequest("HEAD", key, {
creds,
cfg,
extraHeaders: { "Accept-Encoding": "identity" },
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`OSS HEAD ${key} failed: HTTP ${res.status}`);
return Number(res.headers.get("content-length"));
const length = res.headers.get("content-length");
if (length == null) throw new Error(`OSS HEAD ${key} returned no content-length`);
return Number(length);
}
/** GET + parse a JSON object; null when missing or corrupt. */
@@ -210,8 +225,12 @@ export async function mirrorReleaseAssetsToOss({ plans, dryRun = false }) {
}
const { creds, cfg } = ctx;
// An empty tag means the object lives at the prefix root (rolling manifests).
const jobs = plans.flatMap(({ tag, paths }) =>
paths.map((path) => ({ path, key: `${cfg.prefix}/${tag}/${basename(path)}` })),
paths.map((path) => ({
path,
key: [cfg.prefix, tag, basename(path)].filter(Boolean).join("/"),
})),
);
if (jobs.length === 0) return { uploaded: 0, skipped: true };
@@ -273,14 +292,24 @@ export async function mirrorReleaseAssetsToOss({ plans, dryRun = false }) {
}
/**
* Maintain `<prefix>/manifest.json` for STABLE releases only: rewrite it when
* `tag` is a newer version than the current `latest` (first write included).
* Same update rule the FC sync-release used to apply.
* Maintain the STABLE pointers at the prefix root: rewrite `manifest.json`
* (and, when `channelJsonPath` is given, the rolling `latest.json`) when `tag`
* is a newer version than the current `latest` (first write included).
*
* @param {{ tag: string, assetNames: string[], dryRun?: boolean }} options
* @param {{
* tag: string,
* assetNames: string[],
* channelJsonPath?: string | null,
* dryRun?: boolean,
* }} options
* @returns {Promise<{ updated: boolean, latest: string | null }>}
*/
export async function maintainReleaseManifest({ tag, assetNames, dryRun = false }) {
export async function maintainReleaseManifest({
tag,
assetNames,
channelJsonPath = null,
dryRun = false,
}) {
const ctx = ossContext();
if (!ctx) {
process.stdout.write("[info] BAILIAN_OSS_AK/SK unset; skip manifest.json maintenance\n");
@@ -291,7 +320,7 @@ export async function maintainReleaseManifest({ tag, assetNames, dryRun = false
if (dryRun) {
process.stdout.write(
`[dry-run] manifest: GET oss://${cfg.bucket}/${key} → rewrite when ${tag} > latest (assets: ${assetNames.length})\n`,
`[dry-run] manifest: GET oss://${cfg.bucket}/${key} → rewrite manifest.json + latest.json when ${tag} > latest (assets: ${assetNames.length})\n`,
);
return { updated: false, latest: null };
}
@@ -313,5 +342,15 @@ export async function maintainReleaseManifest({ tag, assetNames, dryRun = false
contentType: "application/json",
});
process.stdout.write(`manifest.json → latest=${tag} (was ${currentLatest ?? "none"})\n`);
if (channelJsonPath) {
await putObject({
creds,
cfg,
key: `${cfg.prefix}/latest.json`,
body: readFileSync(channelJsonPath),
contentType: "application/json",
});
process.stdout.write(`latest.json → ${tag}\n`);
}
return { updated: true, latest: tag };
}
+10 -1
View File
@@ -54,7 +54,16 @@ export function loadAndValidatePackages({ packages } = {}) {
return { coreJson, cliJson };
}
const RESERVED_CHANNELS = new Set(["latest", "beta", "alpha", "next", "rc", "canary", "dev"]);
const RESERVED_CHANNELS = new Set([
"latest",
"manifest", // would collide with manifest.json at the OSS prefix root
"beta",
"alpha",
"next",
"rc",
"canary",
"dev",
]);
const CHANNEL_FORMAT = /^[a-z][a-z0-9-]{1,30}$/;
export function assertChannel(channel) {