mirror of
https://github.com/github/codeql-action.git
synced 2026-10-03 17:41:28 +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 * as fs from "node:fs";
|
||||||
|
|
||||||
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
import { CHANGELOG_FILE, DryRunOption } from "./config";
|
||||||
|
|
||||||
/** The placeholder in the header for unreleased changes. */
|
/** The placeholder in the header for unreleased changes. */
|
||||||
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]";
|
||||||
|
|
||||||
/** The default contents for a section in the changelog. */
|
/** The default contents for a section in the changelog. */
|
||||||
export const NO_CHANGES_STR = "No user facing changes.";
|
export const NO_CHANGES_STR = "No user facing changes.";
|
||||||
|
|
||||||
/** Placeholder changelog content for a new release. */
|
/** Placeholder changelog content for a new release. */
|
||||||
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
export const EMPTY_CHANGELOG = `# CodeQL Action Changelog
|
||||||
|
|
||||||
## ${UNRELEASED_PLACEHOLDER}
|
## ${UNRELEASED_PLACEHOLDER}
|
||||||
|
|
||||||
${NO_CHANGES_STR}
|
${NO_CHANGES_STR}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents sections in a changelog.
|
* Represents sections in a changelog.
|
||||||
*/
|
*/
|
||||||
export interface ChangelogSection {
|
export interface ChangelogSection {
|
||||||
headerLine: string;
|
headerLine: string;
|
||||||
bodyLines: string[];
|
bodyLines: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a changelog.
|
* Represents a changelog.
|
||||||
*/
|
*/
|
||||||
export interface Changelog {
|
export interface Changelog {
|
||||||
preamble: string[];
|
preamble: string[];
|
||||||
sections: ChangelogSection[];
|
sections: ChangelogSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text of the header (without the '## ' prefix) of the given section.
|
* Returns the text of the header (without the '## ' prefix) of the given section.
|
||||||
* */
|
* */
|
||||||
export function getHeader(section: ChangelogSection): string {
|
export function getHeader(section: ChangelogSection): string {
|
||||||
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
|
return section.headerLine.replace(/^#+\s+/, "").trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns `date` formatted as `DD Mon YYYY`. */
|
/** Returns `date` formatted as `DD Mon YYYY`. */
|
||||||
export function getReleaseDateString(today: Date = new Date()): string {
|
export function getReleaseDateString(today: Date = new Date()): string {
|
||||||
return today.toLocaleDateString("en-GB", {
|
return today.toLocaleDateString("en-GB", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenChangelogOptions {
|
export interface OpenChangelogOptions {
|
||||||
initChangelog?: boolean;
|
initChangelog?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withChangelog(
|
export function withChangelog(
|
||||||
transformer: (contents: string) => string,
|
transformer: (contents: string) => string,
|
||||||
options: DryRunOption & OpenChangelogOptions,
|
options: DryRunOption & OpenChangelogOptions,
|
||||||
): void {
|
): void {
|
||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
|
if (options.initChangelog && !fs.existsSync(CHANGELOG_FILE)) {
|
||||||
content = EMPTY_CHANGELOG;
|
content = EMPTY_CHANGELOG;
|
||||||
} else {
|
} else {
|
||||||
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
|
content = fs.readFileSync(CHANGELOG_FILE, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
|
fs.writeFileSync(CHANGELOG_FILE, transformer(content), "utf8");
|
||||||
} else {
|
} else {
|
||||||
console.info(`[DRY RUN] Would have written updated changelog.`);
|
console.info(`[DRY RUN] Would have written updated changelog.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
|
* Updates the `[UNRELEASED]` marker in `CHANGELOG.md` with the given version
|
||||||
* and today's date.
|
* and today's date.
|
||||||
*/
|
*/
|
||||||
export function setVersionAndDate(
|
export function setVersionAndDate(
|
||||||
version: string,
|
version: string,
|
||||||
content: string,
|
content: string,
|
||||||
date: Date = new Date(),
|
date: Date = new Date(),
|
||||||
): string {
|
): string {
|
||||||
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
const versionAndDate = `${version} - ${getReleaseDateString(date)}`;
|
||||||
return content.replace(UNRELEASED_PLACEHOLDER, versionAndDate);
|
return content.replace(UNRELEASED_PLACEHOLDER, versionAndDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses `content` into a structured representation of a changelog.
|
* Parses `content` into a structured representation of a changelog.
|
||||||
*
|
*
|
||||||
* @param content The contents of the changelog file.
|
* @param content The contents of the changelog file.
|
||||||
*/
|
*/
|
||||||
export function parseChangelog(content: string): Changelog {
|
export function parseChangelog(content: string): Changelog {
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
const preamble: string[] = [];
|
const preamble: string[] = [];
|
||||||
const sections: ChangelogSection[] = [];
|
const sections: ChangelogSection[] = [];
|
||||||
let currentSection: ChangelogSection | undefined = undefined;
|
let currentSection: ChangelogSection | undefined = undefined;
|
||||||
|
|
||||||
// Process all lines of the input file.
|
// Process all lines of the input file.
|
||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
// Sections of the changelog start with `## `.
|
// Sections of the changelog start with `## `.
|
||||||
if (line.startsWith("## ")) {
|
if (line.startsWith("## ")) {
|
||||||
// We have discovered a new section. If `currentSection` is already defined,
|
// 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
|
// then this marks the end of that section. Push it to the array of sections
|
||||||
// in the changelog.
|
// in the changelog.
|
||||||
if (currentSection !== undefined) {
|
if (currentSection !== undefined) {
|
||||||
sections.push(currentSection);
|
sections.push(currentSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialise the new section.
|
// Initialise the new section.
|
||||||
currentSection = { headerLine: line, bodyLines: [] };
|
currentSection = { headerLine: line, bodyLines: [] };
|
||||||
} else if (currentSection !== undefined) {
|
} else if (currentSection !== undefined) {
|
||||||
// Add lines between the section header and the next to the current section.
|
// Add lines between the section header and the next to the current section.
|
||||||
currentSection.bodyLines.push(line);
|
currentSection.bodyLines.push(line);
|
||||||
} else {
|
} else {
|
||||||
// This is neither a section header nor are we in a section already,
|
// This is neither a section header nor are we in a section already,
|
||||||
// so this line is part of the preamble.
|
// so this line is part of the preamble.
|
||||||
preamble.push(line);
|
preamble.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the current section to the array of completed sections, if there is
|
// Push the current section to the array of completed sections, if there is
|
||||||
// still one unfinished.
|
// still one unfinished.
|
||||||
if (currentSection !== undefined) {
|
if (currentSection !== undefined) {
|
||||||
sections.push(currentSection);
|
sections.push(currentSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { preamble, sections };
|
return { preamble, sections };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts the changenotes `lines` in the `[UNRELEASED]` section of `changelog`.
|
* 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.
|
* `lines` will be inserted in place and the stock message will be deleted.
|
||||||
*
|
*
|
||||||
* This function will throw an exception if `[UNRELEASED]` does not exist.
|
* This function will throw an exception if `[UNRELEASED]` does not exist.
|
||||||
*
|
*
|
||||||
* @param changelog The CHANGELOG object to modify.
|
* @param changelog The CHANGELOG object to modify.
|
||||||
* @param lines The changenotes to insert.
|
* @param lines The changenotes to insert.
|
||||||
*/
|
*/
|
||||||
export function addBodyLinesToUnreleasedSection(
|
export function addBodyLinesToUnreleasedSection(
|
||||||
changelog: Changelog,
|
changelog: Changelog,
|
||||||
lines: string[],
|
lines: string[],
|
||||||
) {
|
) {
|
||||||
// Do nothing if there is nothing to insert.
|
// Do nothing if there is nothing to insert.
|
||||||
if (lines.length === 0) return;
|
if (lines.length === 0) return;
|
||||||
|
|
||||||
const unreleasedSection = changelog.sections[0];
|
const unreleasedSection = changelog.sections[0];
|
||||||
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
|
if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) {
|
||||||
throw Error("'[UNRELEASED]' is not the first section of 'CHANGELOG.md'");
|
throw Error(`'${UNRELEASED_PLACEHOLDER}' is not the first section of 'CHANGELOG.md'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
|
if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) {
|
||||||
unreleasedSection.bodyLines = ["", ...lines, ""];
|
unreleasedSection.bodyLines = ["", ...lines, ""];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unreleasedSection.bodyLines.pop(); // Remove the last empty line.
|
unreleasedSection.bodyLines.pop(); // Remove the last empty line.
|
||||||
unreleasedSection.bodyLines.push(...lines);
|
unreleasedSection.bodyLines.push(...lines);
|
||||||
unreleasedSection.bodyLines.push("");
|
unreleasedSection.bodyLines.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines an array of lines into a single string by adding line breaks.
|
* Combines an array of lines into a single string by adding line breaks.
|
||||||
*/
|
*/
|
||||||
export function unlines(lines: string[]): string {
|
export function unlines(lines: string[]): string {
|
||||||
return `${lines.join("\n")}`;
|
return `${lines.join("\n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a given changelog to a string.
|
* Renders a given changelog to a string.
|
||||||
*/
|
*/
|
||||||
export function renderChangelog(changelog: Changelog): string {
|
export function renderChangelog(changelog: Changelog): string {
|
||||||
let result = unlines(changelog.preamble);
|
let result = unlines(changelog.preamble);
|
||||||
|
|
||||||
for (const section of changelog.sections) {
|
for (const section of changelog.sections) {
|
||||||
result += `\n${section.headerLine}\n${unlines(section.bodyLines)}`;
|
result += `\n${section.headerLine}\n${unlines(section.bodyLines)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes changelog entries for a backport, converting version references
|
* Processes changelog entries for a backport, converting version references
|
||||||
* from the source major version to the target major version and filtering
|
* from the source major version to the target major version and filtering
|
||||||
* entries that only apply to newer versions.
|
* entries that only apply to newer versions.
|
||||||
*/
|
*/
|
||||||
export function processChangelogForBackports(
|
export function processChangelogForBackports(
|
||||||
sourceBranchMajorVersion: string,
|
sourceBranchMajorVersion: string,
|
||||||
targetBranchMajorVersion: string,
|
targetBranchMajorVersion: string,
|
||||||
content: string,
|
content: string,
|
||||||
): string {
|
): string {
|
||||||
// Changelog entries can use the following format to indicate
|
// Changelog entries can use the following format to indicate
|
||||||
// that they only apply to newer versions
|
// that they only apply to newer versions
|
||||||
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
|
const someVersionsOnlyRegex = /\[v(\d+)\+ only\]/;
|
||||||
|
|
||||||
// Parse the changelog.
|
// Parse the changelog.
|
||||||
const changelog = parseChangelog(content);
|
const changelog = parseChangelog(content);
|
||||||
|
|
||||||
if (changelog.sections.length === 0) {
|
if (changelog.sections.length === 0) {
|
||||||
throw new Error("Could not find any change sections in CHANGELOG.md");
|
throw new Error("Could not find any change sections in CHANGELOG.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out changelog entries that only apply to newer versions and
|
// Filter out changelog entries that only apply to newer versions and
|
||||||
// update the section headings with the backport major version for
|
// update the section headings with the backport major version for
|
||||||
// sections we keep.
|
// sections we keep.
|
||||||
for (const section of changelog.sections) {
|
for (const section of changelog.sections) {
|
||||||
// Update the section headings with the backport major version.
|
// Update the section headings with the backport major version.
|
||||||
section.headerLine = section.headerLine.replace(
|
section.headerLine = section.headerLine.replace(
|
||||||
`## ${sourceBranchMajorVersion}`,
|
`## ${sourceBranchMajorVersion}`,
|
||||||
`## ${targetBranchMajorVersion}`,
|
`## ${targetBranchMajorVersion}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredEntries: string[] = [];
|
const filteredEntries: string[] = [];
|
||||||
let foundContent = false;
|
let foundContent = false;
|
||||||
|
|
||||||
for (const line of section.bodyLines) {
|
for (const line of section.bodyLines) {
|
||||||
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
|
// Skip the entry if `someVersionsOnlyRegex` matches and the major version
|
||||||
// of the target branch is smaller than the required version.
|
// of the target branch is smaller than the required version.
|
||||||
const match = someVersionsOnlyRegex.exec(line);
|
const match = someVersionsOnlyRegex.exec(line);
|
||||||
if (
|
if (
|
||||||
match &&
|
match &&
|
||||||
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
Number.parseInt(targetBranchMajorVersion) < Number.parseInt(match[1])
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the line.
|
// Keep the line.
|
||||||
filteredEntries.push(line);
|
filteredEntries.push(line);
|
||||||
|
|
||||||
// Set `foundContent` to `true` if the line is not empty.
|
// Set `foundContent` to `true` if the line is not empty.
|
||||||
if (line.trim() !== "") {
|
if (line.trim() !== "") {
|
||||||
foundContent = true;
|
foundContent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the section with the retained entries.
|
// Update the section with the retained entries.
|
||||||
section.bodyLines = filteredEntries;
|
section.bodyLines = filteredEntries;
|
||||||
|
|
||||||
// Add an entry if we didn't keep any.
|
// Add an entry if we didn't keep any.
|
||||||
if (!foundContent) {
|
if (!foundContent) {
|
||||||
section.bodyLines.push(NO_CHANGES_STR);
|
section.bodyLines.push(NO_CHANGES_STR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderChangelog(changelog);
|
return renderChangelog(changelog);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user