FN-7875: split archived pre-0.50 release notes out of root CHANGELOG.md

Adds a deterministic changelog-archive split so scripts/release.mjs stops regenerating one ever-growing root CHANGELOG.md and instead keeps only current release notes at the root while durably archiving pre-0.50.0 history.

- Add scripts/lib/changelog-archive.mjs with partitionVersionsByCutoff (splits a version-ordered list at the 0.50.0 cutoff, preserving order and treating non-parseable keys as archived) and archivePointerLine (renders the "older releases" pointer appended to the current changelog).
- Rework scripts/release.mjs's syncRootChangelog to build CHANGELOG.md (current versions + archive pointer) and a new CHANGELOG-archive.md (versions before 0.50.0) via a shared buildRootChangelogLines/normalizeChangelogLines helper instead of one monolithic file.
- Add scripts/__tests__/changelog-archive.test.mjs covering cutoff partitioning, boundary/patch handling, non-parseable keys, custom cutoffs, and the archive pointer text.
- Regenerate CHANGELOG.md (now only 0.50.0+) and add CHANGELOG-archive.md containing the pre-0.50.0 history moved out of the root file.

Files changed:
 CHANGELOG-archive.md                         | 10882 +++++++++++++++++++++++
 CHANGELOG.md                                 | 11717 ++-----------------------
 scripts/__tests__/changelog-archive.test.mjs |    58 +
 scripts/lib/changelog-archive.mjs            |    56 +
 scripts/release.mjs                          |    49 +-
 5 files changed, 11710 insertions(+), 11052 deletions(-)

Fusion-Task-Id: FN-7875
Fusion-Task-Lineage: 220e6aa1-54fb-4800-a86e-6d8d21f6bf18
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 13:10:52 -07:00
parent b77e12351e
commit 2c7df1bd78
5 changed files with 11703 additions and 11045 deletions

10882
CHANGELOG-archive.md Normal file

File diff suppressed because it is too large Load Diff

11703
CHANGELOG.md

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
archivePointerLine,
CHANGELOG_ARCHIVE_CUTOFF,
CHANGELOG_ARCHIVE_FILE,
partitionVersionsByCutoff,
} from "../lib/changelog-archive.mjs";
test("partitions versions at the pre-0.50 archive cutoff", () => {
assert.deepEqual(
partitionVersionsByCutoff(["0.58.0", "0.50.0", "0.49.0", "0.11.1"]),
{
current: ["0.58.0", "0.50.0"],
archived: ["0.49.0", "0.11.1"],
},
);
});
test("keeps the 0.50.0 boundary and newer patch versions current", () => {
const partitioned = partitionVersionsByCutoff(["0.53.1", "0.50.0", "0.49.0"]);
assert.deepEqual(partitioned.current, ["0.53.1", "0.50.0"]);
assert.deepEqual(partitioned.archived, ["0.49.0"]);
});
test("preserves input order and archives non-parseable version keys", () => {
const partitioned = partitionVersionsByCutoff([
"0.51.0",
"legacy notes",
"0.50.0 (pre-release, unpublished)",
"0.49.9",
"0.53.0",
]);
assert.deepEqual(partitioned.current, [
"0.51.0",
"0.50.0 (pre-release, unpublished)",
"0.53.0",
]);
assert.deepEqual(partitioned.archived, ["legacy notes", "0.49.9"]);
});
test("supports a custom cutoff with release.mjs semver-ish parsing", () => {
assert.deepEqual(partitionVersionsByCutoff(["1.2.0", "1.1.9", "not semver"], "1.2.0"), {
current: ["1.2.0"],
archived: ["1.1.9", "not semver"],
});
});
test("archive pointer references the archive file and cutoff", () => {
const pointer = archivePointerLine();
assert.ok(pointer.includes(CHANGELOG_ARCHIVE_FILE));
assert.ok(pointer.includes(CHANGELOG_ARCHIVE_CUTOFF));
assert.match(pointer, /\.\/CHANGELOG-archive\.md/);
});

View File

@@ -0,0 +1,56 @@
/**
* FNXC:ReleaseChangelog 2026-07-12-00:00:
* The root CHANGELOG.md is regenerated by scripts/release.mjs on every release, so pruning old release notes by hand would be reverted.
* Keep the 0.50.0 cutoff in generator-owned pure logic so current release notes stay in CHANGELOG.md and older notes are durably split into CHANGELOG-archive.md.
*/
export const CHANGELOG_ARCHIVE_CUTOFF = "0.50.0";
export const CHANGELOG_ARCHIVE_FILE = "CHANGELOG-archive.md";
/**
* Partition version keys while preserving the source changelog order inside each bucket.
* Versions greater than or equal to the cutoff remain in the current changelog; versions below it, including non-parseable keys treated as 0.0.0, move to the archive.
*
* @param {string[]} versionOrder
* @param {string} cutoff
* @returns {{ current: string[], archived: string[] }}
*/
export function partitionVersionsByCutoff(versionOrder, cutoff = CHANGELOG_ARCHIVE_CUTOFF) {
const current = [];
const archived = [];
for (const version of versionOrder) {
if (compareSemver(version, cutoff) >= 0) {
current.push(version);
} else {
archived.push(version);
}
}
return { current, archived };
}
/**
* Return the stable archive pointer appended to the current root changelog.
*
* @param {string} archiveFile
* @returns {string}
*/
export function archivePointerLine(archiveFile = CHANGELOG_ARCHIVE_FILE) {
return `> Older releases (before ${CHANGELOG_ARCHIVE_CUTOFF}) are archived in [\`${archiveFile}\`](./${archiveFile}).`;
}
/** Compare two semver-ish version strings ("0.2.5", "0.4.0 (pre-release)"). */
function compareSemver(a, b) {
const pa = parseVersionKey(a);
const pb = parseVersionKey(b);
for (let i = 0; i < 3; i++) {
if (pa[i] !== pb[i]) return pa[i] - pb[i];
}
return 0;
}
function parseVersionKey(key) {
const m = key.match(/^(\d+)\.(\d+)\.(\d+)/);
if (!m) return [0, 0, 0];
return [Number(m[1]), Number(m[2]), Number(m[3])];
}

View File

@@ -37,6 +37,12 @@ import { extractVersionNotes, replaceVersionSection } from "./lib/extract-versio
import { parseChangesetFile } from "./lib/changeset-schema.mjs";
import { distillDeterministic } from "./lib/distill-release-notes.mjs";
import { shouldPromptForVersion } from "./lib/release-prompt-gate.mjs";
import {
archivePointerLine,
CHANGELOG_ARCHIVE_CUTOFF,
CHANGELOG_ARCHIVE_FILE,
partitionVersionsByCutoff,
} from "./lib/changelog-archive.mjs";
const args = new Set(process.argv.slice(2));
/*
@@ -68,7 +74,7 @@ function run(cmd, { capture = false, allowFail = false, cwd } = {}) {
}
/**
* Rewrite the repo-root CHANGELOG.md by aggregating every
* Rewrite the repo-root changelogs by aggregating every
* `packages/*\/CHANGELOG.md` into a single per-version view.
*
* For each version that appears in any package, we emit a top-level
@@ -80,6 +86,10 @@ function run(cmd, { capture = false, allowFail = false, cwd } = {}) {
* release (the one whose top version is highest by semver). Any extra
* versions found only in other packages are appended in semver-descending
* order at the end.
*
* FNXC:ReleaseChangelog 2026-07-12-00:00:
* The root CHANGELOG.md is regenerated during every release, so the pre-0.50 prune must happen in this generator instead of as a manual docs edit.
* Keep versions greater than or equal to the 0.50.0 cutoff in CHANGELOG.md, and write older versions to CHANGELOG-archive.md so the split survives the next release sync.
*/
function syncRootChangelog() {
const pkgsDir = "packages";
@@ -113,12 +123,31 @@ function syncRootChangelog() {
}
}
const lines = [
"# Fusion changelog",
"",
"User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand.",
"",
];
const { current, archived } = partitionVersionsByCutoff(versionOrder);
const currentLines = buildRootChangelogLines({
title: "# Fusion changelog",
banner: "User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand.",
parsed,
versionOrder: current,
});
if (archived.length > 0) {
currentLines.push(archivePointerLine(), "");
}
const archiveLines = buildRootChangelogLines({
title: "# Fusion changelog archive",
banner: `Archived release notes before ${CHANGELOG_ARCHIVE_CUTOFF}. This file is auto-synced from each \`packages/*/CHANGELOG.md\` by \`scripts/release.mjs\` — do not edit by hand.`,
parsed,
versionOrder: archived,
});
writeFileSync("CHANGELOG.md", normalizeChangelogLines(currentLines));
writeFileSync(CHANGELOG_ARCHIVE_FILE, normalizeChangelogLines(archiveLines));
}
function buildRootChangelogLines({ title, banner, parsed, versionOrder }) {
const lines = [title, "", banner, ""];
for (const version of versionOrder) {
lines.push(`## ${version}`, "");
@@ -136,7 +165,11 @@ function syncRootChangelog() {
}
}
writeFileSync("CHANGELOG.md", lines.join("\n").replace(/\n{3,}/g, "\n\n"));
return lines;
}
function normalizeChangelogLines(lines) {
return lines.join("\n").replace(/\n{3,}/g, "\n\n");
}
/**