mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: update manifest.json path
This commit is contained in:
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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]`. */
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user