From cd10acb3104ca42796e7f2f691fc98b8f3d9b056 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 30 Jul 2026 09:45:29 +0800 Subject: [PATCH] fix(release): mark promoted CNB release as latest --- .github/scripts/sync-cnb-release.mjs | 10 +- .github/scripts/sync-cnb-release.test.mjs | 119 ++++++++++++++++++---- .github/workflows/publish-packages.yml | 3 +- 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/.github/scripts/sync-cnb-release.mjs b/.github/scripts/sync-cnb-release.mjs index d4bcc1fbb..6b94f2933 100644 --- a/.github/scripts/sync-cnb-release.mjs +++ b/.github/scripts/sync-cnb-release.mjs @@ -16,7 +16,7 @@ async function main() { if (!tag) throw new Error("GitHub release JSON is missing tagName."); const client = new CnbClient(args); - const release = await client.ensureRelease(tag, githubRelease); + const release = await client.ensureRelease(tag, githubRelease, { makeLatest: args.makeLatest }); if (args.metadataOnly) { console.log(`Updated CNB release metadata for ${tag}.`); @@ -56,6 +56,7 @@ function parseArgs(argv) { assetsDir: "", metadataOnly: false, pruneAssets: false, + makeLatest: false, }; for (let index = 0; index < argv.length; index++) { const arg = argv[index]; @@ -64,12 +65,13 @@ function parseArgs(argv) { else if (arg === "--metadata-only") args.metadataOnly = true; else if (arg === "--overwrite-existing") args.overwriteExisting = true; else if (arg === "--prune-assets") args.pruneAssets = true; + else if (arg === "--make-latest") args.makeLatest = true; else throw new Error(`Unknown argument: ${arg}`); } if (!args.token) throw new Error("CNB_TOKEN is required."); if (!args.githubReleasePath || (!args.metadataOnly && !args.assetsDir)) { throw new Error( - "Usage: sync-cnb-release.mjs --github-release (--assets-dir | --metadata-only) [--prune-assets]", + "Usage: sync-cnb-release.mjs --github-release (--assets-dir | --metadata-only) [--prune-assets] [--make-latest]", ); } if (!Number.isInteger(args.concurrency) || args.concurrency < 1) { @@ -112,13 +114,14 @@ export class CnbClient { this.token = token; } - async ensureRelease(tag, githubRelease) { + async ensureRelease(tag, githubRelease, { makeLatest = false } = {}) { const payload = { tag_name: tag, name: githubRelease.name || tag, body: githubRelease.body || "", prerelease: Boolean(githubRelease.isPrerelease || githubRelease.prerelease), target_commitish: githubRelease.targetCommitish || githubRelease.target_commitish || "", + ...(makeLatest ? { make_latest: "true" } : {}), }; const existing = await this.request("GET", `/${this.repository}/-/releases/tags/${encodeURIComponent(tag)}`, null, true); if (!existing) return this.request("POST", `/${this.repository}/-/releases`, payload); @@ -128,6 +131,7 @@ export class CnbClient { name: payload.name, body: payload.body, prerelease: payload.prerelease, + ...(makeLatest ? { make_latest: "true" } : {}), }); return existing; } diff --git a/.github/scripts/sync-cnb-release.test.mjs b/.github/scripts/sync-cnb-release.test.mjs index 2a2609330..80d94a775 100644 --- a/.github/scripts/sync-cnb-release.test.mjs +++ b/.github/scripts/sync-cnb-release.test.mjs @@ -70,23 +70,85 @@ test("ensureRelease accepts an empty successful PATCH response", async (t) => { ]); }); -test("metadata-only sync updates release without reading an assets directory", async (t) => { +test("ensureRelease explicitly marks a promoted release as latest", async (t) => { + const release = { id: "release-latest", tag_name: "v1.2.3", assets: [] }; + const requests = []; + const server = createServer((request, response) => { + const bodyChunks = []; + request.on("data", (chunk) => bodyChunks.push(chunk)); + request.on("end", () => { + requests.push({ + method: request.method, + url: request.url, + body: bodyChunks.length ? JSON.parse(Buffer.concat(bodyChunks).toString("utf8")) : null, + }); + if (request.method === "GET") { + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify(release)); + return; + } + response.statusCode = 200; + response.end(); + }); + }); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + t.after(() => new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))); + + const address = server.address(); + assert.ok(address && typeof address !== "string"); + const client = new CnbClient({ + apiBase: `http://127.0.0.1:${address.port}`, + repository: "dbxio.com/dbx", + token: "test-token", + }); + + await client.ensureRelease( + "v1.2.3", + { name: "DBX v1.2.3", body: "Stable", isPrerelease: false }, + { makeLatest: true }, + ); + + assert.deepEqual(requests, [ + { method: "GET", url: "/dbxio.com/dbx/-/releases/tags/v1.2.3", body: null }, + { + method: "PATCH", + url: "/dbxio.com/dbx/-/releases/release-latest", + body: { + name: "DBX v1.2.3", + body: "Stable", + prerelease: false, + make_latest: "true", + }, + }, + ]); +}); + +test("metadata-only sync can mark the release as latest without reading an assets directory", async (t) => { const release = { id: "release-2", tag_name: "v1.2.3", assets: [{ name: "large.dmg" }] }; const requests = []; const server = createServer((request, response) => { - requests.push({ method: request.method, url: request.url }); - if (request.method === "GET") { - response.setHeader("Content-Type", "application/json"); - response.end(JSON.stringify(release)); - return; - } - if (request.method === "PATCH") { - response.statusCode = 200; + const bodyChunks = []; + request.on("data", (chunk) => bodyChunks.push(chunk)); + request.on("end", () => { + requests.push({ + method: request.method, + url: request.url, + body: bodyChunks.length ? JSON.parse(Buffer.concat(bodyChunks).toString("utf8")) : null, + }); + if (request.method === "GET") { + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify(release)); + return; + } + if (request.method === "PATCH") { + response.statusCode = 200; + response.end(); + return; + } + response.statusCode = 500; response.end(); - return; - } - response.statusCode = 500; - response.end(); + }); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); @@ -103,14 +165,18 @@ test("metadata-only sync updates release without reading an assets directory", a const address = server.address(); assert.ok(address && typeof address !== "string"); const result = await new Promise((resolve) => { - const child = spawn(process.execPath, [scriptPath, "--github-release", releasePath, "--metadata-only"], { - env: { - ...process.env, - CNB_API_BASE: `http://127.0.0.1:${address.port}`, - CNB_REPOSITORY: "dbxio.com/dbx", - CNB_TOKEN: "test-token", + const child = spawn( + process.execPath, + [scriptPath, "--github-release", releasePath, "--metadata-only", "--make-latest"], + { + env: { + ...process.env, + CNB_API_BASE: `http://127.0.0.1:${address.port}`, + CNB_REPOSITORY: "dbxio.com/dbx", + CNB_TOKEN: "test-token", + }, }, - }); + ); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => (stdout += chunk)); @@ -121,8 +187,17 @@ test("metadata-only sync updates release without reading an assets directory", a assert.equal(result.code, 0, result.stderr); assert.match(result.stdout, /Updated CNB release metadata for v1\.2\.3/); assert.deepEqual(requests, [ - { method: "GET", url: "/dbxio.com/dbx/-/releases/tags/v1.2.3" }, - { method: "PATCH", url: "/dbxio.com/dbx/-/releases/release-2" }, + { method: "GET", url: "/dbxio.com/dbx/-/releases/tags/v1.2.3", body: null }, + { + method: "PATCH", + url: "/dbxio.com/dbx/-/releases/release-2", + body: { + name: "DBX v1.2.3", + body: "Stable", + prerelease: false, + make_latest: "true", + }, + }, ]); }); diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 89962e2b2..8fbc81677 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -400,7 +400,8 @@ jobs: # Sync metadata only so promoting a release never re-uploads large CNB assets. node .github/scripts/sync-cnb-release.mjs \ --github-release "$RUNNER_TEMP/github-release/release.json" \ - --metadata-only + --metadata-only \ + --make-latest notify-qq: name: QQ Notify