diff --git a/packages/plugin-changelog/src/index.test.ts b/packages/plugin-changelog/src/index.test.ts index 3c19693..11a6a4f 100644 --- a/packages/plugin-changelog/src/index.test.ts +++ b/packages/plugin-changelog/src/index.test.ts @@ -61,6 +61,10 @@ stuff ### Subsection 2 * New entry 4 * New entry 5`, + + readme_olderEntriesFooter: `Older entries are in [CHANGELOG_OLD.md](CHANGELOG_OLD.md).`, + readme_olderEntriesFooterVariant: `Older entries have been moved to [CHANGELOG_OLD.md](CHANGELOG_OLD.md).`, + readme_olderEntriesFooterCustomLabel: `See [the older changelog](CHANGELOG_OLD.md) for previous releases.`, }; describe("Changelog plugin", () => { @@ -232,6 +236,136 @@ ${fixtures.changelog_old_testParseFooter}`, * New entry 2`); }); + it('strips the "Older entries" footer from the last entry and stores it in changelog_after', async () => { + const changelogPlugin = new ChangelogPlugin(); + const context = createMockContext({ + plugins: [changelogPlugin], + cwd: testFSRoot, + }); + + await testFS.create({ + "README.md": `${fixtures.readme_testParseHeader} +${fixtures.readme_testParse1} + +${fixtures.readme_testParse2} + +${fixtures.readme_testParse3} + +${fixtures.readme_olderEntriesFooter}`, + "CHANGELOG_OLD.md": `${fixtures.changelog_old_testParseHeader} +${fixtures.changelog_old_testParse1} + +${fixtures.changelog_old_testParse2}`, + }); + + await changelogPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + + // The last README entry should not contain the footer + const entries = context.getData("changelog_entries"); + expect(entries[2]).toBe(fixtures.readme_testParse3); + + // The footer should have been moved to changelog_after + const changelogAfter = context.getData("changelog_after"); + expect(changelogAfter).toBe(`\n\n${fixtures.readme_olderEntriesFooter}`); + }); + + it('preserves a custom-phrased "Older entries" footer verbatim', async () => { + const changelogPlugin = new ChangelogPlugin(); + const context = createMockContext({ + plugins: [changelogPlugin], + cwd: testFSRoot, + }); + + await testFS.create({ + "README.md": `${fixtures.readme_testParseHeader} +${fixtures.readme_testParse1} + +${fixtures.readme_testParse2} + +${fixtures.readme_testParse3} + +${fixtures.readme_olderEntriesFooterVariant}`, + "CHANGELOG_OLD.md": `${fixtures.changelog_old_testParseHeader} +${fixtures.changelog_old_testParse1} + +${fixtures.changelog_old_testParse2}`, + }); + + await changelogPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + + const entries = context.getData("changelog_entries"); + expect(entries[2]).toBe(fixtures.readme_testParse3); + + // Custom phrasing must be preserved exactly as the user wrote it + const changelogAfter = context.getData("changelog_after"); + expect(changelogAfter).toBe(`\n\n${fixtures.readme_olderEntriesFooterVariant}`); + }); + + it("detects a footer even without a blank line separator", async () => { + const changelogPlugin = new ChangelogPlugin(); + const context = createMockContext({ + plugins: [changelogPlugin], + cwd: testFSRoot, + }); + + await testFS.create({ + "README.md": `${fixtures.readme_testParseHeader} +${fixtures.readme_testParse1} + +${fixtures.readme_testParse2} + +${fixtures.readme_testParse3} +${fixtures.readme_olderEntriesFooter}`, + "CHANGELOG_OLD.md": `${fixtures.changelog_old_testParseHeader} +${fixtures.changelog_old_testParse1} + +${fixtures.changelog_old_testParse2}`, + }); + + await changelogPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + + const entries = context.getData("changelog_entries"); + expect(entries[2]).toBe(fixtures.readme_testParse3); + + const changelogAfter = context.getData("changelog_after"); + expect(changelogAfter).toBe(`\n\n${fixtures.readme_olderEntriesFooter}`); + }); + + it("preserves a footer with custom link label verbatim", async () => { + const changelogPlugin = new ChangelogPlugin(); + const context = createMockContext({ + plugins: [changelogPlugin], + cwd: testFSRoot, + }); + + await testFS.create({ + "README.md": `${fixtures.readme_testParseHeader} +${fixtures.readme_testParse1} + +${fixtures.readme_testParse2} + +${fixtures.readme_testParse3} + +${fixtures.readme_olderEntriesFooterCustomLabel}`, + "CHANGELOG_OLD.md": `${fixtures.changelog_old_testParseHeader} +${fixtures.changelog_old_testParse1} + +${fixtures.changelog_old_testParse2}`, + }); + + await changelogPlugin.executeStage(context, DefaultStages.check); + expect(context.errors).toHaveLength(0); + + const entries = context.getData("changelog_entries"); + expect(entries[2]).toBe(fixtures.readme_testParse3); + + const changelogAfter = context.getData("changelog_after"); + expect(changelogAfter).toBe(`\n\n${fixtures.readme_olderEntriesFooterCustomLabel}`); + }); + it("correctly handles changelogs with sub-sections", async () => { const changelogPlugin = new ChangelogPlugin(); const context = createMockContext({ @@ -419,6 +553,58 @@ ${fixtures.readme_testParse3.slice(1)} ${fixtures.changelog_old_testParse1} +${fixtures.changelog_old_testParse2}`); + }); + + it('keeps the "Older entries" footer in README.md and excludes it from CHANGELOG_OLD.md when rotating', async () => { + const changelogPlugin = new ChangelogPlugin(); + const context = createMockContext({ + plugins: [changelogPlugin], + cwd: testFSRoot, + argv: { + numChangelogEntries: 2, + }, + }); + + context.setData("changelog_filename", "README.md"); + context.setData("changelog_location", "readme"); + context.setData("changelog_before", fixtures.readme_testParseHeader); + // The footer has been moved to changelog_after by the check stage fix + context.setData("changelog_entries", [ + fixtures.readme_testParse1, + fixtures.readme_testParse2, + fixtures.readme_testParse3, + fixtures.changelog_old_testParse1, + fixtures.changelog_old_testParse2, + ]); + context.setData("changelog_after", `\n\n${fixtures.readme_olderEntriesFooter}`); + context.setData("changelog_final_newline", false); + context.setData("changelog_old_before", fixtures.changelog_old_testParseHeader); + context.setData("changelog_old_after", ""); + context.setData("changelog_entry_prefix", "###"); + context.setData("version_new", "2.3.4"); + + await changelogPlugin.executeStage(context, DefaultStages.edit); + + const readmeContent = await fs.readFile(path.join(testFSRoot, "README.md"), "utf8"); + const oldContent = await fs.readFile(path.join(testFSRoot, "CHANGELOG_OLD.md"), "utf8"); + + // Footer must appear in README + expect(readmeContent).toContain(fixtures.readme_olderEntriesFooter); + expect(readmeContent).toBe(`${fixtures.readme_testParseHeader} +${fixtures.readme_testReplaced} + +${fixtures.readme_testParse2} + +${fixtures.readme_olderEntriesFooter}`); + + // Footer must NOT appear in CHANGELOG_OLD + expect(oldContent).not.toContain(fixtures.readme_olderEntriesFooter); + expect(oldContent).toBe(`${fixtures.changelog_old_testParseHeader} +${fixtures.readme_testParse3.slice(1)} + +${fixtures.changelog_old_testParse1} + ${fixtures.changelog_old_testParse2}`); }); diff --git a/packages/plugin-changelog/src/index.ts b/packages/plugin-changelog/src/index.ts index 6919096..4ddc275 100644 --- a/packages/plugin-changelog/src/index.ts +++ b/packages/plugin-changelog/src/index.ts @@ -159,6 +159,19 @@ class ChangelogPlugin implements Plugin { parsedOld = parseChangelogFile(changelogOld, changelogPlaceholderPrefix.substr(1)); } + // When CHANGELOG_OLD.md is present, a trailing footer paragraph linking to + // CHANGELOG_OLD.md (any phrasing/link text) must stay in the README and not + // be rotated. Move it from the last entry into the "after" section verbatim. + if (changelogOld && parsed.entries.length > 0) { + const lastIndex = parsed.entries.length - 1; + const olderEntriesFooterRegex = /\n+([^\n]*\]\(CHANGELOG_OLD\.md\)[^\n]*)$/; + const footerMatch = olderEntriesFooterRegex.exec(parsed.entries[lastIndex]); + if (footerMatch) { + parsed.entries[lastIndex] = parsed.entries[lastIndex].slice(0, footerMatch.index); + parsed.after = "\n\n" + footerMatch[1] + parsed.after; + } + } + const entries = [...parsed.entries, ...(parsedOld?.entries ?? [])]; context.setData("changelog_filename", changelogFilename);