From 702580ca7452c33caeb2ab56e6d526cfad222b1b Mon Sep 17 00:00:00 2001 From: Chris Barth Date: Thu, 17 Sep 2026 09:28:34 -0500 Subject: [PATCH] Read a frozen changelog whose title is not the configured one _readChangelogSections found the first release by stripping an exact match of template.changelogTitle. Anything else at the top of the file -- a funding banner, a title whose wording has changed since the file was written -- stayed attached to the release below it, so that release's section was keyed off the preamble's first line instead, found no version, and was dropped. The next generate then wrote the changelog without it, silently, and only when the newest release was old enough for frozenBefore to freeze it. The title is now whatever precedes the first release heading, so the sections read back depend on the configuration rather than on what the file being replaced happens to say. The heading pattern the section split already used is shared with it, so the two cannot drift. Co-Authored-By: Claude Opus 5 --- lib/src/Gren.js | 18 +++++++++++++++++- test/Gren.membership.spec.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/src/Gren.js b/lib/src/Gren.js index 69286b4c..bffe2eb5 100644 --- a/lib/src/Gren.js +++ b/lib/src/Gren.js @@ -35,6 +35,12 @@ const TAGS_LIMIT = 30; // The data sources whose releases are built from the commits each release contains. const MEMBERSHIP_DATA_SOURCES = ["prs", "prs-with-issues"]; +// Where a release section starts. The preamble a changelog opens with ends here, and a +// changelog written without a separator is split here; the two have to agree on the answer. +const RELEASE_HEADING = "#{1,6} +v?\\d+\\.\\d+\\.\\d+"; +const PREAMBLE_END = new RegExp(`^${RELEASE_HEADING}`, "m"); +const SECTION_SPLIT = new RegExp(`(?<=\\n)(?=${RELEASE_HEADING})`); + /** * Get the semver version a tag or release name stands for * @@ -350,12 +356,22 @@ class Gren { content = content.slice(changelogTitle.length); } + // The title in the file is not always the one in the configuration: a banner is added to + // it, or its wording changes. Whatever stands before the first release is title too, and + // leaving it in makes the preamble's first line the heading of the release under it, which + // drops that release from the file this run is about to write over it. + const firstRelease = content.search(PREAMBLE_END); + + if (firstRelease > 0) { + content = content.slice(firstRelease); + } + let current = null; // A changelog written with nothing between its releases can only be split on the headings, // at the start of each one so that the release before it keeps its last line. const chunks = releaseSeparator ? content.split(releaseSeparator) - : content.split(/(?<=\n)(?=#{1,6} +v?\d+\.\d+\.\d+)/); + : content.split(SECTION_SPLIT); chunks.forEach((chunk) => { const heading = chunk.trimStart().split("\n")[0]; diff --git a/test/Gren.membership.spec.js b/test/Gren.membership.spec.js index 83526103..854a2c12 100644 --- a/test/Gren.membership.spec.js +++ b/test/Gren.membership.spec.js @@ -707,6 +707,42 @@ describe("Gren release membership", () => { }); }); + it("Should read the first release under a title the configuration does not spell", () => { + // A banner, or any title edited since the file was written, is still title. Keying the + // first section off the configured changelogTitle alone leaves the leftover preamble + // attached to the release under it, and that release is then dropped from the file the + // next run writes. + fs.writeFileSync( + path.join(repo.dir, "CHANGELOG.md"), + "# Changelog\n\n> Sponsor this project.\n\n" + Object.values(sections).join(separator), + ); + + const gren = createGren({ frozenBefore: "2020-01-16", changelogFilename: "CHANGELOG.md" }); + const read = gren._readChangelogSections(); + + assert.equal(read.get("2.0.0"), sections["2.0.0"]); + assert.equal(read.get("1.2.0"), sections["1.2.0"]); + }); + + it("Should read the first release under an unknown title without a separator", () => { + const plain = { "2.0.0": sections["2.0.0"], "1.2.0": sections["1.2.0"] }; + + fs.writeFileSync( + path.join(repo.dir, "CHANGELOG.md"), + "Release history\n===============\n\n" + Object.values(plain).join(""), + ); + + const gren = createGren({ + frozenBefore: "2020-01-16", + changelogFilename: "CHANGELOG.md", + template: { releaseSeparator: "" }, + }); + const read = gren._readChangelogSections(); + + assert.equal(read.get("2.0.0"), plain["2.0.0"]); + assert.equal(read.get("1.2.0"), plain["1.2.0"]); + }); + it("Should read a changelog with nothing between its releases", async () => { const plain = { "2.0.0": sections["2.0.0"], "1.2.0": sections["1.2.0"] };