From 522e0dddf3909655f7fcdd92b20daea8ef9df8ea Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 17 Jul 2026 00:29:28 +0800 Subject: [PATCH] fix(release): promote CNB releases after package publishing --- .github/scripts/sync-cnb-release.mjs | 15 +++- .github/scripts/sync-cnb-release.test.mjs | 104 +++++++++++++++++++--- .github/workflows/publish-packages.yml | 21 +++++ 3 files changed, 126 insertions(+), 14 deletions(-) diff --git a/.github/scripts/sync-cnb-release.mjs b/.github/scripts/sync-cnb-release.mjs index 10893ac87..d11067731 100644 --- a/.github/scripts/sync-cnb-release.mjs +++ b/.github/scripts/sync-cnb-release.mjs @@ -17,6 +17,12 @@ async function main() { const client = new CnbClient(args); const release = await client.ensureRelease(tag, githubRelease); + + if (args.metadataOnly) { + console.log(`Updated CNB release metadata for ${tag}.`); + return; + } + const existingAssets = new Set((release.assets || []).map((asset) => asset.name)); const assets = localAssets(args.assetsDir).filter((assetPath) => { const name = basename(assetPath); @@ -42,17 +48,21 @@ function parseArgs(argv) { overwriteExisting: false, githubReleasePath: "", assetsDir: "", + metadataOnly: false, }; for (let index = 0; index < argv.length; index++) { const arg = argv[index]; if (arg === "--github-release") args.githubReleasePath = argv[++index]; else if (arg === "--assets-dir") args.assetsDir = argv[++index]; + else if (arg === "--metadata-only") args.metadataOnly = true; else if (arg === "--overwrite-existing") args.overwriteExisting = true; else throw new Error(`Unknown argument: ${arg}`); } if (!args.token) throw new Error("CNB_TOKEN is required."); - if (!args.githubReleasePath || !args.assetsDir) { - throw new Error("Usage: sync-cnb-release.mjs --github-release --assets-dir "); + if (!args.githubReleasePath || (!args.metadataOnly && !args.assetsDir)) { + throw new Error( + "Usage: sync-cnb-release.mjs --github-release (--assets-dir | --metadata-only)", + ); } if (!Number.isInteger(args.concurrency) || args.concurrency < 1) { throw new Error("CNB_UPLOAD_CONCURRENCY must be a positive integer."); @@ -99,6 +109,7 @@ export class CnbClient { await this.request("PATCH", `/${this.repository}/-/releases/${existing.id}`, { name: payload.name, body: payload.body, + prerelease: payload.prerelease, }); return existing; } diff --git a/.github/scripts/sync-cnb-release.test.mjs b/.github/scripts/sync-cnb-release.test.mjs index 9d2a2d799..892786f49 100644 --- a/.github/scripts/sync-cnb-release.test.mjs +++ b/.github/scripts/sync-cnb-release.test.mjs @@ -1,12 +1,78 @@ import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import test from "node:test"; import { CnbClient } from "./sync-cnb-release.mjs"; +const scriptPath = fileURLToPath(new URL("./sync-cnb-release.mjs", import.meta.url)); + test("ensureRelease accepts an empty successful PATCH response", async (t) => { const release = { id: "release-1", tag_name: "agents-latest", 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; + } + if (request.method === "PATCH") { + response.statusCode = 200; + response.end(); + return; + } + response.statusCode = 500; + 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", + }); + + const result = await client.ensureRelease("agents-latest", { + name: "Latest agents", + body: "Latest stable agent release", + isPrerelease: false, + }); + + assert.deepEqual(result, release); + assert.deepEqual(requests, [ + { method: "GET", url: "/dbxio.com/dbx/-/releases/tags/agents-latest", body: null }, + { + method: "PATCH", + url: "/dbxio.com/dbx/-/releases/release-1", + body: { + name: "Latest agents", + body: "Latest stable agent release", + prerelease: false, + }, + }, + ]); +}); + +test("metadata-only sync updates release 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") { @@ -26,22 +92,36 @@ test("ensureRelease accepts an empty successful PATCH response", async (t) => { 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 tempDir = await mkdtemp(join(tmpdir(), "sync-cnb-release-")); + t.after(() => rm(tempDir, { recursive: true, force: true })); + const releasePath = join(tempDir, "release.json"); + await writeFile( + releasePath, + JSON.stringify({ tagName: "v1.2.3", name: "DBX v1.2.3", body: "Stable", isPrerelease: false }), + ); + 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", + 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", + }, + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => (stdout += chunk)); + child.stderr.on("data", (chunk) => (stderr += chunk)); + child.on("close", (code) => resolve({ code, stdout, stderr })); }); - const result = await client.ensureRelease("agents-latest", { - name: "Latest agents", - body: "Latest stable agent release", - }); - - assert.deepEqual(result, release); + 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/agents-latest" }, - { method: "PATCH", url: "/dbxio.com/dbx/-/releases/release-1" }, + { method: "GET", url: "/dbxio.com/dbx/-/releases/tags/v1.2.3" }, + { method: "PATCH", url: "/dbxio.com/dbx/-/releases/release-2" }, ]); }); diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 61c210369..1a0d58062 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -365,6 +365,9 @@ jobs: needs: [publish, upload-r2] runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v5 + - name: Promote prerelease to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -376,6 +379,24 @@ jobs: --latest echo "::notice::Release ${TAG} promoted from prerelease to latest release" + - name: Promote CNB prerelease to release + env: + CNB_TOKEN: ${{ secrets.CNB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/github-release" + gh release view "$TAG" \ + --repo "${{ github.repository }}" \ + --json tagName,name,body,targetCommitish,isPrerelease,isDraft \ + > "$RUNNER_TEMP/github-release/release.json" + + # 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 + notify-qq: name: QQ Notify needs: [promote-release]