Skip to content

Commit 181630b

Browse files
committed
Add CHANGELOG function addBodyLinesToUnreleasedSection
This will be used by the `pr-checks/changenotes.mts` script to "compile" the latest release entry of CHANGELOG.md.
1 parent be07e96 commit 181630b

2 files changed

Lines changed: 132 additions & 0 deletions

File tree

‎pr-checks/changelog.test.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import * as fs from "node:fs";
99
import { describe, it } from "node:test";
1010

1111
import {
12+
addBodyLinesToUnreleasedSection,
13+
ChangelogSection,
1214
EMPTY_CHANGELOG,
1315
getHeader,
1416
getReleaseDateString,
17+
NO_CHANGES_STR,
1518
parseChangelog,
1619
processChangelogForBackports,
1720
renderChangelog,
1821
setVersionAndDate,
22+
UNRELEASED_PLACEHOLDER,
1923
} from "./changelog";
2024
import { CHANGELOG_FILE } from "./config";
2125

@@ -90,3 +94,78 @@ describe("processChangelogForBackports", async () => {
9094
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
9195
});
9296
});
97+
98+
describe("addBodyLinesToUnreleasedSection", async () => {
99+
function newChangelogWithSections(sections: ChangelogSection[]) {
100+
return {
101+
preamble: [],
102+
sections,
103+
};
104+
}
105+
106+
await it("throws error if '[UNRELEASED]' section does not exist", async () => {
107+
const emptyChangelog = newChangelogWithSections([]);
108+
assert.throws(() => addBodyLinesToUnreleasedSection(emptyChangelog, []));
109+
110+
const releasedChangelog = newChangelogWithSections([
111+
{
112+
headerLine: "## Release 1.0.0",
113+
bodyLines: [],
114+
},
115+
{
116+
headerLine: "## Release 2.0.0",
117+
bodyLines: [],
118+
},
119+
{
120+
headerLine: "## Release 3.0.0",
121+
bodyLines: [],
122+
},
123+
]);
124+
assert.throws(() => addBodyLinesToUnreleasedSection(releasedChangelog, []));
125+
});
126+
127+
await it("overwrites 'No user facing changes.'", async () => {
128+
const changelog = newChangelogWithSections([
129+
{
130+
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
131+
bodyLines: ["", NO_CHANGES_STR, ""],
132+
},
133+
]);
134+
135+
addBodyLinesToUnreleasedSection(changelog, ["- foo"]);
136+
137+
assert.equal(changelog.sections[0].bodyLines.length, 3);
138+
assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]);
139+
});
140+
141+
await it("does nothing if lines is empty", async () => {
142+
const changelog = newChangelogWithSections([
143+
{
144+
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
145+
bodyLines: ["", NO_CHANGES_STR, ""],
146+
},
147+
]);
148+
const changelogClone = structuredClone(changelog);
149+
150+
addBodyLinesToUnreleasedSection(changelog, []);
151+
152+
assert.deepEqual(changelog, changelogClone);
153+
});
154+
155+
await it("inserts a line", async () => {
156+
const changelog = newChangelogWithSections([
157+
{
158+
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
159+
bodyLines: ["", "- Added a new dependency.", ""],
160+
},
161+
]);
162+
const lineToInsert = "- foo";
163+
164+
addBodyLinesToUnreleasedSection(changelog, [lineToInsert]);
165+
166+
assert.equal(changelog.sections[0].bodyLines.length, 4);
167+
assert.ok(
168+
changelog.sections[0].bodyLines.some((line) => line === lineToInsert),
169+
);
170+
});
171+
});

‎pr-checks/changelog.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,59 @@ export function parseChangelog(content: string): Changelog {
132132
return { preamble, sections };
133133
}
134134

135+
/**
136+
* Inserts the changenotes `notes` under the `[UNRELEASED]` section of `changelog`.
137+
* If the section contains the stock message {@link NO_CHANGES_STR}, then
138+
* `notes` will be inserted in place and the stock message will be deleted.
139+
*
140+
* This function will throw an exception if `[UNRELEASED]` does not exist.
141+
*
142+
* @param changelog The CHANGELOG object to modify.
143+
* @param lines The changenotes to insert.
144+
*/
145+
export function addBodyLinesToUnreleasedSection(
146+
changelog: Changelog,
147+
lines: string[],
148+
) {
149+
// Find the '[UNRELEASED]' section.
150+
let unreleasedSection: ChangelogSection | undefined;
151+
for (const section of changelog.sections) {
152+
if (getHeader(section.headerLine).trim() === UNRELEASED_PLACEHOLDER) {
153+
unreleasedSection = section;
154+
break;
155+
}
156+
}
157+
158+
// Ensure that the '[UNRELEASED]' section exists first.
159+
if (unreleasedSection === undefined) {
160+
throw Error(
161+
"Cannot put changenotes into CHANGELOG.md's '[UNRELEASED]' section because it does not exist",
162+
);
163+
}
164+
165+
let insertAtIndex = 0;
166+
let deleteCount = 0;
167+
168+
// If the section contains an empty line, preserve it -- insert afterward.
169+
if (
170+
unreleasedSection.bodyLines.length > 0 &&
171+
unreleasedSection.bodyLines[0] === ""
172+
) {
173+
insertAtIndex++;
174+
}
175+
176+
// If the section contains the stock message 'No user facing changes.'
177+
if (
178+
lines.length > 0 &&
179+
unreleasedSection.bodyLines.length > insertAtIndex &&
180+
unreleasedSection.bodyLines[insertAtIndex].trim() === NO_CHANGES_STR
181+
) {
182+
deleteCount++; // Delete the line by incrementing the delete marker.
183+
}
184+
185+
unreleasedSection.bodyLines.splice(insertAtIndex, deleteCount, ...lines);
186+
}
187+
135188
/**
136189
* Combines an array of lines into a single string by adding line breaks.
137190
*/

0 commit comments

Comments
 (0)