feat(release): add draft GitHub release flow

This commit is contained in:
Mohamed Boudra
2026-03-09 11:41:21 +07:00
parent 06f8722f25
commit 8a60dc30d6
8 changed files with 195 additions and 3 deletions
+66
View File
@@ -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.`);
+45
View File
@@ -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) {