Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions packages/plugin-changelog/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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<string[]>("changelog_entries");
expect(entries[2]).toBe(fixtures.readme_testParse3);

// The footer should have been moved to changelog_after
const changelogAfter = context.getData<string>("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<string[]>("changelog_entries");
expect(entries[2]).toBe(fixtures.readme_testParse3);

// Custom phrasing must be preserved exactly as the user wrote it
const changelogAfter = context.getData<string>("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<string[]>("changelog_entries");
expect(entries[2]).toBe(fixtures.readme_testParse3);

const changelogAfter = context.getData<string>("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<string[]>("changelog_entries");
expect(entries[2]).toBe(fixtures.readme_testParse3);

const changelogAfter = context.getData<string>("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({
Expand Down Expand Up @@ -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}`);
});

Expand Down
13 changes: 13 additions & 0 deletions packages/plugin-changelog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down