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:
@@ -321,6 +321,11 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: "1.3"
|
||||
- name: Generate release notes from CHANGELOGs
|
||||
run: bun scripts/ci-release-notes.ts
|
||||
- name: Download release binaries
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -332,6 +337,7 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
packages/coding-agent/binaries/omp-*
|
||||
body_path: release-notes.md
|
||||
generate_release_notes: true
|
||||
|
||||
|
||||
|
||||
@@ -1063,9 +1063,10 @@ fn parse_key_inner(bytes: &[u8], kitty_protocol_active: bool) -> Option<Cow<'sta
|
||||
&& bytes[0] == 0x1b
|
||||
&& bytes[1] == 0x1b
|
||||
&& (bytes[2] == b'[' || bytes[2] == b'O')
|
||||
&& let Some(inner_key) = parse_key_inner(&bytes[1..], true) {
|
||||
return Some(Cow::Owned(format!("alt+{inner_key}")));
|
||||
}
|
||||
&& let Some(inner_key) = parse_key_inner(&bytes[1..], true)
|
||||
{
|
||||
return Some(Cow::Owned(format!("alt+{inner_key}")));
|
||||
}
|
||||
|
||||
// Two-byte ESC sequences (legacy ALT prefix, with exceptions even in kitty
|
||||
// mode)
|
||||
|
||||
@@ -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}).`);
|
||||
Reference in New Issue
Block a user