Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Add parseChangelog and renderChangelog
  • Loading branch information
mbg committed Jul 24, 2026
commit 916098aa8d13708f92e6c439083f7832ba3b89c7
12 changes: 12 additions & 0 deletions pr-checks/changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
*/

import * as assert from "node:assert/strict";
import * as fs from "node:fs";
import { describe, it } from "node:test";

import {
EMPTY_CHANGELOG,
getReleaseDateString,
parseChangelog,
processChangelogForBackports,
renderChangelog,
setVersionAndDate,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";

const testDate = new Date(2026, 7, 14);

Expand All @@ -37,6 +41,14 @@ describe("setVersionAndDate", async () => {
});
});

describe("parseChangelog + renderChangelog", async () => {
await it("renderChangelog(parseChangelog(c)) == c", async () => {
const actualChangelog = fs.readFileSync(CHANGELOG_FILE, "utf-8");
const roundtrip = renderChangelog(parseChangelog(actualChangelog));
assert.deepEqual(roundtrip.split("\n"), actualChangelog.split("\n"));
});
});

const testChangelog = `# CodeQL Action Changelog

## 4.12.3 - 14 Aug 2026
Expand Down
85 changes: 85 additions & 0 deletions pr-checks/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ export const EMPTY_CHANGELOG = `# CodeQL Action Changelog

${NO_CHANGES_STR}`;

/**
* Represents sections in a changelog.
*/
export interface ChangelogSection {
headerLine: string;
bodyLines: string[];
}

/**
* Represents a changelog.
*/
export interface Changelog {
preamble: string[];
sections: ChangelogSection[];
}

/** Returns `date` formatted as `DD Mon YYYY`. */
export function getReleaseDateString(today: Date = new Date()): string {
return today.toLocaleDateString("en-GB", {
Expand Down Expand Up @@ -60,6 +76,75 @@ export function setVersionAndDate(
return content.replace(UNRELEASED_PLACEHOLDER, versionAndDate);
}

/**
* Parses `content` into a structured representation of a changelog.
*
* @param content The contents of the changelog file.
*/
export function parseChangelog(content: string): Changelog {
const lines = content.split("\n");
let i = 0;

const preamble: string[] = [];
const sections: ChangelogSection[] = [];
let currentSection: ChangelogSection | undefined = undefined;

// Process all lines of the input file.
while (i < lines.length) {
const line = lines[i];

// Sections of the changelog start with `## `.
if (line.startsWith("## ")) {
// We have discovered a new section. If `currentSection` is already defined,
// then this marks the end of that section. Push it to the array of sections
// in the changelog.
if (currentSection !== undefined) {
sections.push(currentSection);
}

// Initialise the new section.
currentSection = { headerLine: line, bodyLines: [] };
} else if (currentSection !== undefined) {
// Add lines between the section header and the next to the current section.
currentSection.bodyLines.push(line);
} else {
// This is neither a section header nor are we in a section already,
// so this line is part of the preamble.
preamble.push(line);
}

i++;
}

// Push the current section to the array of completed sections, if there is
// still one unfinished.
if (currentSection !== undefined) {
sections.push(currentSection);
}

return { preamble, sections };
}

/**
* Combines an array of lines into a single string by adding line breaks.
*/
export function unlines(lines: string[]): string {
return `${lines.join("\n")}`;
}

/**
* Renders a given changelog to a string.
*/
export function renderChangelog(changelog: Changelog): string {
let result = unlines(changelog.preamble);

for (const section of changelog.sections) {
result += `\n${section.headerLine}\n${unlines(section.bodyLines)}`;
}

return result;
}

/**
* Processes changelog entries for a backport, converting version references
* from the source major version to the target major version and filtering
Expand Down