diff --git a/pr-checks/changelog.test.ts b/pr-checks/changelog.test.ts index 45658154f..b88f9b0c0 100755 --- a/pr-checks/changelog.test.ts +++ b/pr-checks/changelog.test.ts @@ -9,13 +9,17 @@ import * as fs from "node:fs"; import { describe, it } from "node:test"; import { + addBodyLinesToUnreleasedSection, + ChangelogSection, EMPTY_CHANGELOG, getHeader, getReleaseDateString, + NO_CHANGES_STR, parseChangelog, processChangelogForBackports, renderChangelog, setVersionAndDate, + UNRELEASED_PLACEHOLDER, } from "./changelog"; import { CHANGELOG_FILE } from "./config"; @@ -89,3 +93,78 @@ describe("processChangelogForBackports", async () => { assert.deepEqual(result.split("\n"), testChangelogResult.split("\n")); }); }); + +describe("addBodyLinesToUnreleasedSection", async () => { + function newChangelogWithSections(sections: ChangelogSection[]) { + return { + preamble: [], + sections, + }; + } + + await it("throws error if '[UNRELEASED]' section does not exist", async () => { + const emptyChangelog = newChangelogWithSections([]); + assert.throws(() => addBodyLinesToUnreleasedSection(emptyChangelog, [])); + + const releasedChangelog = newChangelogWithSections([ + { + headerLine: "## Release 1.0.0", + bodyLines: [], + }, + { + headerLine: "## Release 2.0.0", + bodyLines: [], + }, + { + headerLine: "## Release 3.0.0", + bodyLines: [], + }, + ]); + assert.throws(() => addBodyLinesToUnreleasedSection(releasedChangelog, [])); + }); + + await it("overwrites 'No user facing changes.'", async () => { + const changelog = newChangelogWithSections([ + { + headerLine: `## ${UNRELEASED_PLACEHOLDER}`, + bodyLines: ["", NO_CHANGES_STR, ""], + }, + ]); + + addBodyLinesToUnreleasedSection(changelog, ["- foo"]); + + assert.equal(changelog.sections[0].bodyLines.length, 3); + assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]); + }); + + await it("does nothing if lines is empty", async () => { + const changelog = newChangelogWithSections([ + { + headerLine: `## ${UNRELEASED_PLACEHOLDER}`, + bodyLines: ["", NO_CHANGES_STR, ""], + }, + ]); + const changelogClone = structuredClone(changelog); + + addBodyLinesToUnreleasedSection(changelog, []); + + assert.deepEqual(changelog, changelogClone); + }); + + await it("inserts a line", async () => { + const changelog = newChangelogWithSections([ + { + headerLine: `## ${UNRELEASED_PLACEHOLDER}`, + bodyLines: ["", "- Added a new dependency.", ""], + }, + ]); + const lineToInsert = "- foo"; + + addBodyLinesToUnreleasedSection(changelog, [lineToInsert]); + + assert.equal(changelog.sections[0].bodyLines.length, 4); + assert.ok( + changelog.sections[0].bodyLines.some((line) => line === lineToInsert), + ); + }); +}); diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index 826543024..ec765b4c8 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -132,6 +132,59 @@ export function parseChangelog(content: string): Changelog { return { preamble, sections }; } +/** + * Inserts the changenotes `notes` under the `[UNRELEASED]` section of `changelog`. + * If the section contains the stock message {@link NO_CHANGES_STR}, then + * `notes` will be inserted in place and the stock message will be deleted. + * + * This function will throw an exception if `[UNRELEASED]` does not exist. + * + * @param changelog The CHANGELOG object to modify. + * @param lines The changenotes to insert. + */ +export function addBodyLinesToUnreleasedSection( + changelog: Changelog, + lines: string[], +) { + // Find the '[UNRELEASED]' section. + let unreleasedSection: ChangelogSection | undefined; + for (const section of changelog.sections) { + if (getHeader(section.headerLine) === UNRELEASED_PLACEHOLDER) { + unreleasedSection = section; + break; + } + } + + // Ensure that the '[UNRELEASED]' section exists first. + if (unreleasedSection === undefined) { + throw Error( + "Cannot put changenotes into CHANGELOG.md's '[UNRELEASED]' section because it does not exist", + ); + } + + let insertAtIndex = 0; + let deleteCount = 0; + + // If the section contains an empty line, preserve it -- insert afterward. + if ( + unreleasedSection.bodyLines.length > 0 && + unreleasedSection.bodyLines[0] === "" + ) { + insertAtIndex++; + } + + // If the section contains the stock message 'No user facing changes.' + if ( + lines.length > 0 && + unreleasedSection.bodyLines.length > insertAtIndex && + unreleasedSection.bodyLines[insertAtIndex].trim() === NO_CHANGES_STR + ) { + deleteCount++; // Delete the line by incrementing the delete marker. + } + + unreleasedSection.bodyLines.splice(insertAtIndex, deleteCount, ...lines); +} + /** * Combines an array of lines into a single string by adding line breaks. */