Compare commits

...

29 Commits

Author SHA1 Message Date
Michael B. Gale 9e0d7b8d25 Merge pull request #3905 from github/update-v4.35.5-d4b485515
Merge main into releases/v4
2026-05-15 12:22:46 +01:00
Michael B. Gale 6d7d59927c Add changelog entry for #3899 2026-05-15 11:58:39 +01:00
github-actions[bot] 51f7e38c69 Update changelog for v4.35.5 2026-05-15 10:48:24 +00:00
Michael B. Gale d4b485515e Merge pull request #3899 from github/mbg/esbuild/split
Reduce duplication across JS bundles by creating one bundle with smaller entry point wrappers
2026-05-15 10:11:56 +00:00
Michael B. Gale 127de8117f Merge remote-tracking branch 'origin/main' into mbg/esbuild/split 2026-05-14 19:14:01 +01:00
Michael B. Gale 7fde13f26a Use src + basename in header to avoid issues on Windows 2026-05-14 19:10:19 +01:00
Michael B. Gale dfa61e7305 Improve pattern matching and error handling 2026-05-14 18:36:41 +01:00
Michael B. Gale 52aafec073 Import and call runWrapper normally in analyze tests 2026-05-14 18:32:40 +01:00
Michael B. Gale 0d08c01f78 Auto-generate shared bundle 2026-05-14 18:27:46 +01:00
Michael B. Gale 14085a675c Auto-generate entry points 2026-05-14 18:13:01 +01:00
Sam Robson eb17ca4f4d Merge pull request #3791 from github/sam-robson/overlay-fallback
Fall back to non-overlay analysis when diff-informed analysis is unavailable
2026-05-14 15:41:25 +00:00
Sam Robson a41c444cd9 Merge branch 'main' into sam-robson/overlay-fallback 2026-05-14 15:51:24 +01:00
Michael B. Gale d7e50c23fe Fix linter errors 2026-05-14 15:24:33 +01:00
Michael B. Gale bb30f3132d Avoid top-level promise in analyze-action 2026-05-14 15:14:03 +01:00
Michael B. Gale 2f137c9dc6 Add remaining new entry points 2026-05-14 14:55:33 +01:00
Michael B. Gale f0489abddd Update action specs for new entry points 2026-05-14 14:47:23 +01:00
Sam Robson 9d7243005b Merge remote-tracking branch 'origin/main' into sam-robson/overlay-fallback
* origin/main: (40 commits)
  Bump the npm-minor group across 1 directory with 3 updates
  Bump actions/create-github-app-token
  Nit: Tweak JSDoc for `getRawLanguagesNoAutodetect`
  Enable only `code-scanning`
  Use overlay-aware version for code scanning exclusively
  Add changelog entry
  Rebuild
  Bump five transitive dependencies
  Throw error if multiple analysis kinds are specified
  Bump fast-xml-builder from 1.1.5 to 1.2.0
  Improve tests
  Improve error message
  Remove dead code
  Remove `makeOverlayMatchFeatures` indirection
  Add JSDoc for `getRawLanguagesNoAutodetect`
  Enable overlay-aware version selection in `setup-codeql`
  Minor: Introduce constant to avoid duplication
  Improve changelog note
  Rebuild
  Update changelog and version after v4.35.4
  ...

# Conflicts:
#	lib/init-action.js
#	src/diff-informed-analysis-utils.test.ts
2026-05-14 13:39:44 +01:00
Michael B. Gale 237b03b3c3 WIP: Reduce bundle duplication 2026-05-14 12:47:34 +01:00
Sam Robson 9d6b456c59 Merge branch 'main' into sam-robson/overlay-fallback 2026-05-06 20:26:20 +01:00
Sam Robson e259d26055 refactor: rename overlay-disabled reason and add changelog entry 2026-05-06 20:23:20 +01:00
Sam Robson 8ab64a211d Merge branch 'main' into sam-robson/overlay-fallback 2026-05-01 16:35:49 +01:00
Sam Robson f8b93c30a6 Merge branch 'main' into sam-robson/overlay-fallback 2026-05-01 11:28:43 +01:00
Sam Robson 80a72986d3 fix: re-import withGroupAsync in init-action.ts after merge 2026-04-30 16:48:45 +01:00
Sam Robson e9e36aec74 test: drop codescanning-config-cli scenario for overlay without diff-informed 2026-04-30 16:39:05 +01:00
Sam Robson 4ed52dcbfa Merge branch 'main' into sam-robson/overlay-fallback 2026-04-30 16:24:23 +01:00
Sam Robson 3cc8dd3e59 refactor: report missing PR diff ranges via OverlayDisabledReason and disable overlay 2026-04-30 16:12:30 +01:00
Sam Robson 5ded561dcd Merge branch 'main' into sam-robson/overlay-fallback 2026-04-24 06:39:07 +01:00
Sam Robson faca00d3ae refactor: address review feedback on overlay fallback 2026-04-23 20:38:10 +01:00
Sam Robson 5d1c58464f refactor: fall back to non-overlay analysis when diff-informed analysis is unavailable 2026-04-23 12:10:22 +01:00
52 changed files with 31945 additions and 1046527 deletions
+1 -28
View File
@@ -6,13 +6,6 @@ env:
# Diff informed queries add an additional query filter which is not yet
# taken into account by these tests.
CODEQL_ACTION_DIFF_INFORMED_QUERIES: false
# Specify overlay enablement manually to ensure stability around the exclude-from-incremental
# query filter. Here we only enable for the default code scanning suite.
CODEQL_ACTION_OVERLAY_ANALYSIS: true
CODEQL_ACTION_OVERLAY_ANALYSIS_JAVASCRIPT: false
CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_JAVASCRIPT: true
CODEQL_ACTION_OVERLAY_ANALYSIS_STATUS_CHECK: false
CODEQL_ACTION_OVERLAY_ANALYSIS_SKIP_RESOURCE_CHECKS: true
on:
push:
@@ -79,33 +72,13 @@ jobs:
with:
version: ${{ matrix.version }}
# On PRs, overlay analysis may change the config that is passed to the CLI.
# Therefore, we have two variants of the following test, one for PRs and one for other events.
- name: Empty file (non-PR)
if: github.event_name != 'pull_request'
- name: Empty file
uses: ./../action/.github/actions/check-codescanning-config
with:
expected-config-file-contents: "{}"
languages: javascript
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Empty file (PR)
if: github.event_name == 'pull_request'
uses: ./../action/.github/actions/check-codescanning-config
with:
expected-config-file-contents: |
{
"query-filters": [
{
"exclude": {
"tags": "exclude-from-incremental"
}
}
]
}
languages: javascript
tools: ${{ steps.prepare-test.outputs.tools-url }}
- name: Packs from input
if: success() || failure()
uses: ./../action/.github/actions/check-codescanning-config
+3 -1
View File
@@ -2,8 +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]
## 4.35.5 - 15 May 2026
- We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. [#3899](https://github.com/github/codeql-action/pull/3899)
- For performance and accuracy reasons, [improved incremental analysis](https://github.com/github/roadmap/issues/1158) will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. [#3791](https://github.com/github/codeql-action/pull/3791)
- If multiple inputs are provided for the GitHub-internal `analysis-kinds` input, only `code-scanning` will be enabled. The `analysis-kinds` input is experimental, for GitHub-internal use only, and may change without notice at any time. [#3892](https://github.com/github/codeql-action/pull/3892)
- Added an experimental change which, when running a Code Scanning analysis for a PR with [improved incremental analysis](https://github.com/github/roadmap/issues/1158) enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. [#3880](https://github.com/github/codeql-action/pull/3880)
+2 -2
View File
@@ -95,5 +95,5 @@ outputs:
description: The ID of the uploaded SARIF file.
runs:
using: node24
main: "../lib/analyze-action.js"
post: "../lib/analyze-action-post.js"
main: "../lib/analyze-entry.js"
post: "../lib/analyze-post-entry.js"
+1 -1
View File
@@ -16,4 +16,4 @@ inputs:
required: false
runs:
using: node24
main: '../lib/autobuild-action.js'
main: '../lib/autobuild-entry.js'
+113 -8
View File
@@ -1,5 +1,5 @@
import { copyFile, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { copyFile, readFile, rm, writeFile } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as esbuild from "esbuild";
@@ -62,18 +62,123 @@ const onEndPlugin = {
},
};
/** The name of the virtual `entry-points` module. */
const SHARED_ENTRYPOINT = "entry-points";
/**
* This plugin finds all source files that contain action entry points.
* It then generates the virtual `entry-points` module which imports all identifies files,
* and re-exports their `runWrapper` functions with suitable aliases.
* A tiny stub file is emitted for each Action entrypoint. Each stub imports the shared bundle
* and calls the respective entry point.
*
* @type {esbuild.Plugin}
*/
const entryPointsPlugin = {
name: "entry-points",
setup(build) {
const namespace = "actions";
const actions = [];
const toPascal = (s) =>
s.replace(/(^|-)([a-z0-9])/gi, (_, __, c) => c.toUpperCase());
// Find the source files containing action entry points.
build.onStart(() => {
const actionFiles = globSync("src/*-action{,-post}.ts");
for (const actionFile of actionFiles) {
const match = basename(actionFile).match(/(.*)-action(-post)?/);
if (match.length < 2) {
throw new Error(`'${actionFile}' didn't match expected pattern.`);
}
const actionName = match[1];
const isPost = match[2] !== undefined;
actions.push({
path: actionFile,
name: actionName,
isPost,
pascalCaseName: `${toPascal(actionName)}${isPost ? "Post" : ""}Action`,
});
}
});
// Resolve the virtual `entry-points` file and set the corresponding namespace.
// Ideally, we'd `RegExp.escape` the entrypoint here, but that API isn't supported in Node 20.
// Since we're dealing with a hardcoded string, this isn't too much of a problem.
build.onResolve({ filter: new RegExp(`^${SHARED_ENTRYPOINT}$`) }, () => {
return { path: SHARED_ENTRYPOINT, namespace };
});
// Generate the virtual `entry-points` file based on the actions we discovered.
// Restrict using the namespace. The path filter does not need to discriminate any further.
build.onLoad({ filter: /.*/, namespace }, async () => {
const wrapperTemplatePath = "entry-wrapper.js.tpl";
const wrapperTemplate = await readFile(
join(SRC_DIR, wrapperTemplatePath),
"utf-8",
);
const actionsSorted = actions.sort((a, b) =>
a.name.localeCompare(b.name),
);
const imports = actionsSorted
.map(
(action) =>
`import * as ${action.pascalCaseName} from "./src/${basename(action.path)}"`,
)
.join("\n");
const wrappers = actionsSorted
.map((action) =>
wrapperTemplate.replaceAll("__ACTION__", action.pascalCaseName),
)
.join("\n\n");
return {
contents: `"use strict";\n${imports}\n\n${wrappers}\n`,
resolveDir: ".",
loader: "ts",
};
});
// Emit entry point stubs for each action using the entry template.
build.onEnd(async (result) => {
// Read the entry point template.
const templatePath = "action-entry.js.tpl";
const template = await readFile(join(SRC_DIR, templatePath), "utf-8");
const makeHeader = (sourceFile) =>
`// Automatically generated from '${templatePath}' for 'src/${basename(sourceFile)}'.\n\n`;
// Write entry point stubs for each action.
for (const action of actions) {
await writeFile(
join(
OUT_DIR,
`${action.name}${action.isPost ? "-post" : ""}-entry.js`,
),
makeHeader(action.path) +
template.replaceAll("__ACTION__", action.pascalCaseName),
);
}
});
},
};
const context = await esbuild.context({
// Include upload-lib.ts as an entry point for use in testing environments.
entryPoints: globSync([
`${SRC_DIR}/*-action.ts`,
`${SRC_DIR}/*-action-post.ts`,
"src/upload-lib.ts",
]),
entryPoints: [
{ in: SHARED_ENTRYPOINT, out: SHARED_ENTRYPOINT },
join(SRC_DIR, "upload-lib.ts"),
],
bundle: true,
format: "cjs",
outdir: OUT_DIR,
platform: "node",
plugins: [cleanPlugin, copyDefaultsPlugin, onEndPlugin],
external: ["./entry-points"],
plugins: [cleanPlugin, copyDefaultsPlugin, entryPointsPlugin, onEndPlugin],
target: ["node20"],
define: {
__CODEQL_ACTION_VERSION__: JSON.stringify(pkg.version),
+2 -2
View File
@@ -171,5 +171,5 @@ outputs:
description: The version of the CodeQL binary used for analysis
runs:
using: node24
main: '../lib/init-action.js'
post: '../lib/init-action-post.js'
main: '../lib/init-entry.js'
post: '../lib/init-post-entry.js'
-129183
View File
File diff suppressed because one or more lines are too long
-96038
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/analyze-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runAnalyzeAction)();
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/analyze-action-post.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runAnalyzePostAction)();
-88244
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/autobuild-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runAutobuildAction)();
File diff suppressed because it is too large Load Diff
-93006
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/init-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runInitAction)();
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/init-action-post.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runInitPostAction)();
-87798
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/resolve-environment-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runResolveEnvironmentAction)();
-89620
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/setup-codeql-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runSetupCodeqlAction)();
-127995
View File
File diff suppressed because one or more lines are too long
-105058
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/start-proxy-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runStartProxyAction)();
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/start-proxy-action-post.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runStartProxyPostAction)();
-128020
View File
File diff suppressed because one or more lines are too long
-94427
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/upload-sarif-action.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runUploadSarifAction)();
+6
View File
@@ -0,0 +1,6 @@
// Automatically generated from 'action-entry.js.tpl' for 'src/upload-sarif-action-post.ts'.
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.runUploadSarifPostAction)();
+1 -1
View File
@@ -22,4 +22,4 @@ outputs:
description: The inferred build environment configuration.
runs:
using: node24
main: '../lib/resolve-environment-action.js'
main: '../lib/resolve-environment-entry.js'
+1 -1
View File
@@ -55,4 +55,4 @@ outputs:
description: The version of the CodeQL binary that was installed.
runs:
using: node24
main: '../lib/setup-codeql-action.js'
main: '../lib/setup-codeql-entry.js'
+4
View File
@@ -0,0 +1,4 @@
"use strict";
const import_entry_points = require("./entry-points");
void (0, import_entry_points.run__ACTION__)();
+2 -7
View File
@@ -3,6 +3,7 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as analyze from "./analyze";
import { runWrapper } from "./analyze-action";
import * as api from "./api-client";
import * as configUtils from "./config-utils";
import * as gitUtils from "./git-utils";
@@ -62,14 +63,8 @@ test("analyze action with RAM & threads from environment variables", async (t) =
const runFinalizeStub = sinon.stub(analyze, "runFinalize");
const runQueriesStub = sinon.stub(analyze, "runQueries");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const analyzeAction = require("./analyze-action");
// When analyze-action.ts loads, it runs an async function from the top
// level but does not wait for it to finish. To ensure that calls to
// runFinalize and runQueries are correctly captured by spies, we explicitly
// wait for the action promise to complete before starting verification.
await analyzeAction.runPromise;
await runWrapper();
t.assert(
runFinalizeStub.calledOnceWith(
+2 -7
View File
@@ -3,6 +3,7 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as analyze from "./analyze";
import { runWrapper } from "./analyze-action";
import * as api from "./api-client";
import * as configUtils from "./config-utils";
import * as gitUtils from "./git-utils";
@@ -60,14 +61,8 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
const runFinalizeStub = sinon.stub(analyze, "runFinalize");
const runQueriesStub = sinon.stub(analyze, "runQueries");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const analyzeAction = require("./analyze-action");
// When analyze-action.ts loads, it runs an async function from the top
// level but does not wait for it to finish. To ensure that calls to
// runFinalize and runQueries are correctly captured by spies, we explicitly
// wait for the action promise to complete before starting verification.
await analyzeAction.runPromise;
await runWrapper();
t.assert(
runFinalizeStub.calledOnceWith(
+1 -3
View File
@@ -20,7 +20,7 @@ import { EnvVar } from "./environment";
import { getActionsLogger } from "./logging";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
async function runWrapper() {
export async function runWrapper() {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -72,5 +72,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+3 -8
View File
@@ -523,14 +523,11 @@ async function run(startedAt: Date) {
}
}
// Module-level startedAt so it can be accessed by runWrapper for error reporting
const startedAt = new Date();
export const runPromise = run(startedAt);
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
await runPromise;
await run(startedAt);
} catch (error) {
core.setFailed(`analyze action failed: ${util.getErrorMessage(error)}`);
await sendUnhandledErrorStatusReport(
@@ -542,5 +539,3 @@ async function runWrapper() {
}
await util.checkForTimeout();
}
void runWrapper();
+1 -3
View File
@@ -142,7 +142,7 @@ async function run(startedAt: Date) {
await sendCompletedStatusReport(config, logger, startedAt, languages ?? []);
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -157,5 +157,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+85
View File
@@ -21,6 +21,7 @@ import { GitVersionInfo } from "./git-utils";
import { BuiltInLanguage, Language } from "./languages";
import { getRunnerLogger } from "./logging";
import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay";
import * as overlayDiagnostics from "./overlay/diagnostics";
import { OverlayDisabledReason } from "./overlay/diagnostics";
import { OverlayDatabaseMode } from "./overlay/overlay-database-mode";
import * as overlayStatus from "./overlay/status";
@@ -2143,3 +2144,87 @@ test.serial(
});
},
);
test("applyIncrementalAnalysisSettings: no-op when mode is not Overlay and diff ranges are unavailable", async (t) => {
const config = createTestConfig({});
config.overlayDatabaseMode = OverlayDatabaseMode.None;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
await configUtils.applyIncrementalAnalysisSettings(
config,
false,
codeql,
logger,
);
t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None);
t.deepEqual(config.extraQueryExclusions, []);
});
test("applyIncrementalAnalysisSettings: keeps overlay mode and adds exclusions when diff ranges are available", async (t) => {
const config = createTestConfig({
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
});
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
await configUtils.applyIncrementalAnalysisSettings(
config,
true,
codeql,
logger,
);
t.is(config.overlayDatabaseMode, OverlayDatabaseMode.Overlay);
t.deepEqual(config.extraQueryExclusions, [
{ exclude: { tags: "exclude-from-incremental" } },
]);
});
test("applyIncrementalAnalysisSettings: disables overlay analysis when diff ranges are unavailable", async (t) => {
const config = createTestConfig({
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
});
config.useOverlayDatabaseCaching = true;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
const addDiagnosticsStub = sinon
.stub(overlayDiagnostics, "addOverlayDisablementDiagnostics")
.resolves();
await configUtils.applyIncrementalAnalysisSettings(
config,
false,
codeql,
logger,
);
t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None);
t.is(config.useOverlayDatabaseCaching, false);
t.deepEqual(config.extraQueryExclusions, []);
t.true(addDiagnosticsStub.calledOnce);
t.is(
addDiagnosticsStub.firstCall.args[2],
OverlayDisabledReason.DiffInformedAnalysisNotEnabled,
);
});
test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only runs", async (t) => {
const config = createTestConfig({});
config.overlayDatabaseMode = OverlayDatabaseMode.None;
const codeql = createStubCodeQL({});
const logger = getRunnerLogger(true);
await configUtils.applyIncrementalAnalysisSettings(
config,
true,
codeql,
logger,
);
t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None);
t.deepEqual(config.extraQueryExclusions, [
{ exclude: { tags: "exclude-from-incremental" } },
]);
});
+55 -13
View File
@@ -31,7 +31,7 @@ import {
addNoLanguageDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import * as errorMessages from "./error-messages";
import { Feature, FeatureEnablement } from "./feature-flags";
@@ -1077,6 +1077,48 @@ function hasQueryCustomisation(userConfig: UserConfig): boolean {
);
}
/**
* Finalize the incremental-analysis configuration for this run.
*
* Overlay analysis has only been validated in combination with diff-informed
* analysis, so if `Overlay` mode was selected for a pull request but the diff
* ranges could not be computed, fall back to a full non-overlay analysis.
*
* Query exclusions for incremental-only queries are then applied whenever the
* diff ranges are available — which, after the fallback above, is exactly the
* set of runs where any kind of incremental analysis (overlay or
* diff-informed) is in effect.
*/
export async function applyIncrementalAnalysisSettings(
config: Config,
hasDiffRanges: boolean,
codeql: CodeQL,
logger: Logger,
): Promise<void> {
if (
config.overlayDatabaseMode === OverlayDatabaseMode.Overlay &&
!hasDiffRanges
) {
logger.info(
`Reverting overlay database mode to ${OverlayDatabaseMode.None} ` +
"because the PR diff ranges could not be computed.",
);
config.overlayDatabaseMode = OverlayDatabaseMode.None;
config.useOverlayDatabaseCaching = false;
await addOverlayDisablementDiagnostics(
config,
codeql,
OverlayDisabledReason.DiffInformedAnalysisNotEnabled,
);
}
if (hasDiffRanges) {
config.extraQueryExclusions.push({
exclude: { tags: "exclude-from-incremental" },
});
}
}
/**
* Load and return the config.
*
@@ -1231,18 +1273,18 @@ export async function initConfig(
);
}
if (
config.overlayDatabaseMode === OverlayDatabaseMode.Overlay ||
(await shouldPerformDiffInformedAnalysis(
inputs.codeql,
inputs.features,
logger,
))
) {
config.extraQueryExclusions.push({
exclude: { tags: "exclude-from-incremental" },
});
}
const hasDiffRanges = await prepareDiffInformedAnalysis(
inputs.codeql,
inputs.features,
logger,
);
await applyIncrementalAnalysisSettings(
config,
hasDiffRanges,
inputs.codeql,
logger,
);
if (await isTrapCachingEnabled(features, config.overlayDatabaseMode)) {
const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime(
+136 -5
View File
@@ -5,14 +5,16 @@ import * as actionsUtil from "./actions-util";
import type { PullRequestBranches } from "./actions-util";
import * as apiClient from "./api-client";
import {
shouldPerformDiffInformedAnalysis,
getDiffInformedAnalysisBranches,
prepareDiffInformedAnalysis,
exportedForTesting,
} from "./diff-informed-analysis-utils";
import { Feature, initFeatures } from "./feature-flags";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
setupTests,
createFeatures,
mockCodeQLVersion,
mockFeatureFlagApiEndpoint,
setupActionsVars,
@@ -80,13 +82,13 @@ const testShouldPerformDiffInformedAnalysis = makeMacro({
.stub(actionsUtil, "getPullRequestBranches")
.returns(testCase.pullRequestBranches);
const result = await shouldPerformDiffInformedAnalysis(
const branches = await getDiffInformedAnalysisBranches(
codeql,
features,
logger,
);
t.is(result, expectedResult);
t.is(branches !== undefined, expectedResult);
delete process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES;
@@ -94,7 +96,7 @@ const testShouldPerformDiffInformedAnalysis = makeMacro({
getPullRequestBranchesStub.restore();
});
},
title: (title) => `shouldPerformDiffInformedAnalysis: ${title}`,
title: (title) => `getDiffInformedAnalysisBranches: ${title}`,
});
testShouldPerformDiffInformedAnalysis.serial(
@@ -178,6 +180,135 @@ testShouldPerformDiffInformedAnalysis.serial(
false,
);
test.serial(
"prepareDiffInformedAnalysis: returns false when not a pull request",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
sinon.stub(actionsUtil, "getPullRequestBranches").returns(undefined);
sinon
.stub(apiClient, "getGitHubVersion")
.resolves({ type: GitHubVariant.DOTCOM });
const result = await prepareDiffInformedAnalysis(
codeql,
features,
logger,
);
t.false(result);
});
},
);
test.serial(
"prepareDiffInformedAnalysis: returns false when applicability check throws",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const codeql = mockCodeQLVersion("2.21.0");
// A features implementation whose getValue rejects, simulating an
// unexpected failure when determining whether diff-informed analysis
// should run.
const features: FeatureEnablement = {
getEnabledDefaultCliVersions: async () => {
throw new Error("not implemented");
},
getValue: async () => {
throw new Error("feature flag lookup failed");
},
};
const result = await prepareDiffInformedAnalysis(
codeql,
features,
logger,
);
t.false(result);
});
},
);
test.serial(
"prepareDiffInformedAnalysis: returns true when the diff is fetched successfully",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
sinon
.stub(actionsUtil, "getPullRequestBranches")
.returns({ base: "main", head: "feature" });
sinon
.stub(apiClient, "getGitHubVersion")
.resolves({ type: GitHubVariant.DOTCOM });
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sinon.stub(apiClient, "getApiClient").returns({
rest: {
repos: {
compareCommitsWithBasehead: sinon
.stub()
.resolves({ data: { files: [] } }),
},
},
} as any);
const result = await prepareDiffInformedAnalysis(
codeql,
features,
logger,
);
t.true(result);
});
},
);
test.serial(
"prepareDiffInformedAnalysis: returns false when the diff API call fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const logger = getRunnerLogger(true);
const codeql = mockCodeQLVersion("2.21.0");
const features = createFeatures([Feature.DiffInformedQueries]);
sinon
.stub(actionsUtil, "getPullRequestBranches")
.returns({ base: "main", head: "feature" });
sinon
.stub(apiClient, "getGitHubVersion")
.resolves({ type: GitHubVariant.DOTCOM });
const notFoundError: any = new Error("Not Found");
notFoundError.status = 404;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
sinon.stub(apiClient, "getApiClient").returns({
rest: {
repos: {
compareCommitsWithBasehead: sinon.stub().rejects(notFoundError),
},
},
} as any);
const result = await prepareDiffInformedAnalysis(
codeql,
features,
logger,
);
t.false(result);
});
},
);
function runGetDiffRanges(changes: number, patch: string[] | undefined): any {
return exportedForTesting.getDiffRanges(
{
+69 -16
View File
@@ -5,9 +5,9 @@ import type { PullRequestBranches } from "./actions-util";
import { getApiClient, getGitHubVersion } from "./api-client";
import type { CodeQL } from "./codeql";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
import { Logger, withGroupAsync } from "./logging";
import { getRepositoryNwoFromEnv } from "./repository";
import { GitHubVariant, satisfiesGHESVersion } from "./util";
import { getErrorMessage, GitHubVariant, satisfiesGHESVersion } from "./util";
/**
* This interface is an abbreviated version of the file diff object returned by
@@ -21,20 +21,6 @@ interface FileDiff {
patch?: string | undefined;
}
/**
* Check if the action should perform diff-informed analysis.
*/
export async function shouldPerformDiffInformedAnalysis(
codeql: CodeQL,
features: FeatureEnablement,
logger: Logger,
): Promise<boolean> {
return (
(await getDiffInformedAnalysisBranches(codeql, features, logger)) !==
undefined
);
}
/**
* Get the branches to use for diff-informed analysis.
*
@@ -69,6 +55,46 @@ export async function getDiffInformedAnalysisBranches(
return branches;
}
/**
* Prepares the diff ranges needed for diff-informed analysis for the current
* run.
*
* @returns `true` if the diff ranges were successfully computed and persisted
* and are therefore available for use, `false` otherwise.
*/
export async function prepareDiffInformedAnalysis(
codeql: CodeQL,
features: FeatureEnablement,
logger: Logger,
): Promise<boolean> {
let branches: PullRequestBranches | undefined;
try {
branches = await getDiffInformedAnalysisBranches(codeql, features, logger);
} catch (e) {
// If we cannot determine whether diff-informed analysis applies (for
// example, because a feature-flag lookup failed), treat it as not
// applicable rather than triggering the overlay fallback.
logger.warning(
`Failed to determine branch information for diff-informed analysis: ${getErrorMessage(e)}`,
);
return false;
}
if (!branches) {
return false;
}
return await withGroupAsync("Computing PR diff ranges", async () => {
try {
return await computeAndPersistDiffRanges(branches, logger);
} catch (e) {
logger.warning(
`Failed to compute diff-informed analysis ranges: ${getErrorMessage(e)}`,
);
return false;
}
});
}
export interface DiffThunkRange {
/** Relative path from the repository root, using forward slashes as separators. */
path: string;
@@ -151,6 +177,33 @@ export async function getPullRequestEditedDiffRanges(
return results;
}
/**
* Compute and persist the diff ranges for a pull request. This fetches the
* diff from the GitHub API and writes it to the diff ranges JSON file so that
* CodeQL can use it for diff-informed analysis.
*
* @param branches The base and head branches of the pull request, as returned
* by `getDiffInformedAnalysisBranches`.
* @param logger
* @returns `true` if the diff ranges were successfully computed and persisted,
* otherwise `false`.
*/
export async function computeAndPersistDiffRanges(
branches: PullRequestBranches,
logger: Logger,
): Promise<boolean> {
const ranges = await getPullRequestEditedDiffRanges(branches, logger);
if (ranges === undefined) {
return false;
}
writeDiffRangesJsonFile(logger, ranges);
const distinctFiles = new Set(ranges.map((r) => r.path)).size;
logger.info(
`Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).`,
);
return true;
}
async function getFileDiffsWithBasehead(
branches: PullRequestBranches,
logger: Logger,
+3
View File
@@ -0,0 +1,3 @@
export async function run__ACTION__() {
return await __ACTION__.runWrapper();
}
+1 -3
View File
@@ -207,7 +207,7 @@ function getJobStatusFromEnvironment(): JobStatus | undefined {
return undefined;
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -222,5 +222,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+1 -45
View File
@@ -37,11 +37,6 @@ import {
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import {
getDiffInformedAnalysisBranches,
getPullRequestEditedDiffRanges,
writeDiffRangesJsonFile,
} from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement, initFeatures } from "./feature-flags";
import {
@@ -434,7 +429,6 @@ async function run(startedAt: Date) {
}
await checkInstallPython311(config.languages, codeql);
await computeAndPersistDiffRanges(codeql, features, logger);
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
core.setFailed(error.message);
@@ -830,42 +824,6 @@ async function loadRepositoryProperties(
}
}
/**
* Compute and persist diff ranges when diff-informed analysis is enabled
* (feature flag + PR context). This writes the standard pr-diff-range.json
* file for later reuse in the analyze step. Failures are logged but non-fatal.
*/
async function computeAndPersistDiffRanges(
codeql: CodeQL,
features: FeatureEnablement,
logger: Logger,
): Promise<void> {
await withGroupAsync("Computing PR diff ranges", async () => {
try {
const branches = await getDiffInformedAnalysisBranches(
codeql,
features,
logger,
);
if (!branches) {
return;
}
const ranges = await getPullRequestEditedDiffRanges(branches, logger);
if (ranges === undefined) {
return;
}
writeDiffRangesJsonFile(logger, ranges);
const distinctFiles = new Set(ranges.map((r) => r.path)).size;
logger.info(
`Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).`,
);
} catch (e) {
logger.warning(
`Failed to compute and persist PR diff ranges: ${getErrorMessage(e)}`,
);
}
});
}
async function recordZstdAvailability(
config: configUtils.Config,
zstdAvailability: ZstdAvailability,
@@ -880,7 +838,7 @@ async function recordZstdAvailability(
);
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -896,5 +854,3 @@ async function runWrapper() {
}
await checkForTimeout();
}
void runWrapper();
+9
View File
@@ -39,6 +39,15 @@ export enum OverlayDisabledReason {
NotPullRequestOrDefaultBranch = "not-pull-request-or-default-branch",
/** The top-level overlay analysis feature flag is not enabled. */
OverallFeatureNotEnabled = "overall-feature-not-enabled",
/**
* Overlay analysis was selected for a pull request, but diff-informed
* analysis was not enabled for the run (for example, because the
* `DiffInformedQueries` feature flag is off, the GHES version is too old,
* or the PR diff ranges could not be computed). Overlay analysis has only
* been validated in combination with diff-informed analysis, so we fall
* back to a non-overlay analysis in this case.
*/
DiffInformedAnalysisNotEnabled = "diff-informed-analysis-not-enabled",
/** Overlay analysis was skipped because it previously failed with similar hardware resources. */
SkippedDueToCachedStatus = "skipped-due-to-cached-status",
/** Disk usage could not be determined during the overlay status check. */
+1 -3
View File
@@ -117,7 +117,7 @@ async function run(startedAt: Date) {
}
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -137,5 +137,3 @@ async function runWrapper() {
}
await checkForTimeout();
}
void runWrapper();
+1 -3
View File
@@ -196,7 +196,7 @@ async function run(startedAt: Date): Promise<void> {
}
/** Run the action and catch any unhandled errors. */
async function runWrapper(): Promise<void> {
export async function runWrapper(): Promise<void> {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -212,5 +212,3 @@ async function runWrapper(): Promise<void> {
}
await checkForTimeout();
}
void runWrapper();
+1 -3
View File
@@ -12,7 +12,7 @@ import { uploadArtifacts } from "./debug-artifacts";
import { getActionsLogger } from "./logging";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
async function runWrapper() {
export async function runWrapper() {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -62,5 +62,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+1 -3
View File
@@ -128,7 +128,7 @@ async function run(startedAt: Date) {
}
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
@@ -204,5 +204,3 @@ async function startProxy(
return { host, port, cert: config.ca.cert, registries: registry_urls };
}
void runWrapper();
+1 -3
View File
@@ -12,7 +12,7 @@ import { EnvVar } from "./environment";
import { getActionsLogger, withGroup } from "./logging";
import { checkGitHubVersionInRange, getErrorMessage } from "./util";
async function runWrapper() {
export async function runWrapper() {
// To capture errors appropriately, keep as much code within the try-catch as
// possible, and only use safe functions outside.
@@ -48,5 +48,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+1 -3
View File
@@ -165,7 +165,7 @@ async function run(startedAt: Date) {
}
}
async function runWrapper() {
export async function runWrapper() {
const startedAt = new Date();
const logger = getActionsLogger();
try {
@@ -182,5 +182,3 @@ async function runWrapper() {
);
}
}
void runWrapper();
+2 -2
View File
@@ -30,5 +30,5 @@ outputs:
description: A stringified JSON array of objects containing the types and URLs of the configured registries.
runs:
using: node24
main: "../lib/start-proxy-action.js"
post: "../lib/start-proxy-action-post.js"
main: "../lib/start-proxy-entry.js"
post: "../lib/start-proxy-post-entry.js"
+2 -2
View File
@@ -42,5 +42,5 @@ outputs:
{ "code-scanning": "some-id", "code-quality": "some-other-id" }
runs:
using: node24
main: '../lib/upload-sarif-action.js'
post: '../lib/upload-sarif-action-post.js'
main: '../lib/upload-sarif-entry.js'
post: '../lib/upload-sarif-post-entry.js'