ci(ci): added automated release note generation to the GitHub release workflow

- Added a Bun-based `scripts/ci-release-notes.ts` utility that extracts `## [version]` entries from `packages/*/CHANGELOG.md` and writes a combined `release-notes.md` for the release tag.
- Updated the GitHub release workflow to install Bun, run the notes generator, and pass the generated `release-notes.md` to `softprops/action-gh-release` via `body_path`.
This commit is contained in:
can1357
2026-05-26 20:53:59 +02:00
parent 7f08b51f28
commit bc355472af
3 changed files with 102 additions and 3 deletions
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bun
/**
* Generate aggregated release notes from per-package CHANGELOG.md files.
*
* Reads the version from `GITHUB_REF_NAME` (or the first CLI arg, with or
* without a leading `v`), then collects the `## [version]` section of every
* `packages/*\/CHANGELOG.md` and emits a single markdown document grouped by
* `package.json` `name`. Sections without entries are skipped.
*
* Usage:
* bun scripts/ci-release-notes.ts # writes release-notes.md
* bun scripts/ci-release-notes.ts v15.4.3 # explicit tag/version
* bun scripts/ci-release-notes.ts 15.4.3 notes.md # custom output path
*
* Intended for the `release-github` CI job: the output is passed to
* `softprops/action-gh-release` via `body_path:`. The action's
* `generate_release_notes: true` still appends the auto-generated PR list
* underneath, so this only adds curated context — it does not replace it.
*/
import { Glob } from "bun";
const changelogGlob = new Glob("packages/*/CHANGELOG.md");
function stripVPrefix(tag: string): string {
return tag.replace(/^v/, "").trim();
}
function extractVersionSection(content: string, version: string): string {
const lines = content.split("\n");
const headingPrefix = `## [${version}]`;
let start = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith(headingPrefix)) {
start = i + 1;
break;
}
}
if (start < 0) return "";
let end = lines.length;
for (let i = start; i < lines.length; i++) {
if (lines[i].startsWith("## [")) {
end = i;
break;
}
}
return lines.slice(start, end).join("\n").trim();
}
async function loadPackageName(pkgDir: string): Promise<string> {
try {
const pkg = (await Bun.file(`${pkgDir}/package.json`).json()) as { name?: unknown };
return typeof pkg.name === "string" ? pkg.name : pkgDir;
} catch {
return pkgDir;
}
}
const tagInput = process.argv[2] ?? process.env.GITHUB_REF_NAME ?? "";
if (!tagInput) {
console.error(
"Error: version not provided. Pass as argv (e.g. `v15.4.3`) or set GITHUB_REF_NAME.",
);
process.exit(1);
}
const version = stripVPrefix(tagInput);
const outputPath = process.argv[3] ?? "release-notes.md";
const sections: string[] = [];
const changelogPaths = await Array.fromAsync(changelogGlob.scan("."));
changelogPaths.sort();
for (const changelogPath of changelogPaths) {
const content = await Bun.file(changelogPath).text();
const section = extractVersionSection(content, version);
if (!section) continue;
const pkgDir = changelogPath.replace(/\/CHANGELOG\.md$/, "");
const name = await loadPackageName(pkgDir);
sections.push(`## ${name}\n\n${section}`);
}
if (sections.length === 0) {
console.warn(
`No CHANGELOG entries found for version ${version}; writing empty release notes to ${outputPath}.`,
);
await Bun.write(outputPath, "");
process.exit(0);
}
const body = `${sections.join("\n\n")}\n`;
await Bun.write(outputPath, body);
console.log(`Wrote ${sections.length} package section(s) to ${outputPath} (version ${version}).`);