feat(release): add draft GitHub release flow
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const rootPackagePath = path.join(rootDir, "package.json");
|
||||
|
||||
function usageAndExit(code = 0) {
|
||||
process.stderr.write(`Usage: node scripts/finalize-current-release.mjs\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
||||
}
|
||||
|
||||
function runQuiet(cmd, args) {
|
||||
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
function parseRepoSlug(remoteUrl) {
|
||||
const trimmed = remoteUrl.trim().replace(/\.git$/, "");
|
||||
const httpsMatch = trimmed.match(/github\.com[/:]([^/]+\/[^/]+)$/);
|
||||
if (httpsMatch) {
|
||||
return httpsMatch[1];
|
||||
}
|
||||
throw new Error(`Could not determine GitHub repo from origin URL: ${remoteUrl}`);
|
||||
}
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
const arg = process.argv[2];
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
usageAndExit(0);
|
||||
}
|
||||
usageAndExit(1);
|
||||
}
|
||||
|
||||
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
||||
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error('Root package.json must contain a valid "version"');
|
||||
}
|
||||
|
||||
const tag = `v${version}`;
|
||||
const repo = parseRepoSlug(runQuiet("git", ["remote", "get-url", "origin"]));
|
||||
const releaseJson = runQuiet("gh", [
|
||||
"release",
|
||||
"view",
|
||||
tag,
|
||||
"--repo",
|
||||
repo,
|
||||
"--json",
|
||||
"isDraft,tagName,url",
|
||||
]);
|
||||
const release = JSON.parse(releaseJson);
|
||||
|
||||
if (!release.isDraft) {
|
||||
throw new Error(`Release ${tag} is already published. Refusing to finalize a non-draft release.`);
|
||||
}
|
||||
|
||||
run("npm", ["run", "release:publish"]);
|
||||
run("gh", ["release", "edit", tag, "--repo", repo, "--draft=false"]);
|
||||
|
||||
console.log(`Finalized release ${tag}: npm packages published and GitHub draft release promoted.`);
|
||||
@@ -7,6 +7,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
const rootPackagePath = path.join(rootDir, "package.json");
|
||||
|
||||
function usageAndExit(code = 0) {
|
||||
process.stderr.write(`Usage: node scripts/push-current-release-tag.mjs [--draft-release]\n`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function run(cmd, args) {
|
||||
execFileSync(cmd, args, { cwd: rootDir, stdio: "inherit" });
|
||||
}
|
||||
@@ -15,12 +20,39 @@ function runQuiet(cmd, args) {
|
||||
return execFileSync(cmd, args, { cwd: rootDir, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { draftRelease: false };
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg === "--draft-release") {
|
||||
args.draftRelease = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
usageAndExit(0);
|
||||
}
|
||||
usageAndExit(1);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function parseRepoSlug(remoteUrl) {
|
||||
const trimmed = remoteUrl.trim().replace(/\.git$/, "");
|
||||
const httpsMatch = trimmed.match(/github\.com[/:]([^/]+\/[^/]+)$/);
|
||||
if (httpsMatch) {
|
||||
return httpsMatch[1];
|
||||
}
|
||||
throw new Error(`Could not determine GitHub repo from origin URL: ${remoteUrl}`);
|
||||
}
|
||||
|
||||
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
|
||||
const version = typeof rootPackage.version === "string" ? rootPackage.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error('Root package.json must contain a valid "version"');
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const tag = `v${version}`;
|
||||
const headCommit = runQuiet("git", ["rev-parse", "HEAD"]);
|
||||
|
||||
@@ -55,4 +87,17 @@ if (remoteTagExists) {
|
||||
run("git", ["push", "origin", tag]);
|
||||
}
|
||||
|
||||
if (args.draftRelease) {
|
||||
const repo = parseRepoSlug(runQuiet("git", ["remote", "get-url", "origin"]));
|
||||
run("node", [
|
||||
path.join("scripts", "sync-release-notes-from-changelog.mjs"),
|
||||
"--repo",
|
||||
repo,
|
||||
"--tag",
|
||||
tag,
|
||||
"--create-if-missing",
|
||||
"--draft",
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(`Release push complete: branch HEAD and tag ${tag}`);
|
||||
|
||||
@@ -13,6 +13,7 @@ Options:
|
||||
--repo <owner/repo> Repository slug. Defaults to $GITHUB_REPOSITORY.
|
||||
--tag <tag> Release tag (e.g. v0.1.14). Defaults to latest changelog entry.
|
||||
--create-if-missing Create release if it does not already exist.
|
||||
--draft Create missing release as draft.
|
||||
`;
|
||||
process.stderr.write(usage.trimStart());
|
||||
process.stderr.write("\n");
|
||||
@@ -24,6 +25,7 @@ function parseArgs(argv) {
|
||||
repo: process.env.GITHUB_REPOSITORY || "",
|
||||
tag: "",
|
||||
createIfMissing: false,
|
||||
draft: false,
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
@@ -53,6 +55,11 @@ function parseArgs(argv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--draft") {
|
||||
args.draft = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
usageAndExit();
|
||||
}
|
||||
|
||||
@@ -186,6 +193,7 @@ function main() {
|
||||
"--notes-file",
|
||||
notesPath,
|
||||
"--verify-tag",
|
||||
...(args.draft ? ["--draft"] : []),
|
||||
]);
|
||||
console.log(`Created release ${targetTag} with changelog notes.`);
|
||||
} catch (createError) {
|
||||
|
||||
Reference in New Issue
Block a user