mirror of
https://gh-proxy.org/https://github.com/klinsmaya/open-connector.git
synced 2026-11-08 01:16:52 +08:00
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
/**
|
|
* Creates or publishes the GitHub release for a tag, idempotently, through the
|
|
* `gh` CLI (which reads `GH_TOKEN` and `GH_REPO`).
|
|
*
|
|
* node scripts/release/github-release.ts ensure-draft
|
|
* node scripts/release/github-release.ts publish
|
|
*
|
|
* `ensure-draft` leaves a draft release for `RELEASE_TAG` targeting
|
|
* `TARGET_SHA` in place: it reuses a matching draft, replaces a draft that
|
|
* targets another commit (drafts own no tag, so this is safe), and accepts an
|
|
* already published release whose tag points at `TARGET_SHA`. The tag itself
|
|
* is created by GitHub when `publish` flips the draft to a published release,
|
|
* so nothing is visible to users until every asset is attached.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
interface CommandResult {
|
|
status: number;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
interface ExistingRelease {
|
|
isDraft: boolean;
|
|
targetCommitish: string;
|
|
}
|
|
|
|
function gh(args: readonly string[]): CommandResult {
|
|
const result = spawnSync("gh", args, { encoding: "utf8" });
|
|
if (result.error) throw result.error;
|
|
return { status: result.status ?? 1, stdout: result.stdout, stderr: result.stderr };
|
|
}
|
|
|
|
function ghOrThrow(args: readonly string[]): string {
|
|
const result = gh(args);
|
|
if (result.status !== 0) {
|
|
throw new Error(`gh ${args.join(" ")} failed (exit ${result.status}):\n${result.stderr}${result.stdout}`);
|
|
}
|
|
return result.stdout;
|
|
}
|
|
|
|
function requiredEnv(name: string): string {
|
|
const value = process.env[name];
|
|
if (value === undefined || value === "") throw new Error(`${name} is required.`);
|
|
return value;
|
|
}
|
|
|
|
function findRelease(tag: string): ExistingRelease | undefined {
|
|
const result = gh(["release", "view", tag, "--json", "isDraft,targetCommitish"]);
|
|
if (result.status === 0) return JSON.parse(result.stdout) as ExistingRelease;
|
|
if (result.stderr.includes("release not found")) return undefined;
|
|
throw new Error(`gh release view ${tag} failed:\n${result.stderr}`);
|
|
}
|
|
|
|
function tagCommit(tag: string): string {
|
|
return ghOrThrow(["api", `repos/{owner}/{repo}/commits/${encodeURIComponent(tag)}`, "--jq", ".sha"]).trim();
|
|
}
|
|
|
|
function ensureDraft(): void {
|
|
const tag = requiredEnv("RELEASE_TAG");
|
|
const target = requiredEnv("TARGET_SHA");
|
|
const previousTag = process.env.PREVIOUS_TAG ?? "";
|
|
const existing = findRelease(tag);
|
|
|
|
if (existing !== undefined && !existing.isDraft) {
|
|
const commit = tagCommit(tag);
|
|
if (commit !== target) {
|
|
throw new Error(`Release ${tag} is already published from ${commit}, not from ${target}.`);
|
|
}
|
|
console.log(`Release ${tag} is already published from ${target}; nothing to do.`);
|
|
return;
|
|
}
|
|
|
|
if (existing !== undefined) {
|
|
if (existing.targetCommitish === target) {
|
|
console.log(`Reusing draft release ${tag} targeting ${target}.`);
|
|
return;
|
|
}
|
|
console.log(`Draft release ${tag} targets ${existing.targetCommitish}; replacing it with a draft for ${target}.`);
|
|
ghOrThrow(["release", "delete", tag, "--yes"]);
|
|
}
|
|
|
|
const args = ["release", "create", tag, "--draft", "--target", target, "--title", tag, "--generate-notes"];
|
|
if (previousTag !== "") args.push("--notes-start-tag", previousTag);
|
|
ghOrThrow(args);
|
|
console.log(`Created draft release ${tag} targeting ${target}.`);
|
|
}
|
|
|
|
function publish(): void {
|
|
const tag = requiredEnv("RELEASE_TAG");
|
|
const target = requiredEnv("TARGET_SHA");
|
|
const existing = findRelease(tag);
|
|
if (existing === undefined) throw new Error(`Release ${tag} does not exist; run ensure-draft first.`);
|
|
|
|
// The assets were built from TARGET_SHA. Re-check the release still points
|
|
// there: the draft could have been edited while the artifact jobs ran, and
|
|
// publishing it would tag a commit the assets do not belong to.
|
|
const commit = existing.isDraft ? existing.targetCommitish : tagCommit(tag);
|
|
if (commit !== target) {
|
|
throw new Error(
|
|
`Release ${tag} ${existing.isDraft ? "targets" : "is published from"} ${commit}, not ${target}; refusing to publish.`,
|
|
);
|
|
}
|
|
if (!existing.isDraft) {
|
|
console.log(`Release ${tag} is already published.`);
|
|
}
|
|
// Publishing creates the tag at the draft's target commit. `--latest` is
|
|
// idempotent and keeps `releases/latest/download/...` pointing here.
|
|
ghOrThrow(["release", "edit", tag, "--draft=false", "--latest"]);
|
|
console.log(`Published release ${tag}.`);
|
|
}
|
|
|
|
const command = process.argv[2];
|
|
switch (command) {
|
|
case "ensure-draft":
|
|
ensureDraft();
|
|
break;
|
|
case "publish":
|
|
publish();
|
|
break;
|
|
default:
|
|
throw new Error(`Usage: github-release.ts <ensure-draft|publish> (got "${command ?? ""}")`);
|
|
}
|