4d9f9d9edf
scripts/ci-release-notes.ts previously extracted only the single target-version section, so changelog entries finalized under tags pushed without a GitHub Release (e.g. v15.12.5 and v15.12.6 — collateral from the pre-#2564 release-cancellation bug) were stranded out of the next published release body. The generator now walks the range (latest-published-release, target], resolved via 'gh release list', and merges every in-range '## [X.Y.Z]' section per package — grouped by '### <category>' with bullet-level dedup so post-release changelog flattening cannot surface the same entry twice. Versions iterate newest-first so newer phrasing wins on dup resolution, and categories are sorted into the canonical Breaking/Added/Changed/Fixed/Removed order regardless of source order. Falls back to legacy single-version extraction when 'gh' is unavailable or no prior published release resolves (safe no-op); 'OMP_RELEASE_NOTES_FLOOR=v15.12.4' overrides the lookup for manual re-runs (empty string forces legacy mode). Adds scripts/ci-release-notes.test.ts covering: range inclusion above floor, target-inclusive boundary, dedup of bullets flattened forward into multiple versions, canonical category ordering, the null-floor legacy fallback, empty version sections skipped, and no empty-category emission when dedup drains a bucket. Wired into 'bun run test:scripts'. Fixes #2596
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
compareVersions,
|
|
enumerateChangelogVersions,
|
|
mergePackageSection,
|
|
} from "./ci-release-notes";
|
|
|
|
const FIXTURE = [
|
|
"# Changelog",
|
|
"",
|
|
"## [Unreleased]",
|
|
"",
|
|
"### Added",
|
|
"",
|
|
"- Unreleased entry not in any tag yet.",
|
|
"",
|
|
"## [15.13.0] - 2026-06-14",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- Fixed unknown `--`-prefixed flags being silently consumed as prompt text.",
|
|
"- Fixed something only in 15.13.0.",
|
|
"",
|
|
"### Removed",
|
|
"",
|
|
"- Removed a deprecated thing in 15.13.0.",
|
|
"",
|
|
"## [15.12.6] - 2026-06-14",
|
|
"",
|
|
"### Breaking Changes",
|
|
"",
|
|
"- Removed `writeLine`/`writeLineSync` from the public SessionStorageWriter contract.",
|
|
"",
|
|
"### Added",
|
|
"",
|
|
"- Added package-level exports for session context.",
|
|
"",
|
|
"## [15.12.5] - 2026-06-13",
|
|
"",
|
|
"### Changed",
|
|
"",
|
|
"- Changed terminal resize handling to paint only the visible viewport.",
|
|
"",
|
|
"### Fixed",
|
|
"",
|
|
"- Fixed unknown `--`-prefixed flags being silently consumed as prompt text.",
|
|
"",
|
|
"## [15.12.4] - 2026-06-13",
|
|
"",
|
|
"### Added",
|
|
"",
|
|
"- Predates the silent-tag window; must not appear when floor=15.12.4.",
|
|
"",
|
|
].join("\n");
|
|
|
|
describe("compareVersions", () => {
|
|
it("orders semver tags numerically across all components", () => {
|
|
expect(compareVersions("15.12.5", "15.13.0") < 0).toBe(true);
|
|
expect(compareVersions("v15.13.0", "15.12.6") > 0).toBe(true);
|
|
expect(compareVersions("15.12.6", "15.12.6") === 0).toBe(true);
|
|
// Numeric (not lexicographic) — 15.2.0 < 15.13.0.
|
|
expect(compareVersions("15.2.0", "15.13.0") < 0).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("enumerateChangelogVersions", () => {
|
|
it("returns every semver heading in document order, skipping Unreleased", () => {
|
|
const spans = enumerateChangelogVersions(FIXTURE);
|
|
expect(spans.map(s => s.version)).toEqual(["15.13.0", "15.12.6", "15.12.5", "15.12.4"]);
|
|
});
|
|
|
|
it("bounds each span by the next `## [` heading (Unreleased included as boundary)", () => {
|
|
const spans = enumerateChangelogVersions(FIXTURE);
|
|
const lines = FIXTURE.split("\n");
|
|
for (const span of spans) {
|
|
expect(lines[span.start]).toMatch(/^## \[\d+\.\d+\.\d+\]/);
|
|
// Body never bleeds into the next heading.
|
|
for (let i = span.start + 1; i < span.end; i++) {
|
|
expect(lines[i].startsWith("## [")).toBe(false);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("mergePackageSection", () => {
|
|
it("includes every silent-tag section above floor up to target inclusive", () => {
|
|
const merged = mergePackageSection(FIXTURE, "15.12.4", "15.13.0");
|
|
// 15.12.6 and 15.12.5 unique fingerprints must land.
|
|
expect(merged).toContain(
|
|
"Removed `writeLine`/`writeLineSync` from the public SessionStorageWriter contract.",
|
|
);
|
|
expect(merged).toContain("Added package-level exports for session context.");
|
|
expect(merged).toContain("Changed terminal resize handling to paint only the visible viewport.");
|
|
// 15.12.4 entry stays excluded — it is the floor.
|
|
expect(merged).not.toContain("Predates the silent-tag window");
|
|
// Unreleased never leaks.
|
|
expect(merged).not.toContain("Unreleased entry");
|
|
});
|
|
|
|
it("dedupes bullets flattened forward into multiple versions", () => {
|
|
const merged = mergePackageSection(FIXTURE, "15.12.4", "15.13.0");
|
|
const dupRegex = /Fixed unknown `--`-prefixed flags being silently consumed as prompt text\./g;
|
|
expect(merged.match(dupRegex)?.length).toBe(1);
|
|
});
|
|
|
|
it("groups bullets under the canonical category order regardless of source-version order", () => {
|
|
const merged = mergePackageSection(FIXTURE, "15.12.4", "15.13.0");
|
|
// Expected canonical order: Breaking Changes → Added → Changed → Fixed → Removed.
|
|
const headings = [...merged.matchAll(/^### (.+)$/gm)].map(m => m[1]);
|
|
expect(headings).toEqual(["Breaking Changes", "Added", "Changed", "Fixed", "Removed"]);
|
|
});
|
|
|
|
it("floor=null reproduces single-version (legacy) extraction for the target", () => {
|
|
const merged = mergePackageSection(FIXTURE, null, "15.13.0");
|
|
expect(merged).toContain("Fixed something only in 15.13.0");
|
|
expect(merged).toContain("Removed a deprecated thing in 15.13.0");
|
|
// Anything below the target stays out when no floor is set.
|
|
expect(merged).not.toContain("writeLine");
|
|
expect(merged).not.toContain("Added package-level exports");
|
|
});
|
|
|
|
it("returns empty string when no version in the requested range carries body content", () => {
|
|
const empty = ["# Changelog", "", "## [15.13.0] - 2026-06-14", "", "## [15.12.6] - 2026-06-14"]
|
|
.join("\n");
|
|
expect(mergePackageSection(empty, "15.12.5", "15.13.0")).toBe("");
|
|
});
|
|
|
|
it("never emits a category with only blank/whitespace bullets after dedup", () => {
|
|
// If 15.13.0 already pulled the only Fixed bullet, an older section
|
|
// contributing the identical bullet must not produce an empty
|
|
// `### Fixed` heading by itself.
|
|
const merged = mergePackageSection(FIXTURE, "15.12.4", "15.13.0");
|
|
expect(merged).not.toMatch(/### Fixed\s*\n\s*(### |$)/);
|
|
});
|
|
});
|