import { spawnSync } from "node:child_process"; import { pathToFileURL } from "node:url"; import conventionalCommits from "conventional-changelog-conventionalcommits"; import { CommitParser } from "conventional-commits-parser"; const releaseNoteTypes = new Set(["feat", "fix", "perf"]); const parser = new CommitParser(conventionalCommits().parser); function fail(message) { throw new Error(`release notes generation failed: ${message}`); } function runGit(args, cwd) { const result = spawnSync("git", args, { cwd, encoding: "utf8", shell: false, }); if (result.error) fail(result.error.message); if (result.status !== 0) { fail(`${args.join(" ")} exited with ${result.status}: ${result.stderr.trim()}`); } return result.stdout; } function formatSubject(subject) { const sentence = subject.replace(/^([a-z])/, (letter) => letter.toUpperCase()); return /[.!?]$/.test(sentence) ? sentence : `${sentence}.`; } export function releaseNoteForCommit(message) { const commit = parser.parse(message); if (!commit.subject) return null; const isBreaking = commit.notes.length > 0; if (!isBreaking && !releaseNoteTypes.has(commit.type ?? "")) return null; return formatSubject(commit.subject); } export function renderReleaseNotes(messages) { const entries = messages.map(releaseNoteForCommit).filter((entry) => entry !== null); const bullets = entries.length > 0 ? entries.map((entry) => `- ${entry}`).join("\n") : "- "; return `## Changes\n\n${bullets}\n`; } export function readCommitMessagesSince(tag, cwd = process.cwd()) { runGit(["rev-parse", "--verify", `refs/tags/${tag}`], cwd); const output = runGit(["log", "--format=%B%x00", `${tag}..HEAD`], cwd); return output .split("\0") .map((message) => message.trim()) .filter(Boolean); } export function generateReleaseNotes(tag, cwd = process.cwd()) { return renderReleaseNotes(readCommitMessagesSince(tag, cwd)); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const previousTag = process.argv[2]; if (!previousTag) { console.error("usage: npm run release:notes -- X.Y.Z"); process.exit(1); } try { process.stdout.write(generateReleaseNotes(previousTag)); } catch (error) { console.error(error instanceof Error ? error.message : error); process.exit(1); } }