Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
670d2a9c4This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env node
/**
* release-notes.mjs — the GitHub release notes for a version, on stdout.
*
* They are the README's "## What's New in v<version>" section, the same text a
* README reader sees, with its "###" headings raised to "##" and its pointer to
* the CHANGELOG replaced by absolute links. A release without a README section
* gets its CHANGELOG section instead. .github/workflows/release.yml runs this
* when a version tag is pushed; to preview the notes locally:
*
* node scripts/release-notes.mjs 2.8.2 v2.8.1
*
* The second argument is the previous release's tag, for the compare link.
*/
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath, pathToFileURL } from "url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const DEFAULT_REPO = "mixelpixx/KiCAD-MCP-Server";
/** The text between `heading` and the next occurrence of `end`, or null. */
function section(text, heading, end) {
const start = text.indexOf(`${heading}\n`);
if (start === -1) return null;
const from = start + heading.length + 1;
const to = text.indexOf(end, from);
return text.slice(from, to === -1 ? undefined : to);
}
/** The CHANGELOG's "## [x.y.z] - date" heading line for `version`, or null. */
function changelogHeading(changelog, version) {
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const m = changelog.match(new RegExp(`^## \\[${escaped}\\] - \\d{4}-\\d{2}-\\d{2}$`, "m"));
return m ? m[0] : null;
}
/** GitHub's anchor for a Markdown heading, e.g. "[2.8.1] - 2026-09-23" -> "281---2026-09-23". */
export function headingAnchor(heading) {
return heading
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.replace(/\s/g, "-");
}
/**
* The release notes for `version` as Markdown. `readme` and `changelog` are
* the files' texts; `previousTag` (such as "v2.8.1") adds a compare link.
*/
export function buildReleaseNotes({
readme,
changelog,
version,
previousTag,
repo = DEFAULT_REPO,
}) {
const tag = `v${version}`;
const heading = changelogHeading(changelog, version);
let body = section(readme.replace(/\r\n/g, "\n"), `## What's New in ${tag}`, "\n## ");
if (body === null && heading) {
body = section(changelog.replace(/\r\n/g, "\n"), heading, "\n## [");
}
if (body === null) {
throw new Error(
`no "## What's New in ${tag}" in README.md and no "## [${version}]" section in CHANGELOG.md`,
);
}
body = body
.replace(/^### /gm, "## ")
.replace(/^Full details in the \[CHANGELOG\]\(CHANGELOG\.md\)\.[ \t]*$/m, "")
.trim();
const links = [];
if (heading) {
const anchor = headingAnchor(heading.slice(3));
links.push(
`Full details: [CHANGELOG.md](https://github.com/${repo}/blob/${tag}/CHANGELOG.md#${anchor})`,
);
}
if (previousTag) {
links.push(
`Diff: [${previousTag}...${tag}](https://github.com/${repo}/compare/${previousTag}...${tag})`,
);
}
return `${body}${links.length ? `\n\n${links.join(" · ")}` : ""}\n`;
}
function main(argv) {
const [version, previousTag] = argv.slice(2);
if (!version) {
console.error("usage: node scripts/release-notes.mjs <version> [previous-tag]");
process.exit(2);
}
try {
process.stdout.write(
buildReleaseNotes({
readme: readFileSync(join(ROOT, "README.md"), "utf-8"),
changelog: readFileSync(join(ROOT, "CHANGELOG.md"), "utf-8"),
version,
previousTag: previousTag || undefined,
repo: process.env.GITHUB_REPOSITORY || DEFAULT_REPO,
}),
);
} catch (err) {
console.error(`release-notes: ${err.message}`);
process.exit(1);
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main(process.argv);
}