From ba8088007998204f75ccb12b2fdbb870c1071a72 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:11:14 +0000 Subject: [PATCH 01/35] Update changelog and version after v4.38.1 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a7189e20..267b4e557e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs. +## [UNRELEASED] + +No user facing changes. + ## 4.38.1 - 18 Sept 2026 - The CodeQL Action now has experimental support for CodeQL releases for which per-language bundles are available. Per-language bundles support analysis for a single language and are therefore smaller than the combined bundles that allow analysis for all supported languages. As a result, per-language bundles take up less space on disk and are faster to download. We expect to roll this change out to everyone in the coming weeks. [#4146](https://github.com/github/codeql-action/pull/4146) diff --git a/package-lock.json b/package-lock.json index 17a4eee8c3..d4f189db2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeql", - "version": "4.38.1", + "version": "4.38.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeql", - "version": "4.38.1", + "version": "4.38.2", "license": "MIT", "workspaces": [ "pr-checks" diff --git a/package.json b/package.json index c7ad53e2e0..7f31e80e93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeql", - "version": "4.38.1", + "version": "4.38.2", "private": true, "description": "CodeQL action", "scripts": { From f8b1c08e6dfaa417c8626f557accae8a15613e6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:11:23 +0000 Subject: [PATCH 02/35] Rebuild --- lib/entry-points.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index f8a7d6e76a..0f1282075e 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146167,7 +146167,7 @@ function getDiffRangesJsonFilePath(env = getEnv()) { return path2.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } function getActionVersion() { - return "4.38.1"; + return "4.38.2"; } function getWorkflowEventName(env = getEnv()) { return env.getRequired("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); From cb31eabcd8c75b939c159b0f04b821a0bfd130f6 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 09:04:47 -0500 Subject: [PATCH 03/35] Do not include trailing newlines in `NO_CHANGES_STR` Changing `NO_CHANGES_STR` to just be the text will make it easier to insert/use. To not break anything, I added the deleted newlines to the locations where `NO_CHANGES_STR` was used. --- pr-checks/bundle-changelog.test.ts | 4 ++-- pr-checks/changelog.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pr-checks/bundle-changelog.test.ts b/pr-checks/bundle-changelog.test.ts index 6cc4d096ba..fad06826a6 100644 --- a/pr-checks/bundle-changelog.test.ts +++ b/pr-checks/bundle-changelog.test.ts @@ -112,7 +112,7 @@ ${NO_CHANGES_STR}`; describe("updateChangelog", async () => { await it("removes `NO_CHANGES_STR` if present in [UNRELEASED] section", async () => { const result = updateChangelog(EMPTY_CHANGELOG, ""); - assert.ok(!result.includes(NO_CHANGES_STR.trim())); + assert.ok(!result.includes(NO_CHANGES_STR)); }); await it("doesn't remove `NO_CHANGES_STR` if present in versioned section", async () => { @@ -120,7 +120,7 @@ describe("updateChangelog", async () => { EMPTY_CHANGELOG.replace(UNRELEASED_PLACEHOLDER, "1.2.3"), "", ); - assert.ok(result.includes(NO_CHANGES_STR.trim())); + assert.ok(result.includes(NO_CHANGES_STR)); }); await it("throws if there are no sections", async () => { diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index 4cf1e75494..fc17199d6a 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -6,14 +6,16 @@ import { CHANGELOG_FILE, DryRunOption } from "./config"; export const UNRELEASED_PLACEHOLDER = "[UNRELEASED]"; /** The default contents for a section in the changelog. */ -export const NO_CHANGES_STR = "No user facing changes.\n\n"; +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}`; +${NO_CHANGES_STR} + +`; /** * Represents sections in a changelog. @@ -204,7 +206,7 @@ export function processChangelogForBackports( // Add an entry if we didn't keep any. if (!foundContent) { - section.bodyLines.push(NO_CHANGES_STR.trim()); + section.bodyLines.push(NO_CHANGES_STR); } } From 47d607e2c1eebf3c9382b1872b2f902a7915bbc4 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 09:06:31 -0500 Subject: [PATCH 04/35] Add changelog parsing helper `getHeader` --- pr-checks/changelog.test.ts | 19 +++++++++++++++++++ pr-checks/changelog.ts | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/pr-checks/changelog.test.ts b/pr-checks/changelog.test.ts index 817852e3e1..45658154fc 100755 --- a/pr-checks/changelog.test.ts +++ b/pr-checks/changelog.test.ts @@ -10,6 +10,7 @@ import { describe, it } from "node:test"; import { EMPTY_CHANGELOG, + getHeader, getReleaseDateString, parseChangelog, processChangelogForBackports, @@ -20,6 +21,24 @@ import { CHANGELOG_FILE } from "./config"; const testDate = new Date(2026, 7, 14); +describe("getHeader", async () => { + await it("returns non-headers unchanged", () => { + assert.equal("foo", getHeader("foo")); + assert.equal("- bar", getHeader("- bar")); + }); + await it("strips octothorpes", async () => { + assert.equal("foo", getHeader("# foo")); + assert.equal("foo", getHeader("## foo")); + assert.equal("foo", getHeader("### foo")); + assert.equal("foo", getHeader("#### foo")); + assert.equal("foo", getHeader("##### foo")); + assert.equal("foo", getHeader("###### foo")); + }); + await it("strips whitespace", async () => { + assert.equal("foo", getHeader("# foo ")); + }); +}); + describe("getReleaseDateString", async () => { await it("formats dates as expected", async () => { assert.equal(getReleaseDateString(testDate), "14 Aug 2026"); diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index fc17199d6a..8265430243 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -33,6 +33,11 @@ export interface Changelog { sections: ChangelogSection[]; } +/** Returns the text of a CHANGELOG.md header (without the '## ' prefix). */ +export function getHeader(headerLine: string): string { + return headerLine.replace(/^#+\s+/, "").trimEnd(); +} + /** Returns `date` formatted as `DD Mon YYYY`. */ export function getReleaseDateString(today: Date = new Date()): string { return today.toLocaleDateString("en-GB", { From bb1dc5460bab614d5c8c05120b7e5465e04d8417 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 09:07:44 -0500 Subject: [PATCH 05/35] Add CHANGELOG function `addBodyLinesToUnreleasedSection` This will be used by the `pr-checks/changenotes.mts` script to "compile" the latest release entry of CHANGELOG.md. --- pr-checks/changelog.test.ts | 79 +++++++++++++++++++++++++++++++++++++ pr-checks/changelog.ts | 53 +++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/pr-checks/changelog.test.ts b/pr-checks/changelog.test.ts index 45658154fc..b88f9b0c07 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 8265430243..ec765b4c8f 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. */ From 1ee32652624bab3b2447ddd6f0d5794ec2ea6f1a Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 09:23:03 -0500 Subject: [PATCH 06/35] Add `changenotes.mts flush` command This command will "flush" or move the changenotes in the `unreleased-change-notes` directory to the `[UNRELEASED]` section of the CHANGELOG.md file. --- pr-checks/changenotes.mts | 52 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index 2fb86b0cac..c7a10d0229 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -1,11 +1,20 @@ #!/usr/bin/env npx tsx import * as fs from "node:fs"; +import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; +import path from "path"; +import { matter } from "lite-matter"; + +import { + addBodyLinesToUnreleasedSection, + parseChangelog, + renderChangelog, +} from "./changelog"; import { isValidAllChangenoteFiles } from "./changelog/validate.mjs"; -import { CHANGENOTES_DIR } from "./config"; +import { CHANGELOG_FILE, CHANGENOTES_DIR } from "./config"; const entryPoint = process.argv[1]; if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { @@ -27,6 +36,8 @@ function main(): number { case undefined: case "help": return usage(); + case "flush": + return flush(); case "validate": return validate(); default: @@ -36,10 +47,47 @@ function main(): number { } function usage(): number { - console.log(`Usage: changenotes.mts validate`); + const message = + "Usage: changenotes.mts flush\n" + + " changenotes.mts validate\n" + + " changenotes.mts help"; + console.log(message); return 0; } +function flush(): number { + try { + // Get the file paths to our changenotes; these will be useful later. + const changenotePaths = fs + .readdirSync(CHANGENOTES_DIR) + .filter((name) => name !== ".gitkeep") + .map((name) => path.join(CHANGENOTES_DIR, name)); + + // From the file paths, we read the files to obtain the actual notes themselves. + const changenotes = changenotePaths.map((filePath) => { + const fileBody = readFileSync(filePath).toString(); + const { content } = matter(fileBody); + return content.trim(); + }); + + const changelogContents = fs.readFileSync(CHANGELOG_FILE).toString(); + const changelog = parseChangelog(changelogContents); + addBodyLinesToUnreleasedSection(changelog, changenotes); + fs.writeFileSync(CHANGELOG_FILE, renderChangelog(changelog)); + + // Delete changenotes only after successful processing. + for (const p of changenotePaths) { + fs.unlinkSync(p); + } + + return 0; + } catch (e) { + console.error("Failed to flush changenotes to 'CHANGELOG.md'", e); + } + + return 1; +} + function validate(): number { try { if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) { From bffae1c4b8f56e1f31092864ec57102c0c026ac2 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 11:22:39 -0500 Subject: [PATCH 07/35] Use `withChangelog` I/O helper --- pr-checks/changenotes.mts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index c7a10d0229..06b10da8eb 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -12,9 +12,10 @@ import { addBodyLinesToUnreleasedSection, parseChangelog, renderChangelog, + withChangelog, } from "./changelog"; import { isValidAllChangenoteFiles } from "./changelog/validate.mjs"; -import { CHANGELOG_FILE, CHANGENOTES_DIR } from "./config"; +import { CHANGENOTES_DIR } from "./config"; const entryPoint = process.argv[1]; if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { @@ -70,10 +71,11 @@ function flush(): number { return content.trim(); }); - const changelogContents = fs.readFileSync(CHANGELOG_FILE).toString(); - const changelog = parseChangelog(changelogContents); - addBodyLinesToUnreleasedSection(changelog, changenotes); - fs.writeFileSync(CHANGELOG_FILE, renderChangelog(changelog)); + withChangelog((contents) => { + const changelog = parseChangelog(contents); + addBodyLinesToUnreleasedSection(changelog, changenotes); + return renderChangelog(changelog); + }, {}); // Delete changenotes only after successful processing. for (const p of changenotePaths) { From b246e5606946f9e44193fd31a37458b82052507a Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 11:28:17 -0500 Subject: [PATCH 08/35] Use `ExitCode` instead of `0`/`1` --- pr-checks/changenotes.mts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index 06b10da8eb..fd3225b911 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import path from "path"; +import { ExitCode } from "@actions/core"; import { matter } from "lite-matter"; import { @@ -23,11 +24,11 @@ if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { process.exit(main()); } catch (error) { console.error(error); - process.exit(1); + process.exit(ExitCode.Failure); } } -function main(): number { +function main(): ExitCode { const { positionals } = parseArgs({ allowPositionals: true, strict: true, @@ -43,20 +44,20 @@ function main(): number { return validate(); default: console.error(`Unknown command: ${command}`); - return 1; + return ExitCode.Failure; } } -function usage(): number { +function usage(): ExitCode { const message = "Usage: changenotes.mts flush\n" + " changenotes.mts validate\n" + " changenotes.mts help"; console.log(message); - return 0; + return ExitCode.Success; } -function flush(): number { +function flush(): ExitCode { try { // Get the file paths to our changenotes; these will be useful later. const changenotePaths = fs @@ -82,19 +83,19 @@ function flush(): number { fs.unlinkSync(p); } - return 0; + return ExitCode.Success; } catch (e) { console.error("Failed to flush changenotes to 'CHANGELOG.md'", e); } - return 1; + return ExitCode.Failure; } -function validate(): number { +function validate(): ExitCode { try { if (isValidAllChangenoteFiles(fs.readdirSync(CHANGENOTES_DIR))) { console.log(`All changenotes in '${CHANGENOTES_DIR}' are valid.`); - return 0; + return ExitCode.Success; } } catch (error) { console.error( @@ -102,5 +103,5 @@ function validate(): number { error, ); } - return 1; + return ExitCode.Failure; } From d63b2a40db843ae6d4d33fcca9dbf0cd3e5384e0 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 11:42:46 -0500 Subject: [PATCH 09/35] Assume '[UNRELEASED]' section is first section --- pr-checks/changelog.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index ec765b4c8f..e738f2a4a1 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -146,20 +146,9 @@ 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", - ); + const unreleasedSection = changelog.sections[0]; + if (getHeader(unreleasedSection.headerLine) !== UNRELEASED_PLACEHOLDER) { + throw Error("'[UNRELEASED]' is not the first section of 'CHANGELOG.md'"); } let insertAtIndex = 0; From 128614ad8b661f91e7038d97c93e89fbe6c26857 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 12:24:19 -0500 Subject: [PATCH 10/35] Simplify `getHeader` to operate on `ChangelogSection`s --- pr-checks/changelog.test.ts | 24 +++++++++++++++--------- pr-checks/changelog.ts | 10 ++++++---- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pr-checks/changelog.test.ts b/pr-checks/changelog.test.ts index b88f9b0c07..2e8712ea8b 100755 --- a/pr-checks/changelog.test.ts +++ b/pr-checks/changelog.test.ts @@ -26,20 +26,26 @@ import { CHANGELOG_FILE } from "./config"; const testDate = new Date(2026, 7, 14); describe("getHeader", async () => { + function Section(headerLine: string): ChangelogSection { + return { + headerLine, + bodyLines: [], + }; + } await it("returns non-headers unchanged", () => { - assert.equal("foo", getHeader("foo")); - assert.equal("- bar", getHeader("- bar")); + assert.equal("foo", getHeader(Section("foo"))); + assert.equal("- bar", getHeader(Section("- bar"))); }); await it("strips octothorpes", async () => { - assert.equal("foo", getHeader("# foo")); - assert.equal("foo", getHeader("## foo")); - assert.equal("foo", getHeader("### foo")); - assert.equal("foo", getHeader("#### foo")); - assert.equal("foo", getHeader("##### foo")); - assert.equal("foo", getHeader("###### foo")); + assert.equal("foo", getHeader(Section("# foo"))); + assert.equal("foo", getHeader(Section("## foo"))); + assert.equal("foo", getHeader(Section("### foo"))); + assert.equal("foo", getHeader(Section("#### foo"))); + assert.equal("foo", getHeader(Section("##### foo"))); + assert.equal("foo", getHeader(Section("###### foo"))); }); await it("strips whitespace", async () => { - assert.equal("foo", getHeader("# foo ")); + assert.equal("foo", getHeader(Section("# foo "))); }); }); diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index e738f2a4a1..fa992d6eda 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -33,9 +33,11 @@ export interface Changelog { sections: ChangelogSection[]; } -/** Returns the text of a CHANGELOG.md header (without the '## ' prefix). */ -export function getHeader(headerLine: string): string { - return headerLine.replace(/^#+\s+/, "").trimEnd(); +/** + * 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`. */ @@ -147,7 +149,7 @@ export function addBodyLinesToUnreleasedSection( lines: string[], ) { const unreleasedSection = changelog.sections[0]; - if (getHeader(unreleasedSection.headerLine) !== UNRELEASED_PLACEHOLDER) { + if (getHeader(unreleasedSection) !== UNRELEASED_PLACEHOLDER) { throw Error("'[UNRELEASED]' is not the first section of 'CHANGELOG.md'"); } From bc0efd6d9138ed65d251d7762a29ddd22614fc6a Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 12:35:41 -0500 Subject: [PATCH 11/35] Simplify `addBodyLinesToUnreleasedSection` --- pr-checks/changelog.test.ts | 17 ++++++----------- pr-checks/changelog.ts | 27 ++++++++------------------- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/pr-checks/changelog.test.ts b/pr-checks/changelog.test.ts index 2e8712ea8b..8132e65766 100755 --- a/pr-checks/changelog.test.ts +++ b/pr-checks/changelog.test.ts @@ -108,25 +108,20 @@ describe("addBodyLinesToUnreleasedSection", async () => { }; } - await it("throws error if '[UNRELEASED]' section does not exist", async () => { - const emptyChangelog = newChangelogWithSections([]); - assert.throws(() => addBodyLinesToUnreleasedSection(emptyChangelog, [])); - - const releasedChangelog = newChangelogWithSections([ + await it("throws error if '[UNRELEASED]' section is not first", async () => { + const invalidChangelog = newChangelogWithSections([ { headerLine: "## Release 1.0.0", bodyLines: [], }, { - headerLine: "## Release 2.0.0", - bodyLines: [], - }, - { - headerLine: "## Release 3.0.0", + headerLine: `## ${UNRELEASED_PLACEHOLDER}`, bodyLines: [], }, ]); - assert.throws(() => addBodyLinesToUnreleasedSection(releasedChangelog, [])); + assert.throws(() => + addBodyLinesToUnreleasedSection(invalidChangelog, ["foo"]), + ); }); await it("overwrites 'No user facing changes.'", async () => { diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index fa992d6eda..e159a06a98 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -148,32 +148,21 @@ 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'"); } - 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. + if (unreleasedSection.bodyLines.includes(NO_CHANGES_STR)) { + unreleasedSection.bodyLines = ["", ...lines, ""]; + return; } - unreleasedSection.bodyLines.splice(insertAtIndex, deleteCount, ...lines); + // Insert `lines` after the first blank line. + unreleasedSection.bodyLines.splice(1, 0, ...lines); } /** From 977b29b897eab1468a089ac1278b1b7c351bcffd Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 13:47:06 -0500 Subject: [PATCH 12/35] Replace `splice` with `push` and `pop` --- pr-checks/changelog.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index e159a06a98..18bffa51a3 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -161,8 +161,9 @@ export function addBodyLinesToUnreleasedSection( return; } - // Insert `lines` after the first blank line. - unreleasedSection.bodyLines.splice(1, 0, ...lines); + unreleasedSection.bodyLines.pop(); // Remove the last empty line. + unreleasedSection.bodyLines.push(...lines); + unreleasedSection.bodyLines.push(""); } /** From 7f54212a01b1fcd5c22896032d6c677591788874 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 13:49:18 -0500 Subject: [PATCH 13/35] Rename `flush` command to `assemble` --- pr-checks/changenotes.mts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index fd3225b911..6480b19537 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -38,8 +38,8 @@ function main(): ExitCode { case undefined: case "help": return usage(); - case "flush": - return flush(); + case "assemble": + return assemble(); case "validate": return validate(); default: @@ -50,14 +50,14 @@ function main(): ExitCode { function usage(): ExitCode { const message = - "Usage: changenotes.mts flush\n" + + "Usage: changenotes.mts assemble\n" + " changenotes.mts validate\n" + " changenotes.mts help"; console.log(message); return ExitCode.Success; } -function flush(): ExitCode { +function assemble(): ExitCode { try { // Get the file paths to our changenotes; these will be useful later. const changenotePaths = fs @@ -85,7 +85,7 @@ function flush(): ExitCode { return ExitCode.Success; } catch (e) { - console.error("Failed to flush changenotes to 'CHANGELOG.md'", e); + console.error("Failed to assemble changenotes to 'CHANGELOG.md'", e); } return ExitCode.Failure; From a92f7fb68832cf532975c676aac5741c34cc63b2 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Fri, 18 Sep 2026 15:01:59 -0500 Subject: [PATCH 14/35] Refactor changenote file listing into function to D.R.Y. --- pr-checks/changenotes.mts | 51 ++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index 6480b19537..9a5c21cf03 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -1,7 +1,6 @@ #!/usr/bin/env npx tsx import * as fs from "node:fs"; -import { readFileSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { parseArgs } from "node:util"; import path from "path"; @@ -18,6 +17,39 @@ import { import { isValidAllChangenoteFiles } from "./changelog/validate.mjs"; import { CHANGENOTES_DIR } from "./config"; +/** + * Describes a changenote file, including its file path, frontmatter, and content. + */ +interface ChangenoteFile { + name: string; + data: Record; + content: string; +} + +/** + * Returns the absolute file paths of all files in + * {@link CHANGENOTES_DIR} (except ".gitkeep"). + * */ +function listUnreleasedChangenoteDir(): string[] { + return fs + .readdirSync(CHANGENOTES_DIR) + .filter((name) => name !== ".gitkeep") + .map((name) => path.join(CHANGENOTES_DIR, name)); +} + +/** + * Scans the {@link CHANGENOTES_DIR} directory for changenote files + * and returns a parsed listing of those changenote files. + */ +function getChangenotes(): ChangenoteFile[] { + return listUnreleasedChangenoteDir().map((name) => { + return { + name, + ...matter(fs.readFileSync(name, "utf-8")), + }; + }); +} + const entryPoint = process.argv[1]; if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { try { @@ -59,22 +91,13 @@ function usage(): ExitCode { function assemble(): ExitCode { try { - // Get the file paths to our changenotes; these will be useful later. - const changenotePaths = fs - .readdirSync(CHANGENOTES_DIR) - .filter((name) => name !== ".gitkeep") - .map((name) => path.join(CHANGENOTES_DIR, name)); - - // From the file paths, we read the files to obtain the actual notes themselves. - const changenotes = changenotePaths.map((filePath) => { - const fileBody = readFileSync(filePath).toString(); - const { content } = matter(fileBody); - return content.trim(); - }); + const changenotes = getChangenotes(); + const changenoteBodies = changenotes.map((c) => c.content); + const changenotePaths = changenotes.map((c) => c.name); withChangelog((contents) => { const changelog = parseChangelog(contents); - addBodyLinesToUnreleasedSection(changelog, changenotes); + addBodyLinesToUnreleasedSection(changelog, changenoteBodies); return renderChangelog(changelog); }, {}); From 771560691a49adf7ee17519f5091ca8f747d0ce9 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:28:36 -0500 Subject: [PATCH 15/35] Update JSDoc comment with parameter `lines` Co-authored-by: Michael B. Gale --- pr-checks/changelog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index 18bffa51a3..45a7977126 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -135,9 +135,9 @@ export function parseChangelog(content: string): Changelog { } /** - * Inserts the changenotes `notes` under 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 - * `notes` 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. * From 598cda36cf3f4ab1d9ab514b3015be59b2b46eed Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:28:52 -0500 Subject: [PATCH 16/35] Apply suggestion from @mbg Co-authored-by: Michael B. Gale --- pr-checks/changelog.ts | 502 ++++++++++++++++++++--------------------- 1 file changed, 251 insertions(+), 251 deletions(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index 45a7977126..76a3093456 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -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); +} From c496c6cceb2cee0566ea8dd194c7915be1420850 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:31:55 -0500 Subject: [PATCH 17/35] Rename `name` to `absolutePath` for clarity --- pr-checks/changenotes.mts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pr-checks/changenotes.mts b/pr-checks/changenotes.mts index 9a5c21cf03..d19d2da83b 100755 --- a/pr-checks/changenotes.mts +++ b/pr-checks/changenotes.mts @@ -21,7 +21,7 @@ import { CHANGENOTES_DIR } from "./config"; * Describes a changenote file, including its file path, frontmatter, and content. */ interface ChangenoteFile { - name: string; + absolutePath: string; data: Record; content: string; } @@ -42,10 +42,10 @@ function listUnreleasedChangenoteDir(): string[] { * and returns a parsed listing of those changenote files. */ function getChangenotes(): ChangenoteFile[] { - return listUnreleasedChangenoteDir().map((name) => { + return listUnreleasedChangenoteDir().map((absolutePath) => { return { - name, - ...matter(fs.readFileSync(name, "utf-8")), + absolutePath, + ...matter(fs.readFileSync(absolutePath, "utf-8")), }; }); } @@ -93,7 +93,7 @@ function assemble(): ExitCode { try { const changenotes = getChangenotes(); const changenoteBodies = changenotes.map((c) => c.content); - const changenotePaths = changenotes.map((c) => c.name); + const changenotePaths = changenotes.map((c) => c.absolutePath); withChangelog((contents) => { const changelog = parseChangelog(contents); From 02631222091e92e55383c4e23534b590cadc795a Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:32:44 -0500 Subject: [PATCH 18/35] Format code with `npm run lint-fix` --- pr-checks/changelog.ts | 508 +++++++++++++++++++++-------------------- 1 file changed, 255 insertions(+), 253 deletions(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index 76a3093456..bee141d2fd 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -1,253 +1,255 @@ -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 - * `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_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); -} +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 + * `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_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); +} From c0bd54fdf4a49ae453d9ad729e6898dd6fd0a24b Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:36:10 -0500 Subject: [PATCH 19/35] Replace JSDoc text with `@throws` --- pr-checks/changelog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index bee141d2fd..af79666d28 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -139,7 +139,7 @@ export function parseChangelog(content: string): Changelog { * 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. + * @throws Error -- if the [UNRELEASED] section does not exist. * * @param changelog The CHANGELOG object to modify. * @param lines The changenotes to insert. From b1668d6234eb740e49d28a1b1b78a3947d629516 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Mon, 21 Sep 2026 16:40:26 -0500 Subject: [PATCH 20/35] Flesh out a comment --- pr-checks/changelog.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pr-checks/changelog.ts b/pr-checks/changelog.ts index af79666d28..496310d21f 100644 --- a/pr-checks/changelog.ts +++ b/pr-checks/changelog.ts @@ -163,7 +163,9 @@ export function addBodyLinesToUnreleasedSection( return; } - unreleasedSection.bodyLines.pop(); // Remove the last empty line. + // The last body line should be a blank line (for spacing). + // Remove it so that we can add `lines` and then add the blank line back. + unreleasedSection.bodyLines.pop(); unreleasedSection.bodyLines.push(...lines); unreleasedSection.bodyLines.push(""); } From 669351e8804d1bed1bd9f9ae4b4d3542e7b495b1 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 16 Sep 2026 12:29:46 +0100 Subject: [PATCH 21/35] Replace `getRequiredEnvParam` calls in `init` and `setup-codeql` action --- lib/entry-points.js | 14 ++++++++------ src/init-action.ts | 13 +++++++------ src/setup-codeql-action.ts | 7 +++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0f1282075e..ebb980a70b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -162239,8 +162239,8 @@ async function run3(actionState) { apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL") + url: actionState.env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), + apiURL: actionState.env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) }; const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger); @@ -162259,7 +162259,7 @@ async function run3(actionState) { const repositoryProperties = repositoryPropertiesResult.orElse({}); core22.exportVariable("CODEQL_ACTION_INIT_HAS_RUN" /* INIT_ACTION_HAS_RUN */, "true"); sourceRoot = path25.resolve( - getRequiredEnvParam("GITHUB_WORKSPACE"), + actionState.env.getRequired("GITHUB_WORKSPACE" /* GITHUB_WORKSPACE */), getOptionalInput("source-root") || "" ); let analysisKinds; @@ -162355,7 +162355,9 @@ async function run3(actionState) { repository: repositoryNwo, tempDir: getTemporaryDirectory(), codeql, - workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"), + workspacePath: actionState.env.getRequired( + "GITHUB_WORKSPACE" /* GITHUB_WORKSPACE */ + ), sourceRoot, githubVersion: gitHubVersion, apiDetails, @@ -163254,8 +163256,8 @@ async function run6(actionState) { const apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL") + url: actionState.env.getRequired("GITHUB_SERVER_URL" /* GITHUB_SERVER_URL */), + apiURL: actionState.env.getRequired("GITHUB_API_URL" /* GITHUB_API_URL */) }; const gitHubVersion = await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger); diff --git a/src/init-action.ts b/src/init-action.ts index 79c509a5be..e770fe9788 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -38,7 +38,7 @@ import { makeDiagnostic, makeTelemetryDiagnostic, } from "./diagnostics"; -import { EnvVar } from "./environment"; +import { ActionsEnvVars, EnvVar } from "./environment"; import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; import { loadRepositoryProperties } from "./feature-flags/properties"; import { @@ -81,7 +81,6 @@ import { DEFAULT_DEBUG_ARTIFACT_NAME, DEFAULT_DEBUG_DATABASE_NAME, getCodeQLMemoryLimit, - getRequiredEnvParam, getThreadsFlagValue, initializeEnvironment, ConfigurationError, @@ -225,8 +224,8 @@ async function run( apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL"), + url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), + apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL), }; const gitHubVersion = await getGitHubVersion(); @@ -255,7 +254,7 @@ async function run( // source-root is relative, it is relative to the GITHUB_WORKSPACE. If // source-root is absolute, it is used as given. sourceRoot = path.resolve( - getRequiredEnvParam("GITHUB_WORKSPACE"), + actionState.env.getRequired(ActionsEnvVars.GITHUB_WORKSPACE), getOptionalInput("source-root") || "", ); @@ -383,7 +382,9 @@ async function run( repository: repositoryNwo, tempDir: getTemporaryDirectory(), codeql, - workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"), + workspacePath: actionState.env.getRequired( + ActionsEnvVars.GITHUB_WORKSPACE, + ), sourceRoot, githubVersion: gitHubVersion, apiDetails, diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 4bd53e517f..91666f19cd 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -12,7 +12,7 @@ import { getGitHubVersion } from "./api-client"; import { CodeQL } from "./codeql"; import { ComputedInput, getToolsInput } from "./config/inputs"; import { getRawLanguagesNoAutodetect } from "./config-utils"; -import { EnvVar } from "./environment"; +import { ActionsEnvVars, EnvVar } from "./environment"; import { initFeatures } from "./feature-flags"; import { loadRepositoryProperties } from "./feature-flags/properties"; import { initCodeQL } from "./init"; @@ -32,7 +32,6 @@ import { checkDiskUsage, checkForTimeout, checkGitHubVersionInRange, - getRequiredEnvParam, initializeEnvironment, ConfigurationError, wrapError, @@ -108,8 +107,8 @@ async function run( const apiDetails = { auth: getRequiredInput("token"), externalRepoAuth: getOptionalInput("external-repository-token"), - url: getRequiredEnvParam("GITHUB_SERVER_URL"), - apiURL: getRequiredEnvParam("GITHUB_API_URL"), + url: actionState.env.getRequired(ActionsEnvVars.GITHUB_SERVER_URL), + apiURL: actionState.env.getRequired(ActionsEnvVars.GITHUB_API_URL), }; const gitHubVersion = await getGitHubVersion(); From 3a30b151d697aa175402c28a99aca0d24aef4d0c Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 16 Sep 2026 12:35:13 +0100 Subject: [PATCH 22/35] Refactor `setupDiffInformedQueryRun` querying `checkout_path` itself --- lib/entry-points.js | 16 +++++++++++----- src/analyze-action.ts | 14 +++++++++++--- src/analyze.ts | 4 ++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ebb980a70b..c49550e59b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -153887,7 +153887,7 @@ async function finalizeDatabaseCreation(codeql, features, config, threadsFlag, m trap_import_duration_ms: Math.round(trapImportTime) }; } -async function setupDiffInformedQueryRun(logger) { +async function setupDiffInformedQueryRun(logger, checkoutPath) { return await withGroupAsync( "Generating diff range extension pack", async () => { @@ -153898,7 +153898,6 @@ async function setupDiffInformedQueryRun(logger) { ); return void 0; } - const checkoutPath = getRequiredInput("checkout_path"); const packDir = writeDiffRangeDataExtensionPack( logger, diffRanges, @@ -156547,7 +156546,11 @@ async function runAutobuildIfLegacyGoWorkflow(config, logger) { ); await runAutobuild(config, "go" /* go */, logger); } -async function run({ startedAt, logger }) { +async function run({ + startedAt, + logger, + actions +}) { let uploadResults = void 0; let runStats = void 0; let config = void 0; @@ -156613,7 +156616,11 @@ async function run({ startedAt, logger }) { getOptionalInput("ram") || process.env["CODEQL_RAM"], logger ); - const diffRangePackDir = await setupDiffInformedQueryRun(logger); + const checkoutPath = actions.getRequiredInput("checkout_path"); + const diffRangePackDir = await setupDiffInformedQueryRun( + logger, + checkoutPath + ); await warnIfGoInstalledAfterInit(config, logger); await runAutobuildIfLegacyGoWorkflow(config, logger); dbCreationTimings = await runFinalize( @@ -156653,7 +156660,6 @@ async function run({ startedAt, logger }) { getOptionalInput("upload") ); if (runStats) { - const checkoutPath = getRequiredInput("checkout_path"); const category = getOptionalInput("category"); uploadResults = await postProcessAndUploadSarif( logger, diff --git a/src/analyze-action.ts b/src/analyze-action.ts index c3c2e40e7f..2a64ed3c54 100644 --- a/src/analyze-action.ts +++ b/src/analyze-action.ts @@ -212,7 +212,11 @@ async function runAutobuildIfLegacyGoWorkflow(config: Config, logger: Logger) { await runAutobuild(config, BuiltInLanguage.go, logger); } -async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) { +async function run({ + startedAt, + logger, + actions, +}: ActionState<["Base", "Logger", "Actions"]>) { // To capture errors appropriately, keep as much code within the try-catch as // possible, and only use safe functions outside. @@ -307,8 +311,13 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) { logger, ); + const checkoutPath = actions.getRequiredInput("checkout_path"); + // Setup diff informed analysis if needed (based on whether init created the file) - const diffRangePackDir = await setupDiffInformedQueryRun(logger); + const diffRangePackDir = await setupDiffInformedQueryRun( + logger, + checkoutPath, + ); await warnIfGoInstalledAfterInit(config, logger); await runAutobuildIfLegacyGoWorkflow(config, logger); @@ -354,7 +363,6 @@ async function run({ startedAt, logger }: ActionState<["Base", "Logger"]>) { actionsUtil.getOptionalInput("upload"), ); if (runStats) { - const checkoutPath = actionsUtil.getRequiredInput("checkout_path"); const category = actionsUtil.getOptionalInput("category"); uploadResults = await postProcessAndUploadSarif( diff --git a/src/analyze.ts b/src/analyze.ts index 411477b597..8f90711682 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -5,7 +5,7 @@ import { performance } from "perf_hooks"; import * as io from "@actions/io"; import * as yaml from "js-yaml"; -import { getTemporaryDirectory, getRequiredInput } from "./actions-util"; +import { getTemporaryDirectory } from "./actions-util"; import * as analyses from "./analyses"; import { setupCppAutobuild } from "./autobuild"; import { type CodeQL } from "./codeql"; @@ -233,6 +233,7 @@ async function finalizeDatabaseCreation( */ export async function setupDiffInformedQueryRun( logger: Logger, + checkoutPath: string, ): Promise { return await withGroupAsync( "Generating diff range extension pack", @@ -245,7 +246,6 @@ export async function setupDiffInformedQueryRun( return undefined; } - const checkoutPath = getRequiredInput("checkout_path"); const packDir = writeDiffRangeDataExtensionPack( logger, diffRanges, From ada4e83349a368bd2631e8d92ac54b63e1f30608 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 16 Sep 2026 12:37:49 +0100 Subject: [PATCH 23/35] Refactor `cleanupAndUploadOverlayBaseDatabaseToCache` querying `checkout_path` itself --- lib/entry-points.js | 10 +++++++--- src/analyze-action.ts | 7 ++++++- src/overlay/caching.ts | 11 ++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index c49550e59b..9fd643f247 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151255,7 +151255,7 @@ async function checkOverlayBaseDatabase(codeql, config, logger, warningPrefix) { } return true; } -async function cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger) { +async function cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger, checkoutPath) { const overlayDatabaseMode = config.overlayDatabaseMode; if (overlayDatabaseMode !== "overlay-base" /* OverlayBase */) { logger.debug( @@ -151303,7 +151303,6 @@ async function cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger return false; } const codeQlVersion = (await codeql.getVersion()).version; - const checkoutPath = getRequiredInput("checkout_path"); const cacheSaveKey = await getCacheSaveKey( config, codeQlVersion, @@ -156685,7 +156684,12 @@ async function run({ } else { logger.info("Not uploading results"); } - await cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger); + await cleanupAndUploadOverlayBaseDatabaseToCache( + codeql, + config, + logger, + checkoutPath + ); databaseUploadResults = await cleanupAndUploadDatabases( repositoryNwo, codeql, diff --git a/src/analyze-action.ts b/src/analyze-action.ts index 2a64ed3c54..55803d2611 100644 --- a/src/analyze-action.ts +++ b/src/analyze-action.ts @@ -396,7 +396,12 @@ async function run({ // Possibly upload the overlay-base database to actions cache. // Note: Take care with the ordering of this call since databases may be cleaned up // at the `overlay` level. - await cleanupAndUploadOverlayBaseDatabaseToCache(codeql, config, logger); + await cleanupAndUploadOverlayBaseDatabaseToCache( + codeql, + config, + logger, + checkoutPath, + ); // Possibly upload the database bundles for remote queries. // Note: Take care with the ordering of this call since databases may be cleaned up diff --git a/src/overlay/caching.ts b/src/overlay/caching.ts index c4557cd4ef..d246626780 100644 --- a/src/overlay/caching.ts +++ b/src/overlay/caching.ts @@ -3,11 +3,7 @@ import * as fs from "fs"; import * as actionsCache from "@actions/cache"; import * as semver from "semver"; -import { - getRequiredInput, - getWorkflowRunAttempt, - getWorkflowRunID, -} from "../actions-util"; +import { getWorkflowRunAttempt, getWorkflowRunID } from "../actions-util"; import { getAutomationID, listActionsCaches } from "../api-client"; import { createCacheKeyHash } from "../caching-utils"; import { type CodeQL } from "../codeql"; @@ -107,12 +103,13 @@ async function checkOverlayBaseDatabase( * Uploads the overlay-base database to the GitHub Actions cache. If conditions * for uploading are not met, the function does nothing and returns false. * - * This function uses the `checkout_path` input to determine the repository path + * This function uses the `checkoutPath` to determine the repository path * and works only when called from `analyze` or `upload-sarif`. * * @param codeql The CodeQL instance * @param config The configuration object * @param logger The logger instance + * @param checkoutPath The path at which the repository is checked out at. * @returns A promise that resolves to true if the upload was performed and * successfully completed, or false otherwise */ @@ -120,6 +117,7 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache( codeql: CodeQL, config: Config, logger: Logger, + checkoutPath: string, ): Promise { const overlayDatabaseMode = config.overlayDatabaseMode; if (overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase) { @@ -180,7 +178,6 @@ export async function cleanupAndUploadOverlayBaseDatabaseToCache( } const codeQlVersion = (await codeql.getVersion()).version; - const checkoutPath = getRequiredInput("checkout_path"); const cacheSaveKey = await getCacheSaveKey( config, codeQlVersion, From 8a88af684982fdc308a6949123d2894263f2ef88 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 16 Sep 2026 12:57:20 +0100 Subject: [PATCH 24/35] Refactor `cleanupAndUploadDatabases` querying `checkout_path` itself --- lib/entry-points.js | 13 ++-- src/analyze-action.ts | 4 +- src/database-upload.test.ts | 134 +++++++++++++++++++++--------------- src/database-upload.ts | 15 ++-- 4 files changed, 95 insertions(+), 71 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 9fd643f247..3fbacc32e4 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -154172,7 +154172,8 @@ async function warnIfGoInstalledAfterInit(config, logger) { // src/database-upload.ts var fs18 = __toESM(require("fs")); -async function cleanupAndUploadDatabases(repositoryNwo, codeql, config, apiDetails, features, logger) { +async function cleanupAndUploadDatabases(action, repositoryNwo, codeql, config, apiDetails, checkoutPath) { + const logger = action.logger; if (getRequiredInput("upload-database") !== "true") { logger.debug("Database upload disabled in workflow. Skipping upload."); return []; @@ -154195,7 +154196,7 @@ async function cleanupAndUploadDatabases(repositoryNwo, codeql, config, apiDetai logger.debug("Not analyzing default branch. Skipping upload."); return []; } - const shouldUploadOverlayBase = config.overlayDatabaseMode === "overlay-base" /* OverlayBase */ && await features.getValue("upload_overlay_db_to_api" /* UploadOverlayDbToApi */, codeql); + const shouldUploadOverlayBase = config.overlayDatabaseMode === "overlay-base" /* OverlayBase */ && await action.features.getValue("upload_overlay_db_to_api" /* UploadOverlayDbToApi */, codeql); const cleanupLevel = shouldUploadOverlayBase ? "overlay" /* Overlay */ : "clear" /* Clear */; await withGroupAsync("Cleaning up databases", async () => { await codeql.databaseCleanupCluster(config, cleanupLevel); @@ -154208,9 +154209,7 @@ async function cleanupAndUploadDatabases(repositoryNwo, codeql, config, apiDetai includeDiagnostics: false }); bundledDbSize = fs18.statSync(bundledDb).size; - const commitOid = await getCommitOid( - getRequiredInput("checkout_path") - ); + const commitOid = await getCommitOid(checkoutPath); const maxAttempts = 4; let uploadDurationMs; for (let attempt = 1; attempt <= maxAttempts; attempt++) { @@ -156691,12 +156690,12 @@ async function run({ checkoutPath ); databaseUploadResults = await cleanupAndUploadDatabases( + { logger, features }, repositoryNwo, codeql, config, apiDetails, - features, - logger + checkoutPath ); const trapCacheUploadStartTime = import_perf_hooks6.performance.now(); didUploadTrapCaches = await uploadTrapCaches(codeql, config, logger); diff --git a/src/analyze-action.ts b/src/analyze-action.ts index 55803d2611..7963fa52bf 100644 --- a/src/analyze-action.ts +++ b/src/analyze-action.ts @@ -407,12 +407,12 @@ async function run({ // Note: Take care with the ordering of this call since databases may be cleaned up // at the `overlay` or `clear` level. databaseUploadResults = await cleanupAndUploadDatabases( + { logger, features }, repositoryNwo, codeql, config, apiDetails, - features, - logger, + checkoutPath, ); // Possibly upload the TRAP caches for later re-use diff --git a/src/database-upload.test.ts b/src/database-upload.test.ts index bcaf9f1c9e..b6ac5c8115 100644 --- a/src/database-upload.test.ts +++ b/src/database-upload.test.ts @@ -20,8 +20,9 @@ import { checkExpectedLogMessages, createFeatures, createTestConfig, - getRecordingLogger, - LoggedMessage, + getTestEnv, + initAllState, + RecordingLogger, setupActionsVars, setupTests, } from "./testing-utils"; @@ -90,23 +91,24 @@ test.serial( "Abort database upload if 'upload-database' input set to false", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("false"); sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(true); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Database upload disabled in workflow. Skipping upload.", ]); }); @@ -117,7 +119,8 @@ test.serial( "Abort database upload if 'analysis-kinds: code-scanning' is not enabled", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -126,8 +129,9 @@ test.serial( await mockHttpRequests(201); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), { @@ -135,10 +139,9 @@ test.serial( analysisKinds: [AnalysisKind.CodeQuality], }, testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Not uploading database because 'analysis-kinds: code-scanning' is not enabled.", ]); }); @@ -147,7 +150,8 @@ test.serial( test.serial("Abort database upload if running against GHES", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -157,16 +161,16 @@ test.serial("Abort database upload if running against GHES", async (t) => { const config = getTestConfig(tmpDir); config.gitHubVersion = { type: GitHubVariant.GHES, version: "3.0" }; - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), config, testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Not running against github.com or GHEC-DR. Skipping upload.", ]); }); @@ -176,23 +180,24 @@ test.serial( "Abort database upload if not analyzing default branch", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") .returns("true"); sinon.stub(gitUtils, "isAnalyzingDefaultBranch").resolves(false); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Not analyzing default branch. Skipping upload.", ]); }); @@ -203,7 +208,8 @@ test.serial( "Don't crash if uploading a database fails with a non-retryable error", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -212,17 +218,17 @@ test.serial( const databaseUploadSpy = await mockHttpRequests(422); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Failed to upload database for javascript: some error message", ]); @@ -236,7 +242,8 @@ test.serial( "Don't crash if uploading a database fails with a retryable error", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -251,17 +258,17 @@ test.serial( .stub(global, "setTimeout") .callsFake((fn: () => void) => originalSetTimeout(fn, 0)); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Failed to upload database for javascript: some error message", ]); @@ -279,7 +286,8 @@ test.serial( test.serial("Successfully uploading a database to github.com", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -288,16 +296,16 @@ test.serial("Successfully uploading a database to github.com", async (t) => { await mockHttpRequests(201); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), testApiDetails, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Successfully uploaded database for javascript", ]); }); @@ -305,7 +313,8 @@ test.serial("Successfully uploading a database to github.com", async (t) => { test.serial("Successfully uploading a database to GHEC-DR", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -314,8 +323,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => { const databaseUploadSpy = await mockHttpRequests(201); - const loggedMessages: LoggedMessage[] = []; + const logger = new RecordingLogger(); await cleanupAndUploadDatabases( + initAllState({ env, logger }), testRepoName, getCodeQL(), getTestConfig(tmpDir), @@ -324,10 +334,9 @@ test.serial("Successfully uploading a database to GHEC-DR", async (t) => { url: "https://tenant.ghe.com", apiURL: undefined, }, - createFeatures([]), - getRecordingLogger(loggedMessages), + "", ); - checkExpectedLogMessages(t, loggedMessages, [ + checkExpectedLogMessages(t, logger.messages, [ "Successfully uploaded database for javascript", ]); t.assert( @@ -343,7 +352,8 @@ test.serial( "Records overlay and clear cleanup sizes when uploading an overlay-base database", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -375,14 +385,16 @@ test.serial( const config = getTestConfig(tmpDir); config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase; - const loggedMessages: LoggedMessage[] = []; const results = await cleanupAndUploadDatabases( + initAllState({ + env, + features: createFeatures([Feature.UploadOverlayDbToApi]), + }), testRepoName, codeql, config, testApiDetails, - createFeatures([Feature.UploadOverlayDbToApi]), - getRecordingLogger(loggedMessages), + "", ); // The database should be cleaned up at the `overlay` level for the upload @@ -402,7 +414,8 @@ test.serial( "Does not measure clear cleanup size for a regular (non-overlay-base) upload", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -422,12 +435,15 @@ test.serial( }); const results = await cleanupAndUploadDatabases( + initAllState({ + env, + features: createFeatures([Feature.UploadOverlayDbToApi]), + }), testRepoName, codeql, getTestConfig(tmpDir), testApiDetails, - createFeatures([Feature.UploadOverlayDbToApi]), - getRecordingLogger([]), + "", ); // A regular upload is cleaned only once, at the `clear` level. @@ -441,7 +457,8 @@ test.serial( test.serial("Does not measure clear cleanup size in debug mode", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -465,12 +482,15 @@ test.serial("Does not measure clear cleanup size in debug mode", async (t) => { config.debugMode = true; const results = await cleanupAndUploadDatabases( + initAllState({ + env, + features: createFeatures([Feature.UploadOverlayDbToApi]), + }), testRepoName, codeql, config, testApiDetails, - createFeatures([Feature.UploadOverlayDbToApi]), - getRecordingLogger([]), + "", ); // In debug mode we clean up at the `overlay` level for the upload but skip @@ -486,7 +506,8 @@ test.serial( "Does not record a clear cleanup duration when the clear cleanup fails", async (t) => { await withTmpDir(async (tmpDir) => { - setupActionsVars(tmpDir, tmpDir); + const env = getTestEnv(); + setupActionsVars(tmpDir, tmpDir, {}, env); sinon .stub(actionsUtil, "getRequiredInput") .withArgs("upload-database") @@ -510,12 +531,15 @@ test.serial( config.overlayDatabaseMode = OverlayDatabaseMode.OverlayBase; const results = await cleanupAndUploadDatabases( + initAllState({ + env, + features: createFeatures([Feature.UploadOverlayDbToApi]), + }), testRepoName, codeql, config, testApiDetails, - createFeatures([Feature.UploadOverlayDbToApi]), - getRecordingLogger([]), + "", ); // When the `clear` cleanup fails, no size is measured, so we should not diff --git a/src/database-upload.ts b/src/database-upload.ts index 0189bef1e6..9e4339fd47 100644 --- a/src/database-upload.ts +++ b/src/database-upload.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; +import { ActionState } from "./action-common"; import * as actionsUtil from "./actions-util"; import { AnalysisKind } from "./analyses"; import { @@ -9,7 +10,7 @@ import { } from "./api-client"; import { type CodeQL } from "./codeql"; import { Config } from "./config-utils"; -import { Feature, FeatureEnablement } from "./feature-flags"; +import { Feature } from "./feature-flags"; import * as gitUtils from "./git-utils"; import { Logger, withGroupAsync } from "./logging"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; @@ -45,13 +46,15 @@ export interface DatabaseUploadResult { } export async function cleanupAndUploadDatabases( + action: ActionState<["Logger", "FeatureFlags"]>, repositoryNwo: RepositoryNwo, codeql: CodeQL, config: Config, apiDetails: GitHubApiDetails, - features: FeatureEnablement, - logger: Logger, + checkoutPath: string, ): Promise { + const logger = action.logger; + if (actionsUtil.getRequiredInput("upload-database") !== "true") { logger.debug("Database upload disabled in workflow. Skipping upload."); return []; @@ -87,7 +90,7 @@ export async function cleanupAndUploadDatabases( // If config.overlayDatabaseMode is OverlayBase, then we have overlay base databases for all languages. const shouldUploadOverlayBase = config.overlayDatabaseMode === OverlayDatabaseMode.OverlayBase && - (await features.getValue(Feature.UploadOverlayDbToApi, codeql)); + (await action.features.getValue(Feature.UploadOverlayDbToApi, codeql)); const cleanupLevel = shouldUploadOverlayBase ? CleanupLevel.Overlay : CleanupLevel.Clear; @@ -110,9 +113,7 @@ export async function cleanupAndUploadDatabases( includeDiagnostics: false, }); bundledDbSize = fs.statSync(bundledDb).size; - const commitOid = await gitUtils.getCommitOid( - actionsUtil.getRequiredInput("checkout_path"), - ); + const commitOid = await gitUtils.getCommitOid(checkoutPath); // Upload with manual retry logic. We disable Octokit's built-in retries // because the request body is a ReadStream, which can only be consumed // once. From 07dc94940e1af55d7da55811119e389c5acac178 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:07:07 +0100 Subject: [PATCH 25/35] Use default state in per-language bundle tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/per-language-bundles.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index b8f48512fe..0330b51a63 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -38,7 +38,6 @@ async function checkEligibility( [ActionsEnvVars.RUNNER_ENVIRONMENT]: "github-hosted", }), features: createFeatures([Feature.PerLanguageBundles]), - logger: getRecordingLogger([], { logToConsole: false }), ...stateOverrides, }), { ...ELIGIBLE_OPTIONS, ...overrides }, @@ -134,7 +133,6 @@ test("getPerLanguageBundleLanguage explains a disabled feature before checking e const messages: LoggedMessage[] = []; const language = await getPerLanguageBundleLanguage( initAllState({ - env: getTestEnv(), features: createFeatures([]), logger: getRecordingLogger(messages, { logToConsole: false }), }), From 06344e2ba1565124d7acef663be8ff3b6f42643f Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:07:09 +0100 Subject: [PATCH 26/35] Stub nightly release listing directly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/setup-codeql.test.ts | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index c35bdb8406..6ac5bebf18 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -69,25 +69,29 @@ function stubHostedNightly(tagName: string) { available: true, foundZstdBinary: true, }); - const fetchRelease = sinon - .stub, ReturnType>() - .rejects(new Error("Unexpected API request in nightly bundle test")); - fetchRelease - .withArgs( - "https://api.github.com/repos/dsp-testing/codeql-cli-nightlies/releases?per_page=1&page=1&prerelease=true", - sinon.match({ method: "GET" }), - ) - .callsFake( - async () => - new Response(JSON.stringify([{ tag_name: tagName }]), { - headers: { "content-type": "application/json" }, - }), - ); const client = github.getOctokit("123", { - request: { fetch: fetchRelease }, + request: { + fetch: async () => { + throw new Error("Unexpected API request in nightly bundle test"); + }, + }, }); + const listReleases = sinon + .stub(client.rest.repos, "listReleases") + .rejects(new Error("Unexpected release request in nightly bundle test")); + listReleases + .withArgs({ + owner: "dsp-testing", + repo: "codeql-cli-nightlies", + per_page: 1, + page: 1, + prerelease: true, + }) + .resolves({ + data: [{ tag_name: tagName }], + } as Awaited>); sinon.stub(api, "getApiClient").value(() => client); - return fetchRelease; + return listReleases; } test.serial("parse codeql bundle url version", (t) => { From dba87a18dc3eb9d4d09a0585f4148670685519de Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:07:10 +0100 Subject: [PATCH 27/35] Clarify elapsed-time helper documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index 456cd7c3d2..d74e07fa8d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -682,7 +682,7 @@ export async function bundleDb( return databaseBundlePath; } -/** Returns the elapsed milliseconds, rounded, since a `performance.now()` timestamp. */ +/** Returns the elapsed milliseconds, rounded, since `startTime` was recorded with `performance.now()`. */ export function durationMsSince(startTime: number): number { return Math.round(performance.now() - startTime); } From a9a8cd1aecbbc419b7b38acc138c3e3cd844b0f9 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:07:10 +0100 Subject: [PATCH 28/35] Explain the nightly bundle version-check exception Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/per-language-bundles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index f4e46403db..35cfa3e0f5 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -102,8 +102,9 @@ export async function getPerLanguageBundleLanguage( return explain("the job is not running on a GitHub-hosted runner"); } - // Check whether per-language bundles are published for the requested CLI version. - // Latest-nightly selection skips this release-version check, but not the other eligibility checks. + // If the user requested the latest nightly, skip the version check, as nightlies have shipped + // per-language bundles since https://github.com/dsp-testing/codeql-cli-nightlies/releases/tag/codeql-bundle-20260909. + // Otherwise, check the requested CLI version to determine whether per-language bundles are published. if (!isLatestNightly) { if (cliVersion === undefined) { return explain("the requested CLI version is unknown"); From f2ec2f6267210c6d22b53d32be7048187043bd34 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:23:55 +0100 Subject: [PATCH 29/35] Tweak comment for latest nightly version check Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/per-language-bundles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 35cfa3e0f5..6f80096834 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -102,7 +102,7 @@ export async function getPerLanguageBundleLanguage( return explain("the job is not running on a GitHub-hosted runner"); } - // If the user requested the latest nightly, skip the version check, as nightlies have shipped + // When selecting the latest nightly, skip the version check, as nightlies have shipped // per-language bundles since https://github.com/dsp-testing/codeql-cli-nightlies/releases/tag/codeql-bundle-20260909. // Otherwise, check the requested CLI version to determine whether per-language bundles are published. if (!isLatestNightly) { From 48321b2d4823e75454e91867ded94e394d33343b Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:49:21 +0100 Subject: [PATCH 30/35] Update src/per-language-bundles.ts Co-authored-by: Michael B. Gale --- src/per-language-bundles.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 6f80096834..acdff5c740 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -102,9 +102,11 @@ export async function getPerLanguageBundleLanguage( return explain("the job is not running on a GitHub-hosted runner"); } - // When selecting the latest nightly, skip the version check, as nightlies have shipped - // per-language bundles since https://github.com/dsp-testing/codeql-cli-nightlies/releases/tag/codeql-bundle-20260909. - // Otherwise, check the requested CLI version to determine whether per-language bundles are published. + // Nightly releases are identified by dates rather than versions. If + // `isLatestNightly` is `true`, the latest nightly is requested with + // `tools: nightly` and we don't yet have the corresponding tag at this point. + // Therefore, we skip the version check and don't have an equivalent. + // We can safely assume that the latest nightly will have per-language bundles. if (!isLatestNightly) { if (cliVersion === undefined) { return explain("the requested CLI version is unknown"); From 3bacfe2c5b69ecc6f63622003e4ef982bbd5da64 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 22 Sep 2026 16:56:15 +0100 Subject: [PATCH 31/35] Remove trailing whitespace from nightly comment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/per-language-bundles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index acdff5c740..e4468d2f34 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -102,8 +102,8 @@ export async function getPerLanguageBundleLanguage( return explain("the job is not running on a GitHub-hosted runner"); } - // Nightly releases are identified by dates rather than versions. If - // `isLatestNightly` is `true`, the latest nightly is requested with + // Nightly releases are identified by dates rather than versions. If + // `isLatestNightly` is `true`, the latest nightly is requested with // `tools: nightly` and we don't yet have the corresponding tag at this point. // Therefore, we skip the version check and don't have an equivalent. // We can safely assume that the latest nightly will have per-language bundles. From 53162242d5865c69ec07e928d7348f3fa2841c15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:12:09 +0000 Subject: [PATCH 32/35] Update default bundle to codeql-bundle-v2.27.1 --- lib/defaults.json | 8 ++++---- lib/entry-points.js | 4 ++-- src/defaults.json | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/defaults.json b/lib/defaults.json index e4875a8a34..e201dfd79e 100644 --- a/lib/defaults.json +++ b/lib/defaults.json @@ -1,6 +1,6 @@ { - "bundleVersion": "codeql-bundle-v2.27.0", - "cliVersion": "2.27.0", - "priorBundleVersion": "codeql-bundle-v2.26.4", - "priorCliVersion": "2.26.4" + "bundleVersion": "codeql-bundle-v2.27.1", + "cliVersion": "2.27.1", + "priorBundleVersion": "codeql-bundle-v2.27.0", + "priorCliVersion": "2.27.0" } diff --git a/lib/entry-points.js b/lib/entry-points.js index 3fbacc32e4..2340dc38e6 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -147732,8 +147732,8 @@ var path6 = __toESM(require("path")); var semver4 = __toESM(require_semver2()); // src/defaults.json -var bundleVersion = "codeql-bundle-v2.27.0"; -var cliVersion = "2.27.0"; +var bundleVersion = "codeql-bundle-v2.27.1"; +var cliVersion = "2.27.1"; // src/overlay/index.ts var fs5 = __toESM(require("fs")); diff --git a/src/defaults.json b/src/defaults.json index e4875a8a34..e201dfd79e 100644 --- a/src/defaults.json +++ b/src/defaults.json @@ -1,6 +1,6 @@ { - "bundleVersion": "codeql-bundle-v2.27.0", - "cliVersion": "2.27.0", - "priorBundleVersion": "codeql-bundle-v2.26.4", - "priorCliVersion": "2.26.4" + "bundleVersion": "codeql-bundle-v2.27.1", + "cliVersion": "2.27.1", + "priorBundleVersion": "codeql-bundle-v2.27.0", + "priorCliVersion": "2.27.0" } From 81fb67799a74354c9fabb1dbc7ee198832c892db Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:12:17 +0000 Subject: [PATCH 33/35] Add changelog note --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 267b4e557e..a8880f37aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th ## [UNRELEASED] -No user facing changes. +- Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160) ## 4.38.1 - 18 Sept 2026 From 9691115b1ca24ee0f0939d6680facb305b90635f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 23 Sep 2026 11:09:52 +0100 Subject: [PATCH 34/35] Disable `UsePerfData` for `resolveExtractor` --- lib/entry-points.js | 1 + src/codeql.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/entry-points.js b/lib/entry-points.js index 2340dc38e6..bb408af315 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -153247,6 +153247,7 @@ async function getCodeQLForCmd(logger, cmd, checkVersion) { "--format=json", `--language=${language}`, "--extractor-include-aliases", + "-J-XX:-UsePerfData", ...getExtraOptionsFromEnv(["resolve", "extractor"]) ], { diff --git a/src/codeql.ts b/src/codeql.ts index 65e73d9451..fbc119a341 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -937,6 +937,7 @@ async function getCodeQLForCmd( "--format=json", `--language=${language}`, "--extractor-include-aliases", + "-J-XX:-UsePerfData", ...getExtraOptionsFromEnv(["resolve", "extractor"]), ], { From 82d91df95c6f14b803ad9aa51adc03962be62dd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:19:05 +0000 Subject: [PATCH 35/35] Update changelog for v4.38.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8880f37aa..b99740aa6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for the relevant changes to the CodeQL CLI and language packs. -## [UNRELEASED] +## 4.38.2 - 23 Sept 2026 - Update default CodeQL bundle version to [2.27.1](https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.1). [#4160](https://github.com/github/codeql-action/pull/4160)