Skip to content

Commit ebbd173

Browse files
committed
feat: update manifest.json path
1 parent 6bdc165 commit ebbd173

9 files changed

Lines changed: 114 additions & 52 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"wiki:crawl": "node tools/wiki-crawler/index.mjs",
2626
"test:stress": "node packages/cli/tests/stress/run.mjs"
2727
},
28+
"dependencies": {},
2829
"devDependencies": {
2930
"tsx": "catalog:",
3031
"vite-plus": "catalog:"

packages/commands/src/commands/update.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execSync } from "child_process";
22
import { writeFileSync } from "fs";
33
import { join } from "path";
44
import {
5+
DEFAULT_INSTALL_SCRIPT_URL,
56
defineCommand,
67
getConfigDir,
78
getInstallMethod,
@@ -101,9 +102,7 @@ export default defineCommand({
101102
const message = error instanceof Error ? error.message : String(error);
102103
process.stderr.write(`\nAutomatic binary update failed: ${message}\n`);
103104
process.stderr.write("Re-run the install script:\n");
104-
process.stderr.write(
105-
" curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash\n\n",
106-
);
105+
process.stderr.write(` curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash\n\n`);
107106
}
108107
return;
109108
}

packages/core/src/install/cdn.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
/**
2-
* End-user binary download base (OSS mirror).
3-
* GitHub Releases remain the publish source of truth; an external FC syncs assets here.
4-
* Production install.sh / install.ps1 are maintained outside this repo and read OSS only.
2+
* End-user binary download base (OSS). CI publishes release assets and rolling
3+
* channel manifests here directly (tools/release/lib/oss-direct-upload.mjs);
4+
* no external FC is involved.
5+
*
6+
* Layout under the base:
7+
* v<version>/<asset>.zip —— immutable per-version binaries + SHA256SUMS
8+
* manifest.json —— stable pointer { latest, releasedAt, assets }
9+
* latest.json —— stable rolling manifest (os-arch keyed, sha256)
10+
* <channel>.json —— per-channel rolling manifests (beta versions)
511
*
612
* Override with `BAILIAN_CLI_CDN`.
713
*/
8-
export const DEFAULT_CLI_CDN_BASE = "https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli";
14+
export const DEFAULT_CLI_CDN_BASE = "https://bailian-wiki.oss-cn-hangzhou.aliyuncs.com/release";
915

1016
/** GitHub Releases base — used when writing manifests attached to gh release assets. */
1117
export const GITHUB_RELEASES_BASE = "https://github.com/modelstudioai/cli/releases";
@@ -20,16 +26,18 @@ export function getCliCdnBase(): string {
2026
}
2127

2228
/**
23-
* Channel manifest on OSS (after FC sync): `{base}/channels/{channel}.json`
24-
* Formal installs still read `channels/latest.json` on OSS; this repo no longer
25-
* attaches `latest.json` to GitHub Releases (FC / ops maintain OSS latest).
29+
* Rolling channel manifest at the CDN base root: `{base}/{channel}.json`.
30+
* `latest.json` (stable) and `<channel>.json` (betas) share the same shape;
31+
* both are written by CI on publish.
2632
*/
2733
export function channelManifestUrl(channel = "latest"): string {
28-
return `${getCliCdnBase()}/channels/${channel}.json`;
34+
return `${getCliCdnBase()}/${channel}.json`;
2935
}
3036

37+
/** Immutable per-version asset: `{base}/v{version}/{fileName}`. */
3138
export function releaseAssetUrl(version: string, fileName: string): string {
32-
return `${getCliCdnBase()}/releases/${version}/${fileName}`;
39+
const tag = version.startsWith("v") ? version : `v${version}`;
40+
return `${getCliCdnBase()}/${tag}/${fileName}`;
3341
}
3442

3543
/** Platform triple used in asset names: `bl-<ver>-<os>-<arch>[.exe]`. */

packages/runtime/src/utils/update-checker.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { join } from "path";
22
import { readFileSync, writeFileSync } from "fs";
3-
import { getConfigDir, trackingHeaders, getInstallMethod } from "bailian-cli-core";
3+
import {
4+
DEFAULT_INSTALL_SCRIPT_URL,
5+
getConfigDir,
6+
trackingHeaders,
7+
getInstallMethod,
8+
} from "bailian-cli-core";
49

510
export const NPM_REGISTRY = "https://registry.npmjs.org";
611
/** Default npm package; products override per-call via the `npmPackage` argument. */
@@ -257,7 +262,7 @@ export async function performAutoUpdate(
257262
} catch (err) {
258263
process.stderr.write(` ${yellow}⚠ Auto-update failed: ${errorMessage(err)}${reset}\n`);
259264
process.stderr.write(
260-
` ${yellow} Re-run:${reset} ${cyan}curl -fsSL https://bailian-cli.oss-cn-hangzhou.aliyuncs.com/bailian-cli/install.sh | bash${reset}\n\n`,
265+
` ${yellow} Re-run:${reset} ${cyan}curl -fsSL ${DEFAULT_INSTALL_SCRIPT_URL} | bash${reset}\n\n`,
261266
);
262267
return false;
263268
}

tools/release/lib/binary-build.mjs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
* node tools/release/lib/binary-build.mjs --mode stable --host
88
*
99
* Manifests:
10-
* --mode stable → no release manifest (latest.json removed)
11-
* --mode channel → writes <channel>.json only
10+
* --mode stable → writes latest.json (rolling manifest of the implicit
11+
* "latest" channel; OSS prefix root only, not a GH asset)
12+
* --mode channel → writes <channel>.json
1213
*/
1314
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
1415
import { join, resolve } from "node:path";
@@ -156,10 +157,7 @@ function writeChecksums(outdir, artifacts) {
156157
writeFileSync(join(outdir, "SHA256SUMS"), `${lines.join("\n")}\n`);
157158
}
158159

159-
/**
160-
* Channel-only: write `<channel>.json` with per-platform zip file + sha256.
161-
* Stable no longer emits latest.json.
162-
*/
160+
/** Write the rolling channel manifest (`latest.json` / `<channel>.json`) with per-platform zip file + sha256. */
163161
function writeChannelManifest(outdir, version, artifacts, channel) {
164162
const assets = Object.fromEntries(
165163
artifacts.map((item) => [
@@ -224,9 +222,9 @@ export function buildBinaryArtifacts(rawOptions = {}) {
224222
writeChecksums(outdir, artifacts);
225223

226224
const extras = ["SHA256SUMS"];
227-
if (mode === "channel") {
228-
extras.push(writeChannelManifest(outdir, version, artifacts, channel));
229-
}
225+
extras.push(
226+
writeChannelManifest(outdir, version, artifacts, mode === "channel" ? channel : "latest"),
227+
);
230228

231229
log(`\nBuilt ${artifacts.length} zip(s):`);
232230
for (const item of artifacts) {

tools/release/lib/binary-options.mjs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
22
* Shared mode / channel / manifest naming for binary-build and binary-release.
33
*
4-
* Stable releases no longer write `latest.json` (OSS `channels/latest.json` is
5-
* maintained outside this repo / by FC). Channel mode still writes `<channel>.json`
6-
* onto the rolling `channel-<name>` GitHub Release.
4+
* Every mode writes a rolling channel manifest: stable writes `latest.json`
5+
* (uploaded to the OSS prefix root only, never attached to the GitHub Release),
6+
* channel writes `<channel>.json` (attached to the rolling `channel-<name>`
7+
* GitHub Release and uploaded to the OSS prefix root).
78
*/
89
import { assertChannel } from "./validate.mjs";
910

@@ -27,7 +28,7 @@ export function normalizeModeChannel(mode = "stable", channel = null) {
2728
return { mode: "stable", channel: null };
2829
}
2930

30-
/** Channel rolling-manifest basename (`mcp.json`, …). Stable has none. */
31+
/** Rolling-manifest basename: `latest.json` for stable, `<channel>.json` otherwise. */
3132
export function channelManifestFileName(channel) {
3233
if (!channel) throw new Error("channelManifestFileName requires a channel name");
3334
return `${channel}.json`;

tools/release/lib/binary-release.mjs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,9 @@ function ossMirrorPlans({ dir, version, mode, channel, files }) {
138138
const plans = [{ tag: `v${version}`, paths }];
139139
if (mode === "channel") {
140140
const manifest = channelManifestFileName(channel);
141-
plans.push({ tag: `channel-${channel}`, paths: [files ? join(dir, manifest) : manifest] });
141+
// Rolling channel manifest lives at the OSS prefix root, next to
142+
// manifest.json / latest.json — one flat json per channel.
143+
plans.push({ tag: "", paths: [files ? join(dir, manifest) : manifest] });
142144
}
143145
return plans;
144146
}
@@ -182,6 +184,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
182184
await maintainReleaseManifest({
183185
tag: `v${version}`,
184186
assetNames: plans[0].paths.map((path) => basename(path)),
187+
channelJsonPath: null,
185188
dryRun: true,
186189
});
187190
}
@@ -198,13 +201,11 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
198201
if (!files.includes("SHA256SUMS")) {
199202
throw new Error(`Missing SHA256SUMS in ${dir}`);
200203
}
201-
if (mode === "channel") {
202-
const manifestName = channelManifestFileName(channel);
203-
if (!files.includes(manifestName)) {
204-
throw new Error(
205-
`Missing ${manifestName} in ${dir}. Rebuild with matching --mode/--channel (found: ${files.join(", ") || "(empty)"}).`,
206-
);
207-
}
204+
const rollingManifest = channelManifestFileName(mode === "channel" ? channel : "latest");
205+
if (!files.includes(rollingManifest)) {
206+
throw new Error(
207+
`Missing ${rollingManifest} in ${dir}. Rebuild with matching --mode/--channel (found: ${files.join(", ") || "(empty)"}).`,
208+
);
208209
}
209210

210211
process.stdout.write(`artifacts in ${dir}:\n`);
@@ -234,6 +235,7 @@ export async function releaseBinaryArtifacts(rawOptions = {}) {
234235
await maintainReleaseManifest({
235236
tag: `v${version}`,
236237
assetNames: plans[0].paths.map((path) => basename(path)),
238+
channelJsonPath: join(dir, rollingManifest),
237239
dryRun,
238240
});
239241
}

tools/release/lib/oss-direct-upload.mjs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
* No external FC is involved anymore; CI is the single writer.
55
*
66
* Flow:
7-
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>` (channel
8-
* builds include the rolling `channel-<name>/<name>.json`).
7+
* - Every mode uploads its assets to `<prefix>/<tag>/<basename>`; rolling
8+
* channel manifests (`<channel>.json`) go to the prefix root (empty tag).
99
* - After upload, every object is HEAD-verified against the local byte size
1010
* (reconciliation — the runner has the ground-truth artifacts on disk).
1111
* - Stable only: when the tag is a NEWER version than manifest.latest
12-
* (compareVersions), rewrite `<prefix>/manifest.json`. Channel/prerelease
13-
* never touches it — same semantics the FC sync-release used to enforce.
12+
* (compareVersions), rewrite `<prefix>/manifest.json` and the rolling
13+
* `<prefix>/latest.json`. Channel/prerelease never touches either.
1414
*
1515
* Zero-dependency: OSS V1 header signature (HMAC-SHA1) over plain fetch.
1616
*
@@ -132,12 +132,16 @@ function buildManifest(tag, releasedAt, assetNames, cfg) {
132132
* Signed OSS request (V1 header signature). Keys here are [A-Za-z0-9._/-] only,
133133
* so no URL encoding is needed and the signed resource matches the request path.
134134
*/
135-
async function ossRequest(method, key, { creds, cfg, body = null, contentType = "" }) {
135+
async function ossRequest(
136+
method,
137+
key,
138+
{ creds, cfg, body = null, contentType = "", extraHeaders = {} },
139+
) {
136140
const date = new Date().toUTCString();
137141
const contentMd5 = body ? createHash("md5").update(body).digest("base64") : "";
138142
const canonical = `${method}\n${contentMd5}\n${contentType}\n${date}\n/${cfg.bucket}/${key}`;
139143
const signature = createHmac("sha1", creds.sk).update(canonical).digest("base64");
140-
const headers = { Date: date, Authorization: `OSS ${creds.ak}:${signature}` };
144+
const headers = { Date: date, Authorization: `OSS ${creds.ak}:${signature}`, ...extraHeaders };
141145
if (contentType) headers["Content-Type"] = contentType;
142146
if (contentMd5) headers["Content-MD5"] = contentMd5;
143147
const options = { method, headers };
@@ -169,12 +173,23 @@ async function putWithRetry(params, attempts = 3) {
169173
}
170174
}
171175

172-
/** Remote object byte size; null when the object does not exist. */
176+
/**
177+
* Remote object byte size; null when the object does not exist.
178+
* Forces the identity encoding: for compressible types (e.g. JSON) OSS gzips
179+
* the transfer and undici then strips the content-length header, which would
180+
* otherwise read as a bogus size 0 here.
181+
*/
173182
async function headObjectSize(key, creds, cfg) {
174-
const res = await ossRequest("HEAD", key, { creds, cfg });
183+
const res = await ossRequest("HEAD", key, {
184+
creds,
185+
cfg,
186+
extraHeaders: { "Accept-Encoding": "identity" },
187+
});
175188
if (res.status === 404) return null;
176189
if (!res.ok) throw new Error(`OSS HEAD ${key} failed: HTTP ${res.status}`);
177-
return Number(res.headers.get("content-length"));
190+
const length = res.headers.get("content-length");
191+
if (length == null) throw new Error(`OSS HEAD ${key} returned no content-length`);
192+
return Number(length);
178193
}
179194

180195
/** GET + parse a JSON object; null when missing or corrupt. */
@@ -210,8 +225,12 @@ export async function mirrorReleaseAssetsToOss({ plans, dryRun = false }) {
210225
}
211226
const { creds, cfg } = ctx;
212227

228+
// An empty tag means the object lives at the prefix root (rolling manifests).
213229
const jobs = plans.flatMap(({ tag, paths }) =>
214-
paths.map((path) => ({ path, key: `${cfg.prefix}/${tag}/${basename(path)}` })),
230+
paths.map((path) => ({
231+
path,
232+
key: [cfg.prefix, tag, basename(path)].filter(Boolean).join("/"),
233+
})),
215234
);
216235
if (jobs.length === 0) return { uploaded: 0, skipped: true };
217236

@@ -273,14 +292,24 @@ export async function mirrorReleaseAssetsToOss({ plans, dryRun = false }) {
273292
}
274293

275294
/**
276-
* Maintain `<prefix>/manifest.json` for STABLE releases only: rewrite it when
277-
* `tag` is a newer version than the current `latest` (first write included).
278-
* Same update rule the FC sync-release used to apply.
295+
* Maintain the STABLE pointers at the prefix root: rewrite `manifest.json`
296+
* (and, when `channelJsonPath` is given, the rolling `latest.json`) when `tag`
297+
* is a newer version than the current `latest` (first write included).
279298
*
280-
* @param {{ tag: string, assetNames: string[], dryRun?: boolean }} options
299+
* @param {{
300+
* tag: string,
301+
* assetNames: string[],
302+
* channelJsonPath?: string | null,
303+
* dryRun?: boolean,
304+
* }} options
281305
* @returns {Promise<{ updated: boolean, latest: string | null }>}
282306
*/
283-
export async function maintainReleaseManifest({ tag, assetNames, dryRun = false }) {
307+
export async function maintainReleaseManifest({
308+
tag,
309+
assetNames,
310+
channelJsonPath = null,
311+
dryRun = false,
312+
}) {
284313
const ctx = ossContext();
285314
if (!ctx) {
286315
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
291320

292321
if (dryRun) {
293322
process.stdout.write(
294-
`[dry-run] manifest: GET oss://${cfg.bucket}/${key} → rewrite when ${tag} > latest (assets: ${assetNames.length})\n`,
323+
`[dry-run] manifest: GET oss://${cfg.bucket}/${key} → rewrite manifest.json + latest.json when ${tag} > latest (assets: ${assetNames.length})\n`,
295324
);
296325
return { updated: false, latest: null };
297326
}
@@ -313,5 +342,15 @@ export async function maintainReleaseManifest({ tag, assetNames, dryRun = false
313342
contentType: "application/json",
314343
});
315344
process.stdout.write(`manifest.json → latest=${tag} (was ${currentLatest ?? "none"})\n`);
345+
if (channelJsonPath) {
346+
await putObject({
347+
creds,
348+
cfg,
349+
key: `${cfg.prefix}/latest.json`,
350+
body: readFileSync(channelJsonPath),
351+
contentType: "application/json",
352+
});
353+
process.stdout.write(`latest.json → ${tag}\n`);
354+
}
316355
return { updated: true, latest: tag };
317356
}

tools/release/lib/validate.mjs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,16 @@ export function loadAndValidatePackages({ packages } = {}) {
5454
return { coreJson, cliJson };
5555
}
5656

57-
const RESERVED_CHANNELS = new Set(["latest", "beta", "alpha", "next", "rc", "canary", "dev"]);
57+
const RESERVED_CHANNELS = new Set([
58+
"latest",
59+
"manifest", // would collide with manifest.json at the OSS prefix root
60+
"beta",
61+
"alpha",
62+
"next",
63+
"rc",
64+
"canary",
65+
"dev",
66+
]);
5867
const CHANNEL_FORMAT = /^[a-z][a-z0-9-]{1,30}$/;
5968

6069
export function assertChannel(channel) {

0 commit comments

Comments
 (0)