fix(ci): aggregated release notes across silent intervening tags
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
This commit is contained in:
+1
-1
@@ -104,7 +104,7 @@
|
||||
"build:native": "bun --cwd=packages/natives run build",
|
||||
"test": "bun run --parallel test:ts test:rs",
|
||||
"test:ts": "GITHUB_ACTIONS= bun run --workspaces --if-present test -- --only-failures && bun run test:scripts",
|
||||
"test:scripts": "bun test scripts/ci-concurrency.test.ts",
|
||||
"test:scripts": "bun test scripts/ci-concurrency.test.ts scripts/ci-release-notes.test.ts",
|
||||
"test:rs": "bun scripts/run-rs-task.ts test:rs",
|
||||
"check": "bun run --parallel check:ts check:rs",
|
||||
"check:ts": "bun run check:tools && bun run --workspaces --if-present check",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed Kokoro TTS setup loading the workspace/global `@huggingface/transformers` runtime before the side-installed Kokoro runtime, which could leave `onnxruntime-node@1.26.0` bound to an older `libonnxruntime.so.1` and fail with `VERS_1.26.0` missing ([#2591](https://github.com/can1357/oh-my-pi/issues/2591)).
|
||||
- Fixed `scripts/ci-release-notes.ts` stranding curated changelog entries from intervening *silent* tags (a `vX.Y.Z` tag pushed without a GitHub Release, e.g. the `v15.12.5`/`v15.12.6` casualties of the pre-#2564 release-cancellation bug). The generator now walks `(latest-published-release, target]` — resolved via `gh release list` from the `release_github` CI job — and merges every in-range `## [X.Y.Z]` section per package, grouped by `### <category>` with bullet-level dedupe so post-release changelog flattening cannot duplicate entries. Falls back to the legacy single-version extraction when no prior published release resolves, and `OMP_RELEASE_NOTES_FLOOR=v15.12.4` overrides the lookup for manual rebuilds ([#2596](https://github.com/can1357/oh-my-pi/issues/2596)).
|
||||
- Fixed `Test & smoke (TS)` CI timeouts caused by parallel test files racing on the process-global Settings singleton. `CustomEditor` now accepts a `magicKeywordsEnabledOverride` injection point so the shimmer-gate test can assert behaviour without calling `resetSettingsForTest()` / `Settings.init()`; the "streaming tool call preview height" describe drops its gratuitous Settings reset+init. Production wiring is unchanged ([#2582](https://github.com/can1357/oh-my-pi/issues/2582))
|
||||
- Fixed MCP OAuth fallback rendering to show a short terminal hyperlink and keep the raw authorization URL on one unwrapped copy line ([#2121](https://github.com/can1357/oh-my-pi/issues/2121)).
|
||||
- Fixed `omp dry-balance --bench` to recover from 401 token failures by re-minting the failing OAuth credential in place before switching accounts
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
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*(### |$)/);
|
||||
});
|
||||
});
|
||||
Regular → Executable
+241
-53
@@ -2,49 +2,179 @@
|
||||
/**
|
||||
* 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.
|
||||
* Walks the version range `(latest-published-release, target]` so changelog
|
||||
* sections finalized under intervening *silent* tags (a `vX.Y.Z` tag that
|
||||
* exists on the remote but has no GitHub Release — most often because a CI
|
||||
* concurrency-cancel killed the publish job, #2596 / #2564) are rolled into
|
||||
* the next published release body. Sections are grouped by `package.json`
|
||||
* `name`, then merged per `### <category>` bullet bucket. Bullet lines are
|
||||
* deduplicated by exact trimmed text so post-release changelog flattening
|
||||
* (`fix-changelogs`) does not surface the same entry twice. 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
|
||||
*
|
||||
* The lower bound is resolved by `gh release list`. Set
|
||||
* `OMP_RELEASE_NOTES_FLOOR=v15.12.4` to override (empty string forces
|
||||
* single-version mode, matching the pre-#2596 behavior). `OMP_REPO` /
|
||||
* `GITHUB_REPOSITORY` control the queried repo.
|
||||
*
|
||||
* 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.
|
||||
* underneath; this only adds curated context.
|
||||
*/
|
||||
|
||||
import { Glob } from "bun";
|
||||
import { $, Glob } from "bun";
|
||||
|
||||
const changelogGlob = new Glob("packages/*/CHANGELOG.md");
|
||||
const REPO = process.env.OMP_REPO ?? process.env.GITHUB_REPOSITORY ?? "can1357/oh-my-pi";
|
||||
|
||||
function stripVPrefix(tag: string): string {
|
||||
return tag.replace(/^v/, "").trim();
|
||||
// Canonical ordering used by `fix-changelogs`; unknown categories sort
|
||||
// alphabetically after these.
|
||||
const CATEGORY_ORDER = ["Breaking Changes", "Added", "Changed", "Fixed", "Removed"] as const;
|
||||
|
||||
/** Compare two `X.Y.Z` (or `vX.Y.Z`) version strings; non-semver returns 0. */
|
||||
export function compareVersions(a: string, b: string): number {
|
||||
const am = a.replace(/^v/, "").trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
const bm = b.replace(/^v/, "").trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!am || !bm) return 0;
|
||||
if (am[1] !== bm[1]) return Number(am[1]) - Number(bm[1]);
|
||||
if (am[2] !== bm[2]) return Number(am[2]) - Number(bm[2]);
|
||||
return Number(am[3]) - Number(bm[3]);
|
||||
}
|
||||
|
||||
function extractVersionSection(content: string, version: string): string {
|
||||
export interface ChangelogVersionSpan {
|
||||
version: string;
|
||||
/** 0-indexed line of the `## [X.Y.Z]` heading. */
|
||||
start: number;
|
||||
/** 0-indexed line just past the last line of this version's body (exclusive). */
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate every `## [X.Y.Z]` heading in a changelog and compute the line span
|
||||
* up to (but not including) the next `## [` heading. `## [Unreleased]` and
|
||||
* other non-semver `## [...]` headings are ignored, but they still act as
|
||||
* span boundaries for the preceding version.
|
||||
*/
|
||||
export function enumerateChangelogVersions(content: string): ChangelogVersionSpan[] {
|
||||
const lines = content.split("\n");
|
||||
const headingPrefix = `## [${version}]`;
|
||||
let start = -1;
|
||||
const spans: ChangelogVersionSpan[] = [];
|
||||
// Indexes of *any* `## [` heading (including Unreleased) so a version's
|
||||
// span ends at the next heading of any kind.
|
||||
const headingIdx: number[] = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith(headingPrefix)) {
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
if (lines[i].startsWith("## [")) headingIdx.push(i);
|
||||
}
|
||||
if (start < 0) return "";
|
||||
let end = lines.length;
|
||||
for (let i = start; i < lines.length; i++) {
|
||||
if (lines[i].startsWith("## [")) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
for (const idx of headingIdx) {
|
||||
const m = lines[idx].match(/^## \[(\d+\.\d+\.\d+)\]/);
|
||||
if (!m) continue;
|
||||
const nextIdx = headingIdx.find(j => j > idx) ?? lines.length;
|
||||
spans.push({ version: m[1], start: idx, end: nextIdx });
|
||||
}
|
||||
return lines.slice(start, end).join("\n").trim();
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `(floorExclusive, targetInclusive]` version sections from a single
|
||||
* package's changelog into one combined body, grouped by `### <category>`.
|
||||
*
|
||||
* Versions iterate newest → oldest so newer phrasing wins when a bullet was
|
||||
* flattened forward by `fix-changelogs` and ends up in both sections.
|
||||
* `floorExclusive === null` → take only the target version (legacy behavior).
|
||||
* Returns "" when no in-range version contributes any bullet.
|
||||
*/
|
||||
export function mergePackageSection(
|
||||
content: string,
|
||||
floorExclusive: string | null,
|
||||
targetInclusive: string,
|
||||
): string {
|
||||
const spans = enumerateChangelogVersions(content)
|
||||
.filter(v => {
|
||||
if (compareVersions(v.version, targetInclusive) > 0) return false;
|
||||
if (floorExclusive === null) return compareVersions(v.version, targetInclusive) === 0;
|
||||
return compareVersions(v.version, floorExclusive) > 0;
|
||||
})
|
||||
.sort((a, b) => compareVersions(b.version, a.version));
|
||||
if (spans.length === 0) return "";
|
||||
|
||||
const lines = content.split("\n");
|
||||
const seenCategories: string[] = []; // first-seen order
|
||||
const buckets = new Map<string, string[]>();
|
||||
const seenLines = new Set<string>();
|
||||
|
||||
for (const span of spans) {
|
||||
let currentCat: string | null = null;
|
||||
let buf: string[] = [];
|
||||
const flushCurrent = () => {
|
||||
if (currentCat === null || buf.length === 0) return;
|
||||
let bucket = buckets.get(currentCat);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
buckets.set(currentCat, bucket);
|
||||
seenCategories.push(currentCat);
|
||||
}
|
||||
for (const line of buf) {
|
||||
const key = line.trim();
|
||||
if (key.length === 0) continue;
|
||||
if (seenLines.has(key)) continue;
|
||||
seenLines.add(key);
|
||||
bucket.push(line);
|
||||
}
|
||||
};
|
||||
// Skip the `## [X.Y.Z]` heading line itself.
|
||||
for (let i = span.start + 1; i < span.end; i++) {
|
||||
const line = lines[i];
|
||||
const catMatch = line.match(/^### (.+?)\s*$/);
|
||||
if (catMatch) {
|
||||
flushCurrent();
|
||||
currentCat = catMatch[1];
|
||||
buf = [];
|
||||
continue;
|
||||
}
|
||||
// Pre-category prose (rare; usually blank padding) is dropped — there
|
||||
// is no surrounding `###` to attribute it to in the merged output.
|
||||
if (currentCat === null) continue;
|
||||
buf.push(line);
|
||||
}
|
||||
flushCurrent();
|
||||
}
|
||||
|
||||
if (seenCategories.length === 0) return "";
|
||||
|
||||
seenCategories.sort((a, b) => {
|
||||
const ai = CATEGORY_ORDER.indexOf(a as (typeof CATEGORY_ORDER)[number]);
|
||||
const bi = CATEGORY_ORDER.indexOf(b as (typeof CATEGORY_ORDER)[number]);
|
||||
if (ai !== -1 && bi !== -1) return ai - bi;
|
||||
if (ai !== -1) return -1;
|
||||
if (bi !== -1) return 1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const out: string[] = [];
|
||||
for (const cat of seenCategories) {
|
||||
const bucket = buckets.get(cat) ?? [];
|
||||
// Collapse runs of blank lines and strip trailing blanks per bucket.
|
||||
const collapsed: string[] = [];
|
||||
let prevBlank = false;
|
||||
for (const line of bucket) {
|
||||
const blank = line.trim().length === 0;
|
||||
if (blank && prevBlank) continue;
|
||||
collapsed.push(line);
|
||||
prevBlank = blank;
|
||||
}
|
||||
while (collapsed.length > 0 && collapsed[collapsed.length - 1].trim().length === 0) {
|
||||
collapsed.pop();
|
||||
}
|
||||
if (collapsed.length === 0) continue;
|
||||
out.push(`### ${cat}`, "", ...collapsed, "");
|
||||
}
|
||||
while (out.length > 0 && out[out.length - 1] === "") out.pop();
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
async function loadPackageName(pkgDir: string): Promise<string> {
|
||||
@@ -56,37 +186,95 @@ async function loadPackageName(pkgDir: string): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
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.",
|
||||
/**
|
||||
* Resolve the highest published, non-prerelease, non-draft semver tag strictly
|
||||
* below `targetVersion`. Falls back to `null` on error or no candidate, which
|
||||
* downgrades the script to legacy single-version extraction.
|
||||
*
|
||||
* `OMP_RELEASE_NOTES_FLOOR` env override:
|
||||
* - unset → query `gh` (CI default)
|
||||
* - `vX.Y.Z` → use as-is (manual rerun: "I know the floor")
|
||||
* - empty string → force single-version mode (legacy escape hatch)
|
||||
*/
|
||||
async function resolvePublishedFloorTag(targetVersion: string): Promise<string | null> {
|
||||
const override = process.env.OMP_RELEASE_NOTES_FLOOR;
|
||||
if (override !== undefined) {
|
||||
const stripped = override.replace(/^v/, "").trim();
|
||||
return stripped.length === 0 ? null : stripped;
|
||||
}
|
||||
const res =
|
||||
await $`gh release list --repo ${REPO} --limit 200 --exclude-drafts --exclude-pre-releases --json tagName,isDraft,isPrerelease`
|
||||
.quiet()
|
||||
.nothrow();
|
||||
if (res.exitCode !== 0) {
|
||||
console.warn(
|
||||
`Warning: gh release list failed (exit ${res.exitCode}); falling back to single-version notes. stderr: ${res.stderr.toString().trim()}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(res.stdout.toString());
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Warning: failed to parse gh release list output: ${(err as Error).message}; falling back to single-version notes.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(raw)) return null;
|
||||
const candidates = (raw as Array<{ tagName?: unknown; isDraft?: unknown; isPrerelease?: unknown }>)
|
||||
.filter(t => t.isDraft !== true && t.isPrerelease !== true)
|
||||
.map(t => (typeof t.tagName === "string" ? t.tagName : ""))
|
||||
.filter(tag => /^v\d+\.\d+\.\d+$/.test(tag))
|
||||
.filter(tag => compareVersions(tag, targetVersion) < 0)
|
||||
.sort((a, b) => compareVersions(b, a));
|
||||
return candidates[0]?.replace(/^v/, "") ?? null;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
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 = tagInput.replace(/^v/, "").trim();
|
||||
const outputPath = process.argv[3] ?? "release-notes.md";
|
||||
const floor = await resolvePublishedFloorTag(version);
|
||||
if (floor) {
|
||||
console.log(`Aggregating CHANGELOG sections in (${floor}, ${version}].`);
|
||||
} else {
|
||||
console.log(`No prior published release resolved; emitting only ## [${version}] sections.`);
|
||||
}
|
||||
|
||||
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 merged = mergePackageSection(content, floor, version);
|
||||
if (merged === "") continue;
|
||||
const pkgDir = changelogPath.replace(/\/CHANGELOG\.md$/, "");
|
||||
const name = await loadPackageName(pkgDir);
|
||||
sections.push(`## ${name}\n\n${merged}`);
|
||||
}
|
||||
|
||||
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}${floor ? `, floor ${floor}` : ""}).`,
|
||||
);
|
||||
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);
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
}
|
||||
|
||||
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