feat(FN-3311): add version notes extractor for GitHub releases
Merges FN-3311 work into the release pipeline: adds a version notes extractor with tests and integrates GitHub release creation into the release script, plus a minor CSS design token consistency fix in the dashboard. Fusion-Task-Id: FN-3311
This commit is contained in:
71
scripts/__tests__/extract-version-notes.test.mjs
Normal file
71
scripts/__tests__/extract-version-notes.test.mjs
Normal file
@@ -0,0 +1,71 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { extractVersionNotes } from "../lib/extract-version-notes.mjs";
|
||||
|
||||
const changelog = `# Fusion changelog
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### @runfusion/fusion
|
||||
|
||||
#### Patch Changes
|
||||
|
||||
- Added release integration.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### @runfusion/fusion
|
||||
|
||||
#### Patch Changes
|
||||
|
||||
- Fixed parser bug.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### @runfusion/fusion
|
||||
|
||||
#### Patch Changes
|
||||
|
||||
- Initial release.
|
||||
`;
|
||||
|
||||
test("extracts correct section for known version", () => {
|
||||
const notes = extractVersionNotes(changelog, "1.1.0");
|
||||
assert.match(notes, /Fixed parser bug\./);
|
||||
assert.doesNotMatch(notes, /Added release integration\./);
|
||||
});
|
||||
|
||||
test("returns full multiline body including sub-headings", () => {
|
||||
const notes = extractVersionNotes(changelog, "1.2.0");
|
||||
assert.match(notes, /^### @runfusion\/fusion/m);
|
||||
assert.match(notes, /^#### Patch Changes/m);
|
||||
assert.match(notes, /- Added release integration\./);
|
||||
});
|
||||
|
||||
test("returns fallback when version not found", () => {
|
||||
const notes = extractVersionNotes(changelog, "9.9.9");
|
||||
assert.equal(notes, "Release v9.9.9");
|
||||
});
|
||||
|
||||
test("returns fallback when changelog content is empty", () => {
|
||||
const notes = extractVersionNotes("", "1.2.0");
|
||||
assert.equal(notes, "Release v1.2.0");
|
||||
});
|
||||
|
||||
test("handles version as last section with no trailing heading", () => {
|
||||
const notes = extractVersionNotes(changelog, "1.0.0");
|
||||
assert.match(notes, /Initial release\./);
|
||||
});
|
||||
|
||||
test("handles single-version changelog", () => {
|
||||
const single = `# Changelog\n\n## 2.0.0\n\n### pkg\n\n#### Patch Changes\n\n- Solo entry.\n`;
|
||||
const notes = extractVersionNotes(single, "2.0.0");
|
||||
assert.match(notes, /Solo entry\./);
|
||||
});
|
||||
|
||||
test("does not bleed into adjacent version sections", () => {
|
||||
const notes = extractVersionNotes(changelog, "1.1.0");
|
||||
assert.doesNotMatch(notes, /Initial release\./);
|
||||
assert.doesNotMatch(notes, /Added release integration\./);
|
||||
});
|
||||
32
scripts/lib/extract-version-notes.mjs
Normal file
32
scripts/lib/extract-version-notes.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Extract the changelog section for a specific version from the root CHANGELOG.md content.
|
||||
* @param {string} content - Full CHANGELOG.md content (as formatted by syncRootChangelog)
|
||||
* @param {string} version - Bare version string (e.g. "0.16.0"), NOT "v"-prefixed
|
||||
* @returns {string} Release notes body, or a fallback like "Release v{version}" if not found
|
||||
*/
|
||||
export function extractVersionNotes(content, version) {
|
||||
const fallback = `Release v${version}`;
|
||||
|
||||
if (!content || !version) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const lines = content.split(/\r?\n/);
|
||||
const header = `## ${version}`;
|
||||
const startIndex = lines.findIndex((line) => line.trim() === header);
|
||||
|
||||
if (startIndex === -1) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
let endIndex = lines.length;
|
||||
for (let i = startIndex + 1; i < lines.length; i += 1) {
|
||||
if (lines[i].startsWith("## ")) {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const body = lines.slice(startIndex + 1, endIndex).join("\n").trim();
|
||||
return body || fallback;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import { tmpdir } from "node:os";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin, stdout } from "node:process";
|
||||
|
||||
import { extractVersionNotes } from "./lib/extract-version-notes.mjs";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const DRY_RUN = args.has("--dry-run");
|
||||
const AUTO_YES = args.has("--yes") || args.has("-y");
|
||||
@@ -523,4 +525,47 @@ info(`Creating and pushing tag v${version}…`);
|
||||
run(`git tag v${version}`);
|
||||
run(`git push origin v${version}`);
|
||||
|
||||
ok(`Released v${version}. The 'v${version}' tag will trigger release.yml for binary builds.`);
|
||||
// --- GitHub Release ------------------------------------------------------
|
||||
|
||||
let githubReleaseStatus = "not-created";
|
||||
const changelogContent = readFileSync("CHANGELOG.md", "utf8");
|
||||
const releaseNotes = extractVersionNotes(changelogContent, version);
|
||||
const ghCheck = spawnSync("gh", ["--version"], { stdio: "pipe" });
|
||||
|
||||
if (ghCheck.status !== 0) {
|
||||
githubReleaseStatus = "missing-gh";
|
||||
warn(`⚠ gh CLI not found. Create the GitHub Release manually:\n gh release create v${version} --title "v${version}" --latest`);
|
||||
} else {
|
||||
let notesFile;
|
||||
try {
|
||||
const notesDir = mkdtempSync(join(tmpdir(), "fusion-release-notes-"));
|
||||
notesFile = join(notesDir, `v${version}-notes.md`);
|
||||
writeFileSync(notesFile, `${releaseNotes}\n`, "utf8");
|
||||
|
||||
const ghCreate = spawnSync(
|
||||
"gh",
|
||||
["release", "create", `v${version}`, "--title", `v${version}`, "--notes-file", notesFile, "--latest"],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
|
||||
if (ghCreate.status !== 0) {
|
||||
warn(`GitHub Release creation failed for v${version}. You can retry manually with gh release create.`);
|
||||
} else {
|
||||
githubReleaseStatus = "created";
|
||||
}
|
||||
} catch (error) {
|
||||
warn(`GitHub Release creation failed for v${version}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
if (notesFile && existsSync(notesFile)) {
|
||||
unlinkSync(notesFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (githubReleaseStatus === "created") {
|
||||
ok(`Released v${version}. Published to npm, tag pushed, GitHub Release created.`);
|
||||
} else if (githubReleaseStatus === "missing-gh") {
|
||||
ok(`Released v${version}. Published to npm, tag pushed. GitHub Release skipped (gh CLI not found).`);
|
||||
} else {
|
||||
ok(`Released v${version}. Published to npm, tag pushed. GitHub Release was not created (see warnings above).`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user