Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions scripts/publish-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
28 changes: 28 additions & 0 deletions scripts/publish-release.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import path from "node:path";
import { spawnSync } from "node:child_process";
import {
compareReleaseAssets,
fetchRelease,
localAssetInfo,
parseCliArgs,
verifyBinaryAssets,
Expand Down Expand Up @@ -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/);
});
Loading