From 0f57326ac6be7cb27f840456f048974a05159fcf Mon Sep 17 00:00:00 2001 From: whackur Date: Fri, 4 Sep 2026 17:33:51 +0900 Subject: [PATCH] fix(release): find draft releases through the releases list --- scripts/publish-release.mjs | 33 +++++++++++++++++++++----------- scripts/publish-release.test.mjs | 28 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/scripts/publish-release.mjs b/scripts/publish-release.mjs index 18c42c4d..89ad7181 100644 --- a/scripts/publish-release.mjs +++ b/scripts/publish-release.mjs @@ -97,18 +97,29 @@ function remoteTagSha(remote, tag) { return (peeled || lines[0]).split(/\s+/)[0]; } -function fetchRelease(repo, tag) { - const result = spawnSync("gh", ["api", `repos/${repo}/releases/tags/${tag}`], { encoding: "utf8" }); - if (result.status === 0) { - try { - return JSON.parse(result.stdout); - } catch { - throw new Error("GitHub Release lookup returned invalid JSON"); - } +function ghJson(args) { + const result = spawnSync("gh", ["api", ...args], { encoding: "utf8" }); + if (result.status !== 0) { + const detail = `${result.stderr || ""} ${result.stdout || ""}`; + if (/\bHTTP 404\b|404 Not Found|Not Found \(HTTP 404\)/i.test(detail)) return null; + throw new Error("GitHub Release lookup failed; authentication or network errors fail closed"); } - const detail = `${result.stderr || ""} ${result.stdout || ""}`; - if (/\bHTTP 404\b|404 Not Found|Not Found \(HTTP 404\)/i.test(detail)) return null; - throw new Error("GitHub Release lookup failed; authentication or network errors fail closed"); + try { + return JSON.parse(result.stdout); + } catch { + throw new Error("GitHub Release lookup returned invalid JSON"); + } +} + +export function fetchRelease(repo, tag, lookup = ghJson) { + // The tag endpoint never serves drafts, so a 404 must fall back to the releases + // list, which includes drafts and is ordered newest first. + const direct = lookup([`repos/${repo}/releases/tags/${tag}`]); + if (direct) return direct; + const list = lookup([`repos/${repo}/releases?per_page=100`]); + if (list === null) return null; + if (!Array.isArray(list)) throw new Error("GitHub Release listing returned an unexpected payload"); + return list.find((candidate) => candidate.tag_name === tag) || null; } function uploadMissing(repo, tag, dist, missing) { diff --git a/scripts/publish-release.test.mjs b/scripts/publish-release.test.mjs index 9fa539e9..5967ff50 100644 --- a/scripts/publish-release.test.mjs +++ b/scripts/publish-release.test.mjs @@ -6,6 +6,7 @@ import path from "node:path"; import { spawnSync } from "node:child_process"; import { compareReleaseAssets, + fetchRelease, localAssetInfo, parseCliArgs, verifyBinaryAssets, @@ -92,3 +93,30 @@ test("publish CLI accepts separated values and the entrypoint reaches validation fs.rmSync(root, { recursive: true, force: true }); } }); + +test("fetchRelease falls back to the releases list because drafts are invisible to the tag endpoint", () => { + const calls = []; + const draft = { id: 1, tag_name: "v0.1.1", draft: true, assets: [] }; + const lookup = (args) => { + calls.push(args[0]); + return args[0].endsWith("/releases/tags/v0.1.1") ? null : [draft, { id: 2, tag_name: "v0.1.2", draft: true, assets: [] }]; + }; + assert.equal(fetchRelease("code0xff/nightcrow", "v0.1.1", lookup), draft); + assert.deepEqual(calls, ["repos/code0xff/nightcrow/releases/tags/v0.1.1", "repos/code0xff/nightcrow/releases?per_page=100"]); +}); + +test("fetchRelease returns null when no release exists anywhere", () => { + const lookup = () => null; + assert.equal(fetchRelease("code0xff/nightcrow", "v0.1.1", lookup), null); +}); + +test("fetchRelease returns the published release served by the tag endpoint", () => { + const published = { id: 3, tag_name: "v0.1.1", draft: false, assets: [] }; + const lookup = (args) => (args[0].endsWith("/releases/tags/v0.1.1") ? published : [published]); + assert.equal(fetchRelease("code0xff/nightcrow", "v0.1.1", lookup), published); +}); + +test("fetchRelease rejects a non-array listing payload", () => { + const lookup = (args) => (args[0].endsWith("/releases/tags/v0.1.1") ? null : { not: "an array" }); + assert.throws(() => fetchRelease("code0xff/nightcrow", "v0.1.1", lookup), /unexpected payload/); +});