mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 09:14:58 +00:00
Apply suggestion from @mbg
Co-authored-by: Michael B. Gale <mbg@github.com>
This commit is contained in:
@@ -1,253 +1,253 @@
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||
|
||||
/** The placeholder in the header for unreleased changes. */
|
||||
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
||||
|
||||
/** The default contents for a section in the changelog. */
|
||||
export const NO_CHANGES_STR = "No user facing changes.";
|
||||
|
||||
/** Placeholder changelog content for a new release. */
|
||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||
|
||||
## ${UNRELEASED_PLACEHOLDER}
|
||||
|
||||
${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 the text of the header (without the '## ' prefix) of the given section.
|
||||
* */
|
||||
export function getHeader(section: ChangelogSection): string {
|
||||
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
|
||||
}
|
||||
|
||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||
export function getReleaseDateString(today: Date = new Date()): string {
|
||||
return today.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export interface OpenChangelogOptions {
|
||||
initChangelog?: boolean;
|
||||
}
|
||||
|
||||
export function withChangelog(
|
||||
transformer: (contents: string) => string,
|
||||
options: DryRunOption & OpenChangelogOptions,
|
||||
): void {
|
||||
let content: string;
|
||||
|
||||
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
|
||||
content = EMPTY_CHANGELOG;
|
||||
} else {
|
||||
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
|
||||
} else {
|
||||
console.info(`[DRY RUN] Would have written updated changelog.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
|
||||
* and today's date.
|
||||
*/
|
||||
export function setVersionAndDate(
|
||||
version: string,
|
||||
content: string,
|
||||
date: Date = new Date(),
|
||||
): string {
|
||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||
|
||||
/** The placeholder in the header for unreleased changes. */
|
||||
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
||||
|
||||
/** The default contents for a section in the changelog. */
|
||||
export const NO_CHANGES_STR = "No user facing changes.";
|
||||
|
||||
/** Placeholder changelog content for a new release. */
|
||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||
|
||||
## ${UNRELEASED_PLACEHOLDER}
|
||||
|
||||
${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 the text of the header (without the '## ' prefix) of the given section.
|
||||
* */
|
||||
export function getHeader(section: ChangelogSection): string {
|
||||
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
|
||||
}
|
||||
|
||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||
export function getReleaseDateString(today: Date = new Date()): string {
|
||||
return today.toLocaleDateString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export interface OpenChangelogOptions {
|
||||
initChangelog?: boolean;
|
||||
}
|
||||
|
||||
export function withChangelog(
|
||||
transformer: (contents: string) => string,
|
||||
options: DryRunOption & OpenChangelogOptions,
|
||||
): void {
|
||||
let content: string;
|
||||
|
||||
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
|
||||
content = EMPTY_CHANGELOG;
|
||||
} else {
|
||||
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
|
||||
} else {
|
||||
console.info(`[DRY RUN] Would have written updated changelog.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
|
||||
* and today's date.
|
||||
*/
|
||||
export function setVersionAndDate(
|
||||
version: string,
|
||||
content: string,
|
||||
date: Date = new Date(),
|
||||
): string {
|
||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the changenotes `lines` in the `[UNRELEASED]` section of `changelog`.
|
||||
* If the section contains the stock message {@link NO_CHANGES_STR}, then
|
||||
* If the section contains the stock message {@link NO_CHANGES_STR}, then
|
||||
* `lines` 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[],
|
||||
) {
|
||||
// Do nothing if there is nothing to insert.
|
||||
if (lines.length === 0) return;
|
||||
|
||||
const unreleasedSection = changelog.sections[0];
|
||||
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
|
||||
throw Error("'[UNRELEASED]' is not the first section of 'CHANGELOG.md'");
|
||||
}
|
||||
|
||||
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
|
||||
unreleasedSection.bodyLines = ["", ...lines, ""];
|
||||
return;
|
||||
}
|
||||
|
||||
unreleasedSection.bodyLines.pop(); // Remove the last empty line.
|
||||
unreleasedSection.bodyLines.push(...lines);
|
||||
unreleasedSection.bodyLines.push("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* entries that only apply to newer versions.
|
||||
*/
|
||||
export function processChangelogForBackports(
|
||||
sourceBranchMajorVersion: string,
|
||||
targetBranchMajorVersion: string,
|
||||
content: string,
|
||||
): string {
|
||||
// Changelog entries can use the following format to indicate
|
||||
// that they only apply to newer versions
|
||||
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
|
||||
|
||||
// Parse the changelog.
|
||||
const changelog = parseChangelog(content);
|
||||
|
||||
if (changelog.sections.length === 0) {
|
||||
throw new Error("Could not find any change sections in CHANGELOG.md");
|
||||
}
|
||||
|
||||
// Filter out changelog entries that only apply to newer versions and
|
||||
// update the section headings with the backport major version for
|
||||
// sections we keep.
|
||||
for (const section of changelog.sections) {
|
||||
// Update the section headings with the backport major version.
|
||||
section.headerLine = section.headerLine.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
|
||||
const filteredEntries: string[] = [];
|
||||
let foundContent = false;
|
||||
|
||||
for (const line of section.bodyLines) {
|
||||
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
|
||||
// of the target branch is smaller than the required version.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (
|
||||
match &&
|
||||
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep the line.
|
||||
filteredEntries.push(line);
|
||||
|
||||
// Set `foundContent` to `true` if the line is not empty.
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the section with the retained entries.
|
||||
section.bodyLines = filteredEntries;
|
||||
|
||||
// Add an entry if we didn't keep any.
|
||||
if (!foundContent) {
|
||||
section.bodyLines.push(NO_CHANGES_STR);
|
||||
}
|
||||
}
|
||||
|
||||
return renderChangelog(changelog);
|
||||
}
|
||||
*
|
||||
* 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[],
|
||||
) {
|
||||
// Do nothing if there is nothing to insert.
|
||||
if (lines.length === 0) return;
|
||||
|
||||
const unreleasedSection = changelog.sections[0];
|
||||
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
|
||||
throw Error(`'${UNRELEASED_PLACEHOLDER}' is not the first section of 'CHANGELOG.md'`);
|
||||
}
|
||||
|
||||
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
|
||||
unreleasedSection.bodyLines = ["", ...lines, ""];
|
||||
return;
|
||||
}
|
||||
|
||||
unreleasedSection.bodyLines.pop(); // Remove the last empty line.
|
||||
unreleasedSection.bodyLines.push(...lines);
|
||||
unreleasedSection.bodyLines.push("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* entries that only apply to newer versions.
|
||||
*/
|
||||
export function processChangelogForBackports(
|
||||
sourceBranchMajorVersion: string,
|
||||
targetBranchMajorVersion: string,
|
||||
content: string,
|
||||
): string {
|
||||
// Changelog entries can use the following format to indicate
|
||||
// that they only apply to newer versions
|
||||
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
|
||||
|
||||
// Parse the changelog.
|
||||
const changelog = parseChangelog(content);
|
||||
|
||||
if (changelog.sections.length === 0) {
|
||||
throw new Error("Could not find any change sections in CHANGELOG.md");
|
||||
}
|
||||
|
||||
// Filter out changelog entries that only apply to newer versions and
|
||||
// update the section headings with the backport major version for
|
||||
// sections we keep.
|
||||
for (const section of changelog.sections) {
|
||||
// Update the section headings with the backport major version.
|
||||
section.headerLine = section.headerLine.replace(
|
||||
`## ${sourceBranchMajorVersion}`,
|
||||
`## ${targetBranchMajorVersion}`,
|
||||
);
|
||||
|
||||
const filteredEntries: string[] = [];
|
||||
let foundContent = false;
|
||||
|
||||
for (const line of section.bodyLines) {
|
||||
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
|
||||
// of the target branch is smaller than the required version.
|
||||
const match = someVersionsOnlyRegex.exec(line);
|
||||
if (
|
||||
match &&
|
||||
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep the line.
|
||||
filteredEntries.push(line);
|
||||
|
||||
// Set `foundContent` to `true` if the line is not empty.
|
||||
if (line.trim() !== "") {
|
||||
foundContent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the section with the retained entries.
|
||||
section.bodyLines = filteredEntries;
|
||||
|
||||
// Add an entry if we didn't keep any.
|
||||
if (!foundContent) {
|
||||
section.bodyLines.push(NO_CHANGES_STR);
|
||||
}
|
||||
}
|
||||
|
||||
return renderChangelog(changelog);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user