From c5128610cf78979aef5494f836ee1a27f3e17f14 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 3 Jul 2026 15:34:40 +0100 Subject: [PATCH 01/18] Extend `UserConfig` type to be aware of Default Setup properties --- src/config/db-config.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config/db-config.ts b/src/config/db-config.ts index a84a20f247..73900a77bc 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -28,6 +28,14 @@ export interface QuerySpec { uses: string; } +/** Not intended to be provided directly by a user. */ +export interface DefaultSetupConfig { + org?: { + /** An array of model pack names. */ + "model-packs"?: string[]; + }; +} + /** * Format of the config file supplied by the user. */ @@ -46,6 +54,15 @@ export interface UserConfig { // Set of query filters to include and exclude extra queries based on // codeql query suite `include` and `exclude` properties "query-filters"?: QueryFilter[]; + + /** An array (possibly empty or absent) of threat models to use. */ + "threat-models"?: string[]; + + /** + * Configuration options that are reserved for us in Default Setup and + * not intended to be supplied directly by users. + */ + "default-setup"?: DefaultSetupConfig; } /** From cd604f831b6cda0421c749a37e3eaffe484179d1 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 3 Jul 2026 15:44:34 +0100 Subject: [PATCH 02/18] Refactor `determineUserConfig` out of `initConfig` --- lib/entry-points.js | 16 ++++++++++++---- src/config-utils.ts | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 44e510c1b7..5fd8835db8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148661,8 +148661,7 @@ async function applyIncrementalAnalysisSettings(config, hasDiffRanges, codeql, l }); } } -async function initConfig(features, inputs) { - const { logger, tempDir } = inputs; +async function determineUserConfig(logger, features, tempDir, inputs) { if (inputs.configInput) { if (inputs.configFile) { logger.warning( @@ -148673,13 +148672,13 @@ async function initConfig(features, inputs) { fs9.writeFileSync(inputs.configFile, inputs.configInput); logger.debug(`Using config from action input: ${inputs.configFile}`); } - let userConfig = {}; if (!inputs.configFile) { logger.debug("No configuration file was provided"); + return {}; } else { logger.debug(`Using configuration file: ${inputs.configFile}`); const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); - userConfig = await loadUserConfig( + return await loadUserConfig( logger, inputs.configFile, inputs.workspacePath, @@ -148688,6 +148687,15 @@ async function initConfig(features, inputs) { validateConfig ); } +} +async function initConfig(features, inputs) { + const { logger, tempDir } = inputs; + const userConfig = await determineUserConfig( + logger, + features, + tempDir, + inputs + ); const config = await initActionState(inputs, userConfig); if (config.analysisKinds.length === 1 && isCodeQualityEnabled(config)) { if (hasQueryCustomisation(config.computedConfig)) { diff --git a/src/config-utils.ts b/src/config-utils.ts index 972734877a..bb87c4992d 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1132,17 +1132,17 @@ export async function applyIncrementalAnalysisSettings( } /** - * Load and return the config. + * Determines where to load the `UserConfig` for the CLI from and loads it. * - * This will parse the config from the user input if present, or generate - * a default config. The parsed config is then stored to a known location. + * @returns The loaded `UserConfig`, which might be empty if no configuration + * was specified. */ -export async function initConfig( +async function determineUserConfig( + logger: Logger, features: FeatureEnablement, + tempDir: string, inputs: InitConfigInputs, -): Promise { - const { logger, tempDir } = inputs; - +): Promise { // if configInput is set, it takes precedence over configFile if (inputs.configInput) { if (inputs.configFile) { @@ -1155,13 +1155,13 @@ export async function initConfig( logger.debug(`Using config from action input: ${inputs.configFile}`); } - let userConfig: UserConfig = {}; if (!inputs.configFile) { logger.debug("No configuration file was provided"); + return {}; } else { logger.debug(`Using configuration file: ${inputs.configFile}`); const validateConfig = await features.getValue(Feature.ValidateDbConfig); - userConfig = await loadUserConfig( + return await loadUserConfig( logger, inputs.configFile, inputs.workspacePath, @@ -1170,6 +1170,26 @@ export async function initConfig( validateConfig, ); } +} + +/** + * Load and return the config. + * + * This will parse the config from the user input if present, or generate + * a default config. The parsed config is then stored to a known location. + */ +export async function initConfig( + features: FeatureEnablement, + inputs: InitConfigInputs, +): Promise { + const { logger, tempDir } = inputs; + + const userConfig = await determineUserConfig( + logger, + features, + tempDir, + inputs, + ); const config = await initActionState(inputs, userConfig); From a52a9662e9ed59dcf5726c3eb40ba4a85db8535a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 3 Jul 2026 16:15:32 +0100 Subject: [PATCH 03/18] Add `mergeUserConfigs` with tests --- src/config/db-config.test.ts | 65 ++++++++++++++++++++++++++++++++++++ src/config/db-config.ts | 50 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index ca0061e136..4d523ad0a9 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -8,6 +8,7 @@ import { getRecordingLogger, LoggedMessage, makeMacro, + RecordingLogger, } from "../testing-utils"; import { ConfigurationError, prettyPrintPack } from "../util"; @@ -488,3 +489,67 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail, ), ); }); + +test("mergeUserConfigs - combines threat models", async (t) => { + const logger = new RecordingLogger(); + const result = dbConfig.mergeUserConfigs( + logger, + { "threat-models": ["a", "b"] }, + { "threat-models": ["local", "remote"] }, + ); + + const threatModels = result["threat-models"]; + + if (t.truthy(threatModels)) { + t.deepEqual(threatModels, ["a", "b", "local", "remote"]); + } +}); + +test("mergeUserConfigs - warns if user-supplied config contains default setup key", async (t) => { + const logger = new RecordingLogger(); + const result = dbConfig.mergeUserConfigs(logger, {}, { "default-setup": {} }); + + // User-supplied value is ignored. + t.deepEqual(result, {}); + + // Warning is logged. + t.true( + logger.hasMessage( + "The 'default-setup' configuration key is not supported in user-supplied configuration files", + ), + ); +}); + +test("mergeUserConfigs - keeps default setup key from 'config' input", async (t) => { + const logger = new RecordingLogger(); + const expected: dbConfig.DefaultSetupConfig = { + org: { "model-packs": ["some-pack"] }, + }; + const result = dbConfig.mergeUserConfigs( + logger, + { "default-setup": expected }, + {}, + ); + + // Result matches the input. + t.deepEqual(result["default-setup"], expected); + + // No warning is logged. + t.false( + logger.hasMessage( + "The 'default-setup' configuration key is not supported in user-supplied configuration files", + ), + ); +}); + +test("mergeUserConfigs - keeps other properties from user-supplied configuration", async (t) => { + const logger = new RecordingLogger(); + const configFile: dbConfig.UserConfig = { + "query-filters": [{ exclude: { a: "b" } }], + "paths-ignore": ["path"], + }; + + const result = dbConfig.mergeUserConfigs(logger, {}, configFile); + + t.deepEqual(result, configFile); +}); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 73900a77bc..c24994476f 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -65,6 +65,56 @@ export interface UserConfig { "default-setup"?: DefaultSetupConfig; } +/** + * Merges supported properties from two configuration files. This is intended only for + * use with merging the `config` input provided by Default Setup with a potentially + * richer configuration file provided by a user. + * + * @param logger The logger to use. + * @param fromConfigInput The configuration from Default Setup. + * @param fromConfigFile The user-supplied configuration. + * @returns The combination of both configuration files. + */ +export function mergeUserConfigs( + logger: Logger, + fromConfigInput: UserConfig, + fromConfigFile: UserConfig, +): UserConfig { + logger.debug( + "Combining configuration files from 'config' and 'config-file' inputs", + ); + + // Combine all specified threat models from both sources. + const threatModels = new Set(fromConfigInput["threat-models"] || []); + for (const configFileThreatModel of fromConfigFile["threat-models"] || []) { + threatModels.add(configFileThreatModel); + } + + // Warn if there is a 'default-setup' configuration key in the user-supplied configuration, + // since it is not meant to be used and we therefore ignore it here. + if (fromConfigFile["default-setup"]) { + logger.warning( + `The 'default-setup' configuration key is not supported in user-supplied configuration files and will be ignored.`, + ); + } + + // Since we expect the `fromConfigInput` configuration to be provided by Default Setup, + // we expect a limited set of options. Therefore, we base the overall configuration on + // the one provided via the `config-file` input, which may be richer. + const result = { ...fromConfigFile }; + delete result["threat-models"]; + delete result["default-setup"]; + + if (fromConfigInput["default-setup"]) { + result["default-setup"] = fromConfigInput["default-setup"]; + } + if (threatModels.size > 0) { + result["threat-models"] = Array.from(threatModels); + } + + return result; +} + /** * Represents additional configuration data from a source other than * a configuration file. From 132634ddfda5200122c1e6178f76cdbe2d2f8d25 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 11:50:36 +0100 Subject: [PATCH 04/18] Add FF for configuration merging --- lib/entry-points.js | 5 +++++ src/feature-flags.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/lib/entry-points.js b/lib/entry-points.js index 5fd8835db8..fd2fe602dd 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -145967,6 +145967,11 @@ var LINKED_CODEQL_VERSION = { tagName: bundleVersion }; var featureConfig = { + ["allow_merge_config_files" /* AllowMergeConfigFiles */]: { + defaultValue: false, + envVar: "CODEQL_ACTION_ALLOW_MERGE_CONFIG_FILES", + minimumVersion: void 0 + }, ["allow_multiple_analysis_kinds" /* AllowMultipleAnalysisKinds */]: { defaultValue: false, envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS", diff --git a/src/feature-flags.ts b/src/feature-flags.ts index f71ecab57b..5b6d623a7a 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -70,6 +70,8 @@ export interface CodeQLDefaultVersionInfo { * Legacy features should end with `_enabled`. */ export enum Feature { + /** Allows supported properties of configuration files to be merged. */ + AllowMergeConfigFiles = "allow_merge_config_files", /** Controls whether we allow multiple values for the `analysis-kinds` input. */ AllowMultipleAnalysisKinds = "allow_multiple_analysis_kinds", AllowToolcacheInput = "allow_toolcache_input", @@ -171,6 +173,11 @@ export type FeatureConfig = { }; export const featureConfig = { + [Feature.AllowMergeConfigFiles]: { + defaultValue: false, + envVar: "CODEQL_ACTION_ALLOW_MERGE_CONFIG_FILES", + minimumVersion: undefined, + }, [Feature.AllowMultipleAnalysisKinds]: { defaultValue: false, envVar: "CODEQL_ACTION_ALLOW_MULTIPLE_ANALYSIS_KINDS", From 53a488ce2e59b206522f5293323f64e4ada3cb59 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 11:52:54 +0100 Subject: [PATCH 05/18] Add JSDoc for `loadUserConfig` --- src/config-utils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/config-utils.ts b/src/config-utils.ts index bb87c4992d..c57c9abc89 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -598,6 +598,17 @@ async function downloadCacheWithTime( return { trapCaches, trapCacheDownloadTime }; } +/** + * Loads a CLI configuration file from `configFile`. + * + * @param logger The logger to use. + * @param configFile The address of the configuration file. + * @param workspacePath The workspace path, used to check that the configuration file exists relative to it. + * @param apiDetails Information for how to access the API to fetch remote files. + * @param tempDir The temporary directory which may contain a CodeQL Action-generated configuration file. + * @param validateConfig Whether to validate the configuration file. + * @returns The loaded configuration file, if successful. + */ async function loadUserConfig( logger: Logger, configFile: string, From 6860cc1f508dd47ae022eeaf795f038d945f915b Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 11:54:15 +0100 Subject: [PATCH 06/18] Move `validateConfig` FF query --- lib/entry-points.js | 2 +- src/config-utils.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index fd2fe602dd..9ac2c64a17 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148667,6 +148667,7 @@ async function applyIncrementalAnalysisSettings(config, hasDiffRanges, codeql, l } } async function determineUserConfig(logger, features, tempDir, inputs) { + const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); if (inputs.configInput) { if (inputs.configFile) { logger.warning( @@ -148682,7 +148683,6 @@ async function determineUserConfig(logger, features, tempDir, inputs) { return {}; } else { logger.debug(`Using configuration file: ${inputs.configFile}`); - const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); return await loadUserConfig( logger, inputs.configFile, diff --git a/src/config-utils.ts b/src/config-utils.ts index c57c9abc89..b67a8f9e28 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1154,6 +1154,8 @@ async function determineUserConfig( tempDir: string, inputs: InitConfigInputs, ): Promise { + const validateConfig = await features.getValue(Feature.ValidateDbConfig); + // if configInput is set, it takes precedence over configFile if (inputs.configInput) { if (inputs.configFile) { @@ -1166,12 +1168,12 @@ async function determineUserConfig( logger.debug(`Using config from action input: ${inputs.configFile}`); } + // Load whatever configuration file we have, if any. if (!inputs.configFile) { logger.debug("No configuration file was provided"); return {}; } else { logger.debug(`Using configuration file: ${inputs.configFile}`); - const validateConfig = await features.getValue(Feature.ValidateDbConfig); return await loadUserConfig( logger, inputs.configFile, From 0188f395e2d1a5876d15002f422d5f3eefd8110e Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 12:17:44 +0100 Subject: [PATCH 07/18] Add unit tests for existing behaviour of `determineUserConfig` --- src/config-utils.test.ts | 183 +++++++++++++++++++++++++++++++++++---- src/config-utils.ts | 8 +- 2 files changed, 171 insertions(+), 20 deletions(-) diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 27de780ad5..cdaad5761e 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -36,6 +36,7 @@ import { mockCodeQLVersion, createTestConfig, makeMacro, + RecordingLogger, } from "./testing-utils"; import { GitHubVariant, @@ -482,6 +483,24 @@ test.serial("load non-existent input", async (t) => { }); }); +/** A non-empty, but fairly minimal configuration file. */ +const simpleConfigFileContents = ` + name: my config + queries: + - uses: ./foo_file`; + +/** A less minimal configuration file. */ +const otherConfigFileContents = ` + name: my config + disable-default-queries: true + queries: + - uses: ./foo + paths-ignore: + - a + - b + paths: + - c/d`; + test.serial("load non-empty input", async (t) => { return await withTmpDir(async (tempDir) => { setupActionsVars(tempDir, tempDir); @@ -496,18 +515,6 @@ test.serial("load non-empty input", async (t) => { }, }); - // Just create a generic config object with non-default values for all fields - const inputFileContents = ` - name: my config - disable-default-queries: true - queries: - - uses: ./foo - paths-ignore: - - a - - b - paths: - - c/d`; - fs.mkdirSync(path.join(tempDir, "foo")); const userConfig: UserConfig = { @@ -534,7 +541,7 @@ test.serial("load non-empty input", async (t) => { }); const languagesInput = "javascript"; - const configFilePath = createConfigFile(inputFileContents, tempDir); + const configFilePath = createConfigFile(otherConfigFileContents, tempDir); const actualConfig = await configUtils.initConfig( createFeatures([]), @@ -562,11 +569,10 @@ test.serial( process.env["RUNNER_TEMP"] = tempDir; process.env["GITHUB_WORKSPACE"] = tempDir; - const inputFileContents = ` - name: my config - queries: - - uses: ./foo_file`; - const configFilePath = createConfigFile(inputFileContents, tempDir); + const configFilePath = createConfigFile( + simpleConfigFileContents, + tempDir, + ); const configInput = ` name: my config @@ -2272,3 +2278,144 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r { exclude: { tags: "exclude-from-incremental" } }, ]); }); + +test("determineUserConfig - empty config when neither input is specified", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + + const result = await configUtils.determineUserConfig( + logger, + createFeatures([]), + tmpDir, + createTestInitConfigInputs({ + configInput: undefined, + configFile: undefined, + }), + ); + + // The returned configuration should be empty. + t.deepEqual(result, {}); + // And the fact that no configuration was provided should have been logged, + // but not the messages for the two input sources. + t.true(logger.hasMessage("No configuration file was provided")); + t.false(logger.hasMessage("Using config from action input:")); + t.false(logger.hasMessage("Using configuration file:")); + // And the warning about both inputs should not have been logged. + t.false( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); + +test("determineUserConfig - loads config file", async (t) => { + await withTmpDir(async (tmpDir) => { + const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir); + const logger = new RecordingLogger(); + + const result = await configUtils.determineUserConfig( + logger, + createFeatures([]), + tmpDir, + createTestInitConfigInputs({ + configInput: undefined, + configFile: configFilePath, + }), + ); + + // The loaded configuration should match `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); + // And the path of the input config file should have been logged, while the + // other two origin messages should not have been logged. + t.true(logger.hasMessage(`Using configuration file: ${configFilePath}`)); + t.false(logger.hasMessage("No configuration file was provided")); + t.false(logger.hasMessage("Using config from action input:")); + // But the warning about both inputs should not have been logged. + t.false( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); + +test("determineUserConfig - loads config input", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + + const result = await configUtils.determineUserConfig( + logger, + createFeatures([]), + tmpDir, + createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: undefined, + }), + ); + + // The loaded configuration should match `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); + // And the input source and path of the generated config file should have been logged, + // while the message about no configuration input should not have been logged. + t.true(logger.hasMessage("Using config from action input:")); + t.true( + logger.hasMessage( + `Using configuration file: ${configUtils.userConfigFromActionPath(tmpDir)}`, + ), + ); + t.false(logger.hasMessage("No configuration file was provided")); + // But the warning about both inputs should not have been logged. + t.false( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); + +test("determineUserConfig - ignores config file input when both specified", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(); + const configFilePath = createConfigFile(otherConfigFileContents, tmpDir); + const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + + const result = await configUtils.determineUserConfig( + logger, + createFeatures([]), + tmpDir, + createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: configFilePath, + }), + ); + + // The loaded configuration should match `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); + // And the path of the generated config file should have been logged. + t.true( + logger.hasMessage( + `Using config from action input: ${expectedConfigPath}`, + ), + ); + t.true( + logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), + ); + t.false(logger.hasMessage("No configuration file was provided")); + // And the warning about both inputs should have been logged. + t.true( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); diff --git a/src/config-utils.ts b/src/config-utils.ts index b67a8f9e28..f9c4bb7023 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1082,7 +1082,11 @@ function dbLocationOrDefault( return dbLocation || path.resolve(tempDir, "codeql_databases"); } -function userConfigFromActionPath(tempDir: string): string { +/** + * Gets the path for the CodeQL Action-generated configuration file, + * which is used to store the `config` input. + */ +export function userConfigFromActionPath(tempDir: string): string { return path.resolve(tempDir, "user-config-from-action.yml"); } @@ -1148,7 +1152,7 @@ export async function applyIncrementalAnalysisSettings( * @returns The loaded `UserConfig`, which might be empty if no configuration * was specified. */ -async function determineUserConfig( +export async function determineUserConfig( logger: Logger, features: FeatureEnablement, tempDir: string, From d1db924e0b79fd8a27931863729a47459c1b2640 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 12:55:07 +0100 Subject: [PATCH 08/18] Allow `env` input for relevant functions in `actions-util.ts` --- lib/entry-points.js | 64 ++++++++++++++++---------------- src/actions-util.ts | 89 +++++++++++++++++++++++++++++---------------- src/environment.ts | 5 +++ src/util.ts | 1 + 4 files changed, 97 insertions(+), 62 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 9ac2c64a17..9e872e7d03 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144984,31 +144984,34 @@ var getOptionalInput = function(name) { const value = core3.getInput(name); return value.length > 0 ? value : void 0; }; -function getTemporaryDirectory() { - const value = process.env["CODEQL_ACTION_TEMP"]; - return value !== void 0 && value !== "" ? value : getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); +function getTemporaryDirectory(env) { + const value = env?.getOptional("CODEQL_ACTION_TEMP" /* TEMP */) || process.env["CODEQL_ACTION_TEMP" /* TEMP */]; + return value !== void 0 && value !== "" ? value : env?.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */) || getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); } var PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -function getDiffRangesJsonFilePath() { - return path2.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME); +function getDiffRangesJsonFilePath(env) { + return path2.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } function getActionVersion() { return "4.36.4"; } -function getWorkflowEventName() { +function getWorkflowEventName(env) { + if (env) { + return env.getRequired("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); + } return getRequiredEnvParam("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); } -function isRunningLocalAction() { - const relativeScriptPath = getRelativeScriptPath(); +function isRunningLocalAction(env) { + const relativeScriptPath = getRelativeScriptPath(env); return relativeScriptPath.startsWith("..") || path2.isAbsolute(relativeScriptPath); } -function getRelativeScriptPath() { - const runnerTemp = getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); +function getRelativeScriptPath(env) { + const runnerTemp = env === void 0 ? getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */) : env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); const actionsDirectory = path2.join(path2.dirname(runnerTemp), "_actions"); return path2.relative(actionsDirectory, __filename); } -function getWorkflowEvent() { - const eventJsonFile = getRequiredEnvParam("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); +function getWorkflowEvent(env) { + const eventJsonFile = env === void 0 ? getRequiredEnvParam("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */) : env.getRequired("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); try { return JSON.parse(fs2.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -145064,8 +145067,8 @@ function getUploadValue(input) { return "always"; } } -function getWorkflowRunID() { - const workflowRunIdString = getRequiredEnvParam("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); +function getWorkflowRunID(env) { + const workflowRunIdString = env === void 0 ? getRequiredEnvParam("GITHUB_RUN_ID" /* GITHUB_RUN_ID */) : env.getRequired("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -145079,10 +145082,8 @@ function getWorkflowRunID() { } return workflowRunID; } -function getWorkflowRunAttempt() { - const workflowRunAttemptString = getRequiredEnvParam( - "GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */ - ); +function getWorkflowRunAttempt(env) { + const workflowRunAttemptString = env === void 0 ? getRequiredEnvParam("GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */) : env.getRequired("GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( @@ -145136,11 +145137,11 @@ var getFileType = async (filePath) => { function isSelfHostedRunner() { return process.env.RUNNER_ENVIRONMENT === "self-hosted"; } -function isDynamicWorkflow() { - return getWorkflowEventName() === "dynamic"; +function isDynamicWorkflow(env) { + return getWorkflowEventName(env) === "dynamic"; } -function isDefaultSetup() { - return isDynamicWorkflow(); +function isDefaultSetup(env) { + return isDynamicWorkflow(env); } function prettyPrintInvocation(cmd, args) { return [cmd, ...args].map((x) => x.includes(" ") ? `'${x}'` : x).join(" "); @@ -145204,8 +145205,9 @@ async function runTool(cmd, args = [], opts = {}) { return stdout; } var persistedInputsKey = "persisted_inputs"; -var persistInputs = function() { - const inputEnvironmentVariables = Object.entries(process.env).filter( +var persistInputs = function(env) { + const entries = env?.entries() || Object.entries(process.env); + const inputEnvironmentVariables = entries.filter( ([name]) => name.startsWith("INPUT_") ); core3.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables)); @@ -145218,7 +145220,7 @@ var restoreInputs = function() { } } }; -function getPullRequestBranches() { +function getPullRequestBranches(env) { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -145229,8 +145231,8 @@ function getPullRequestBranches() { head: pullRequest.head.label }; } - const codeScanningRef = process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = env?.getOptional("CODE_SCANNING_REF") || process.env.CODE_SCANNING_REF; + const codeScanningBaseBranch = env?.getOptional("CODE_SCANNING_BASE_BRANCH") || process.env.CODE_SCANNING_BASE_BRANCH; if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -145241,8 +145243,8 @@ function getPullRequestBranches() { } return void 0; } -function isAnalyzingPullRequest() { - return getPullRequestBranches() !== void 0; +function isAnalyzingPullRequest(env) { + return getPullRequestBranches(env) !== void 0; } var qualityCategoryMapping = { "c#": "csharp", @@ -145254,8 +145256,8 @@ var qualityCategoryMapping = { typescript: "javascript-typescript", kotlin: "java-kotlin" }; -function fixCodeQualityCategory(logger, category) { - if (category !== void 0 && isDefaultSetup() && category.startsWith("/language:")) { +function fixCodeQualityCategory(logger, category, env) { + if (category !== void 0 && isDefaultSetup(env) && category.startsWith("/language:")) { const language = category.substring("/language:".length); const mappedLanguage = qualityCategoryMapping[language]; if (mappedLanguage) { diff --git a/src/actions-util.ts b/src/actions-util.ts index d7fbacbf3e..5d7e3d91ef 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -7,6 +7,7 @@ import * as github from "@actions/github"; import * as io from "@actions/io"; import type { Config } from "./config-utils"; +import { Env, EnvVar } from "./environment"; import { Logger } from "./logging"; import { doesDirectoryExist, @@ -83,17 +84,23 @@ export const getOptionalInput = function (name: string): string | undefined { return value.length > 0 ? value : undefined; }; -export function getTemporaryDirectory(): string { - const value = process.env["CODEQL_ACTION_TEMP"]; +/** + * Gets the temporary directory used by the CodeQL Action. This will either be the temporary + * directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the + * value of `RUNNER_TEMP` otherwise. + */ +export function getTemporaryDirectory(env?: Env): string { + const value = env?.getOptional(EnvVar.TEMP) || process.env[EnvVar.TEMP]; return value !== undefined && value !== "" ? value - : getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); + : env?.getRequired(ActionsEnvVars.RUNNER_TEMP) || + getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); } const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -export function getDiffRangesJsonFilePath(): string { - return path.join(getTemporaryDirectory(), PR_DIFF_RANGE_JSON_FILENAME); +export function getDiffRangesJsonFilePath(env?: Env): string { + return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } export function getActionVersion(): string { @@ -105,7 +112,10 @@ export function getActionVersion(): string { * * This will be "dynamic" for default setup workflow runs. */ -export function getWorkflowEventName() { +export function getWorkflowEventName(env?: Env) { + if (env) { + return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME); + } return getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_NAME); } @@ -113,8 +123,8 @@ export function getWorkflowEventName() { * Returns whether the current workflow is executing a local copy of the Action, e.g. we're running * a workflow on the codeql-action repo itself. */ -export function isRunningLocalAction(): boolean { - const relativeScriptPath = getRelativeScriptPath(); +export function isRunningLocalAction(env?: Env): boolean { + const relativeScriptPath = getRelativeScriptPath(env); return ( relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath) ); @@ -125,15 +135,21 @@ export function isRunningLocalAction(): boolean { * * This can be used to get the Action's name or tell if we're running a local Action. */ -function getRelativeScriptPath(): string { - const runnerTemp = getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); +function getRelativeScriptPath(env?: Env): string { + const runnerTemp = + env === undefined + ? getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP) + : env.getRequired(ActionsEnvVars.RUNNER_TEMP); const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions"); return path.relative(actionsDirectory, __filename); } /** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */ -export function getWorkflowEvent(): any { - const eventJsonFile = getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH); +export function getWorkflowEvent(env?: Env): any { + const eventJsonFile = + env === undefined + ? getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH) + : env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH); try { return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -202,8 +218,11 @@ export function getUploadValue(input: string | undefined): UploadKind { /** * Get the workflow run ID. */ -export function getWorkflowRunID(): number { - const workflowRunIdString = getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID); +export function getWorkflowRunID(env?: Env): number { + const workflowRunIdString = + env === undefined + ? getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID) + : env.getRequired(ActionsEnvVars.GITHUB_RUN_ID); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -221,10 +240,11 @@ export function getWorkflowRunID(): number { /** * Get the workflow run attempt number. */ -export function getWorkflowRunAttempt(): number { - const workflowRunAttemptString = getRequiredEnvParam( - ActionsEnvVars.GITHUB_RUN_ATTEMPT, - ); +export function getWorkflowRunAttempt(env?: Env): number { + const workflowRunAttemptString = + env === undefined + ? getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ATTEMPT) + : env.getRequired(ActionsEnvVars.GITHUB_RUN_ATTEMPT); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( @@ -295,13 +315,13 @@ export function isSelfHostedRunner() { } /** Determines whether the workflow trigger is `dynamic`. */ -export function isDynamicWorkflow(): boolean { - return getWorkflowEventName() === "dynamic"; +export function isDynamicWorkflow(env?: Env): boolean { + return getWorkflowEventName(env) === "dynamic"; } /** Determines whether we are running in default setup. */ -export function isDefaultSetup(): boolean { - return isDynamicWorkflow(); +export function isDefaultSetup(env?: Env): boolean { + return isDynamicWorkflow(env); } export function prettyPrintInvocation(cmd: string, args: string[]): string { @@ -399,9 +419,10 @@ const persistedInputsKey = "persisted_inputs"; * This would be simplified if actions/runner#3514 is addressed. * https://github.com/actions/runner/issues/3514 */ -export const persistInputs = function () { - const inputEnvironmentVariables = Object.entries(process.env).filter( - ([name]) => name.startsWith("INPUT_"), +export const persistInputs = function (env?: Env) { + const entries = env?.entries() || Object.entries(process.env); + const inputEnvironmentVariables = entries.filter(([name]) => + name.startsWith("INPUT_"), ); core.saveState(persistedInputsKey, JSON.stringify(inputEnvironmentVariables)); }; @@ -429,7 +450,9 @@ export interface PullRequestBranches { * @returns the base and head branches of the pull request, or undefined if * we are not analyzing a pull request. */ -export function getPullRequestBranches(): PullRequestBranches | undefined { +export function getPullRequestBranches( + env?: Env, +): PullRequestBranches | undefined { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -443,8 +466,11 @@ export function getPullRequestBranches(): PullRequestBranches | undefined { // PR analysis under Default Setup does not have the pull_request context, // but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH. - const codeScanningRef = process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = + env?.getOptional("CODE_SCANNING_REF") || process.env.CODE_SCANNING_REF; + const codeScanningBaseBranch = + env?.getOptional("CODE_SCANNING_BASE_BRANCH") || + process.env.CODE_SCANNING_BASE_BRANCH; if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -459,8 +485,8 @@ export function getPullRequestBranches(): PullRequestBranches | undefined { /** * Returns whether we are analyzing a pull request. */ -export function isAnalyzingPullRequest(): boolean { - return getPullRequestBranches() !== undefined; +export function isAnalyzingPullRequest(env?: Env): boolean { + return getPullRequestBranches(env) !== undefined; } /** @@ -484,13 +510,14 @@ const qualityCategoryMapping: Record = { export function fixCodeQualityCategory( logger: Logger, category?: string, + env?: Env, ): string | undefined { // The `category` should always be set by Default Setup. We perform this check // to avoid potential issues if Code Quality supports Advanced Setup in the future // and before this workaround is removed. if ( category !== undefined && - isDefaultSetup() && + isDefaultSetup(env) && category.startsWith("/language:") ) { const language = category.substring("/language:".length); diff --git a/src/environment.ts b/src/environment.ts index c0ca050b03..9912f03825 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -83,6 +83,9 @@ export enum EnvVar { /** Whether to suppress the warning if the current CLI will soon be unsupported. */ SUPPRESS_DEPRECATED_SOON_WARNING = "CODEQL_ACTION_SUPPRESS_DEPRECATED_SOON_WARNING", + /** Used to dictate or persist the temporary directory used by the CodeQL Action. */ + TEMP = "CODEQL_ACTION_TEMP", + /** Whether to disable uploading SARIF results or status reports to the GitHub API */ TEST_MODE = "CODEQL_ACTION_TEST_MODE", @@ -167,4 +170,6 @@ export interface Env { getRequired(name: string): string; /** Gets the value for `name`, or `undefined` if it isn't set or empty. */ getOptional(name: string): string | undefined; + /** Gets the entries of the underlying `ProcessEnv`. */ + entries(): Array<[string, string | undefined]>; } diff --git a/src/util.ts b/src/util.ts index 7f52456608..1e79382f6e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -571,6 +571,7 @@ export function getEnv(env: NodeJS.ProcessEnv = process.env): Env { return { getRequired: (name) => getRequiredEnvVar(env, name), getOptional: (name) => getOptionalEnvVarFrom(env, name), + entries: () => Object.entries(env), }; } From 18d20b7c42c6c22f393d6be203f9fa369cf852f4 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:06:23 +0100 Subject: [PATCH 09/18] Allow setting environment variables --- src/environment.ts | 2 ++ src/testing-utils.ts | 12 ++++++++++-- src/util.ts | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/environment.ts b/src/environment.ts index 9912f03825..03d4870be1 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -172,4 +172,6 @@ export interface Env { getOptional(name: string): string | undefined; /** Gets the entries of the underlying `ProcessEnv`. */ entries(): Array<[string, string | undefined]>; + /** Sets an environment variable. */ + set(name: string, value: string): void; } diff --git a/src/testing-utils.ts b/src/testing-utils.ts index 22ed1e21a5..9c6e813b4c 100644 --- a/src/testing-utils.ts +++ b/src/testing-utils.ts @@ -219,11 +219,19 @@ export type ActionVarOverrides = Partial< * excluding some that are expected to be set to paths. See `setupActionsVars`. * * @param overrides Overrides for the defaults. + * @param env The environment to set the variables for. */ -export function setupBaseActionsVars(overrides?: ActionVarOverrides) { +export function setupBaseActionsVars( + overrides?: ActionVarOverrides, + env?: Env, +) { const vars = { ...DEFAULT_ACTIONS_VARS, ...overrides }; for (const [key, value] of Object.entries(vars)) { - process.env[key] = value; + if (env) { + env.set(key, value); + } else { + process.env[key] = value; + } } } diff --git a/src/util.ts b/src/util.ts index 1e79382f6e..36f0ce04b0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -572,6 +572,9 @@ export function getEnv(env: NodeJS.ProcessEnv = process.env): Env { getRequired: (name) => getRequiredEnvVar(env, name), getOptional: (name) => getOptionalEnvVarFrom(env, name), entries: () => Object.entries(env), + set: (name, value) => { + env[name] = value; + }, }; } From 31577749b3adac642fd133fc0ece0cf61e4b8216 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:08:26 +0100 Subject: [PATCH 10/18] Give `determineUserConfig` access to the environment --- lib/entry-points.js | 13 ++++++++++++- src/config-utils.test.ts | 9 +++++++++ src/config-utils.ts | 5 ++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 9e872e7d03..b36ea2a2a6 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144615,6 +144615,16 @@ function initializeEnvironment(version) { core2.exportVariable("CODEQL_ACTION_FEATURE_WILL_UPLOAD" /* FEATURE_WILL_UPLOAD */, "true"); core2.exportVariable("CODEQL_ACTION_VERSION" /* VERSION */, version); } +function getEnv(env = process.env) { + return { + getRequired: (name) => getRequiredEnvVar(env, name), + getOptional: (name) => getOptionalEnvVarFrom(env, name), + entries: () => Object.entries(env), + set: (name, value) => { + env[name] = value; + } + }; +} function getRequiredEnvVar(env, paramName) { const value = env[paramName]; if (value === void 0 || value.length === 0) { @@ -148668,7 +148678,7 @@ async function applyIncrementalAnalysisSettings(config, hasDiffRanges, codeql, l }); } } -async function determineUserConfig(logger, features, tempDir, inputs) { +async function determineUserConfig(logger, env, features, tempDir, inputs) { const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); if (inputs.configInput) { if (inputs.configFile) { @@ -148699,6 +148709,7 @@ async function initConfig(features, inputs) { const { logger, tempDir } = inputs; const userConfig = await determineUserConfig( logger, + getEnv(), features, tempDir, inputs diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index cdaad5761e..c4b3020a90 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -37,6 +37,7 @@ import { createTestConfig, makeMacro, RecordingLogger, + DEFAULT_ACTIONS_VARS, } from "./testing-utils"; import { GitHubVariant, @@ -2282,9 +2283,11 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r test("determineUserConfig - empty config when neither input is specified", async (t) => { await withTmpDir(async (tmpDir) => { const logger = new RecordingLogger(); + const env = util.getEnv(DEFAULT_ACTIONS_VARS); const result = await configUtils.determineUserConfig( logger, + env, createFeatures([]), tmpDir, createTestInitConfigInputs({ @@ -2313,9 +2316,11 @@ test("determineUserConfig - loads config file", async (t) => { await withTmpDir(async (tmpDir) => { const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir); const logger = new RecordingLogger(); + const env = util.getEnv(DEFAULT_ACTIONS_VARS); const result = await configUtils.determineUserConfig( logger, + env, createFeatures([]), tmpDir, createTestInitConfigInputs({ @@ -2346,9 +2351,11 @@ test("determineUserConfig - loads config file", async (t) => { test("determineUserConfig - loads config input", async (t) => { await withTmpDir(async (tmpDir) => { const logger = new RecordingLogger(); + const env = util.getEnv(DEFAULT_ACTIONS_VARS); const result = await configUtils.determineUserConfig( logger, + env, createFeatures([]), tmpDir, createTestInitConfigInputs({ @@ -2383,11 +2390,13 @@ test("determineUserConfig - loads config input", async (t) => { test("determineUserConfig - ignores config file input when both specified", async (t) => { await withTmpDir(async (tmpDir) => { const logger = new RecordingLogger(); + const env = util.getEnv(DEFAULT_ACTIONS_VARS); const configFilePath = createConfigFile(otherConfigFileContents, tmpDir); const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); const result = await configUtils.determineUserConfig( logger, + env, createFeatures([]), tmpDir, createTestInitConfigInputs({ diff --git a/src/config-utils.ts b/src/config-utils.ts index f9c4bb7023..64830acae1 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -32,7 +32,7 @@ import { makeTelemetryDiagnostic, } from "./diagnostics"; import { prepareDiffInformedAnalysis } from "./diff-informed-analysis-utils"; -import { EnvVar } from "./environment"; +import { Env, EnvVar } from "./environment"; import * as errorMessages from "./error-messages"; import { Feature, FeatureEnablement } from "./feature-flags"; import { @@ -77,6 +77,7 @@ import { Success, Failure, isHostedRunner, + getEnv, } from "./util"; /** @@ -1154,6 +1155,7 @@ export async function applyIncrementalAnalysisSettings( */ export async function determineUserConfig( logger: Logger, + env: Env, features: FeatureEnablement, tempDir: string, inputs: InitConfigInputs, @@ -1203,6 +1205,7 @@ export async function initConfig( const userConfig = await determineUserConfig( logger, + getEnv(), features, tempDir, inputs, From 42c0c6aedd6f25c7105837d1805fdd96529095a8 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:30:14 +0100 Subject: [PATCH 11/18] Allow merging Default Setup `config` with config file --- lib/entry-points.js | 62 ++++++++++++++-- src/config-utils.test.ts | 153 ++++++++++++++++++++++++++++++++++++++- src/config-utils.ts | 61 ++++++++++++++-- 3 files changed, 261 insertions(+), 15 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index b36ea2a2a6..643cd9732b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -147168,6 +147168,30 @@ function isKnownPropertyName(name) { } // src/config/db-config.ts +function mergeUserConfigs(logger, fromConfigInput, fromConfigFile) { + logger.debug( + "Combining configuration files from 'config' and 'config-file' inputs" + ); + const threatModels = new Set(fromConfigInput["threat-models"] || []); + for (const configFileThreatModel of fromConfigFile["threat-models"] || []) { + threatModels.add(configFileThreatModel); + } + if (fromConfigFile["default-setup"]) { + logger.warning( + `The 'default-setup' configuration key is not supported in user-supplied configuration files and will be ignored.` + ); + } + const result = { ...fromConfigFile }; + delete result["threat-models"]; + delete result["default-setup"]; + if (fromConfigInput["default-setup"]) { + result["default-setup"] = fromConfigInput["default-setup"]; + } + if (threatModels.size > 0) { + result["threat-models"] = Array.from(threatModels); + } + return result; +} function shouldCombine(inputValue) { return !!inputValue?.trim().startsWith("+"); } @@ -148681,14 +148705,40 @@ async function applyIncrementalAnalysisSettings(config, hasDiffRanges, codeql, l async function determineUserConfig(logger, env, features, tempDir, inputs) { const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); if (inputs.configInput) { - if (inputs.configFile) { - logger.warning( - `Both a config file and config input were provided. Ignoring config file.` + const computedConfigPath = userConfigFromActionPath(tempDir); + const allowMergeConfigs = features.getValue("allow_merge_config_files" /* AllowMergeConfigFiles */); + if (inputs.configFile && isDefaultSetup(env) && await allowMergeConfigs) { + const fromConfigInput = parseUserConfig( + logger, + "`config` input", + inputs.configInput, + validateConfig + ); + const fromConfigFile = await loadUserConfig( + logger, + inputs.configFile, + inputs.workspacePath, + inputs.apiDetails, + tempDir, + validateConfig + ); + fs9.writeFileSync( + computedConfigPath, + dump(mergeUserConfigs(logger, fromConfigInput, fromConfigFile)) ); + logger.debug( + `Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}` + ); + } else { + if (inputs.configFile) { + logger.warning( + `Both a config file and config input were provided. Ignoring config file.` + ); + } + fs9.writeFileSync(computedConfigPath, inputs.configInput); + logger.debug(`Using config from action input: ${computedConfigPath}`); } - inputs.configFile = userConfigFromActionPath(tempDir); - fs9.writeFileSync(inputs.configFile, inputs.configInput); - logger.debug(`Using config from action input: ${inputs.configFile}`); + inputs.configFile = computedConfigPath; } if (!inputs.configFile) { logger.debug("No configuration file was provided"); diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index c4b3020a90..7d03c622a1 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -567,8 +567,7 @@ test.serial( "Using config input and file together, config input should be used.", async (t) => { return await withTmpDir(async (tempDir) => { - process.env["RUNNER_TEMP"] = tempDir; - process.env["GITHUB_WORKSPACE"] = tempDir; + setupActionsVars(tempDir, tempDir); const configFilePath = createConfigFile( simpleConfigFileContents, @@ -2428,3 +2427,153 @@ test("determineUserConfig - ignores config file input when both specified", asyn ); }); }); + +/** A `config` input that we might get from Default Setup. */ +const defaultSetupConfigInput = ` + threat-models: [local, remote] + default-setup: + org: + model-packs: [foo, bar]`; + +test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(true); + const env = util.getEnv({ + ...DEFAULT_ACTIONS_VARS, + [actionsUtil.ActionsEnvVars.GITHUB_EVENT_NAME]: "dynamic", + }); + const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir); + const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + + const result = await configUtils.determineUserConfig( + logger, + env, + createFeatures([Feature.AllowMergeConfigFiles]), + tmpDir, + createTestInitConfigInputs({ + configInput: defaultSetupConfigInput, + configFile: configFilePath, + }), + ); + + // The loaded configuration should match the result of merging + // `defaultSetupConfigInput` and `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + "threat-models": ["local", "remote"], + "default-setup": { + org: { + "model-packs": ["foo", "bar"], + }, + }, + } satisfies UserConfig); + // And the appropriate origin messages should have been logged. + t.true( + logger.hasMessage( + `Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`, + ), + ); + t.true( + logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), + ); + t.false(logger.hasMessage("No configuration file was provided")); + t.false( + logger.hasMessage( + `Using config from action input: ${expectedConfigPath}`, + ), + ); + t.false( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); + +test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(true); + const env = util.getEnv({ + ...DEFAULT_ACTIONS_VARS, + [actionsUtil.ActionsEnvVars.GITHUB_EVENT_NAME]: "dynamic", + }); + const configFilePath = createConfigFile(otherConfigFileContents, tmpDir); + const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + + const result = await configUtils.determineUserConfig( + logger, + env, + createFeatures([]), + tmpDir, + createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: configFilePath, + }), + ); + + // The loaded configuration should match `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); + // And the path of the generated config file should have been logged. + t.true( + logger.hasMessage( + `Using config from action input: ${expectedConfigPath}`, + ), + ); + t.true( + logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), + ); + t.false(logger.hasMessage("No configuration file was provided")); + // And the warning about both inputs should have been logged. + t.true( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); + +test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => { + await withTmpDir(async (tmpDir) => { + const logger = new RecordingLogger(true); + const env = util.getEnv(DEFAULT_ACTIONS_VARS); + const configFilePath = createConfigFile(otherConfigFileContents, tmpDir); + const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + + const result = await configUtils.determineUserConfig( + logger, + env, + createFeatures([Feature.AllowMergeConfigFiles]), + tmpDir, + createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: configFilePath, + }), + ); + + // The loaded configuration should match `simpleConfigFileContents`. + t.deepEqual(result, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); + // And the path of the generated config file should have been logged. + t.true( + logger.hasMessage( + `Using config from action input: ${expectedConfigPath}`, + ), + ); + t.true( + logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), + ); + t.false(logger.hasMessage("No configuration file was provided")); + // And the warning about both inputs should have been logged. + t.true( + logger.hasMessage( + "Both a config file and config input were provided. Ignoring config file.", + ), + ); + }); +}); diff --git a/src/config-utils.ts b/src/config-utils.ts index 64830acae1..bb3850c239 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -9,6 +9,7 @@ import { getActionVersion, getOptionalInput, isAnalyzingPullRequest, + isDefaultSetup, isDynamicWorkflow, } from "./actions-util"; import { @@ -24,6 +25,7 @@ import { calculateAugmentation, ExcludeQueryFilter, generateCodeScanningConfig, + mergeUserConfigs, parseUserConfig, UserConfig, } from "./config/db-config"; @@ -1162,16 +1164,61 @@ export async function determineUserConfig( ): Promise { const validateConfig = await features.getValue(Feature.ValidateDbConfig); - // if configInput is set, it takes precedence over configFile + // We have the following cases: + // 1. A `config` or `config-file` input is provided, but not both: use the provided one. + // 2. Both are provided and we are in an advanced workflow: ignore the `config-file` input. + // 3. Both are provided and we are in Default Setup: the `config` input uses a limited + // set of options, which are supported by `mergeUserConfigs`, and we merge the two configs. if (inputs.configInput) { - if (inputs.configFile) { - logger.warning( - `Both a config file and config input were provided. Ignoring config file.`, + const computedConfigPath = userConfigFromActionPath(tempDir); + + // Get a promise which enables us to determine whether the FF that allows us to + // merge supported configuration file properties is enabled. We only execute + // the promise lazily if the other checks pass. + const allowMergeConfigs = features.getValue(Feature.AllowMergeConfigFiles); + + // Check whether we also have a `config-file` input and decide what to do. + if (inputs.configFile && isDefaultSetup(env) && (await allowMergeConfigs)) { + // If the FF is enabled and we are in Default Setup, combine the supported + // configuration file properties and write the result to disk. + const fromConfigInput = parseUserConfig( + logger, + "`config` input", + inputs.configInput, + validateConfig, + ); + const fromConfigFile = await loadUserConfig( + logger, + inputs.configFile, + inputs.workspacePath, + inputs.apiDetails, + tempDir, + validateConfig, + ); + + fs.writeFileSync( + computedConfigPath, + yaml.dump(mergeUserConfigs(logger, fromConfigInput, fromConfigFile)), + ); + logger.debug( + `Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`, ); + } else { + // If we are in this branch and there is a `config-file` input, then it means + // we didn't meet the conditions for merging the configurations. Warn the user + // that the configuration file will be ignored. + if (inputs.configFile) { + logger.warning( + `Both a config file and config input were provided. Ignoring config file.`, + ); + } + + // Write the `config` input straight to disk. + fs.writeFileSync(computedConfigPath, inputs.configInput); + logger.debug(`Using config from action input: ${computedConfigPath}`); } - inputs.configFile = userConfigFromActionPath(tempDir); - fs.writeFileSync(inputs.configFile, inputs.configInput); - logger.debug(`Using config from action input: ${inputs.configFile}`); + + inputs.configFile = computedConfigPath; } // Load whatever configuration file we have, if any. From 3707b44eaa8db748807da382d818c98cf1e31e59 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:38:54 +0100 Subject: [PATCH 12/18] Document that `inputs` might be mutated --- src/config-utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config-utils.ts b/src/config-utils.ts index bb3850c239..6b039681e9 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1152,6 +1152,9 @@ export async function applyIncrementalAnalysisSettings( /** * Determines where to load the `UserConfig` for the CLI from and loads it. * + * @param inputs The Action inputs. The `inputConfigFile` value will be mutated + * if a CodeQL Action-generated file should be used. + * * @returns The loaded `UserConfig`, which might be empty if no configuration * was specified. */ From d149f93d92f457c14ade00fa59952e3dfcb1a491 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:40:10 +0100 Subject: [PATCH 13/18] Return `mergedConfig` instead of loading it again --- lib/entry-points.js | 14 +++++++++----- src/config-utils.test.ts | 2 +- src/config-utils.ts | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 643cd9732b..c51d62a0b1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148722,13 +148722,17 @@ async function determineUserConfig(logger, env, features, tempDir, inputs) { tempDir, validateConfig ); - fs9.writeFileSync( - computedConfigPath, - dump(mergeUserConfigs(logger, fromConfigInput, fromConfigFile)) + const mergedConfig = mergeUserConfigs( + logger, + fromConfigInput, + fromConfigFile ); + fs9.writeFileSync(computedConfigPath, dump(mergedConfig)); logger.debug( `Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}` ); + inputs.configFile = computedConfigPath; + return mergedConfig; } else { if (inputs.configFile) { logger.warning( @@ -148736,9 +148740,9 @@ async function determineUserConfig(logger, env, features, tempDir, inputs) { ); } fs9.writeFileSync(computedConfigPath, inputs.configInput); - logger.debug(`Using config from action input: ${computedConfigPath}`); + inputs.configFile = computedConfigPath; + logger.debug(`Using config from action input: ${inputs.configFile}`); } - inputs.configFile = computedConfigPath; } if (!inputs.configFile) { logger.debug("No configuration file was provided"); diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 7d03c622a1..fc885d7c7b 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2474,7 +2474,7 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a `Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`, ), ); - t.true( + t.false( logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), ); t.false(logger.hasMessage("No configuration file was provided")); diff --git a/src/config-utils.ts b/src/config-utils.ts index 6b039681e9..8f71009e8d 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1199,13 +1199,20 @@ export async function determineUserConfig( validateConfig, ); - fs.writeFileSync( - computedConfigPath, - yaml.dump(mergeUserConfigs(logger, fromConfigInput, fromConfigFile)), + // Write the merged configuration to disk so that it can be loaded subsequently by + // the CLI or other CodeQL Action steps. + const mergedConfig = mergeUserConfigs( + logger, + fromConfigInput, + fromConfigFile, ); + fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig)); logger.debug( `Using merged configurations from 'config' input with configuration from '${inputs.configFile}': ${computedConfigPath}`, ); + + inputs.configFile = computedConfigPath; + return mergedConfig; } else { // If we are in this branch and there is a `config-file` input, then it means // we didn't meet the conditions for merging the configurations. Warn the user @@ -1218,10 +1225,9 @@ export async function determineUserConfig( // Write the `config` input straight to disk. fs.writeFileSync(computedConfigPath, inputs.configInput); - logger.debug(`Using config from action input: ${computedConfigPath}`); + inputs.configFile = computedConfigPath; + logger.debug(`Using config from action input: ${inputs.configFile}`); } - - inputs.configFile = computedConfigPath; } // Load whatever configuration file we have, if any. From 764f470b7435119cf5eabc73e07d4c533d4987b1 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 13:43:59 +0100 Subject: [PATCH 14/18] Test `inputs.configFile` mutation in tests --- src/config-utils.test.ts | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index fc885d7c7b..fbbf1de761 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2317,15 +2317,16 @@ test("determineUserConfig - loads config file", async (t) => { const logger = new RecordingLogger(); const env = util.getEnv(DEFAULT_ACTIONS_VARS); + const inputs = createTestInitConfigInputs({ + configInput: undefined, + configFile: configFilePath, + }); const result = await configUtils.determineUserConfig( logger, env, createFeatures([]), tmpDir, - createTestInitConfigInputs({ - configInput: undefined, - configFile: configFilePath, - }), + inputs, ); // The loaded configuration should match `simpleConfigFileContents`. @@ -2333,6 +2334,8 @@ test("determineUserConfig - loads config file", async (t) => { name: "my config", queries: [{ uses: "./foo_file" }], }); + // The `configFile` input should not have changed. + t.is(inputs.configFile, configFilePath); // And the path of the input config file should have been logged, while the // other two origin messages should not have been logged. t.true(logger.hasMessage(`Using configuration file: ${configFilePath}`)); @@ -2351,16 +2354,18 @@ test("determineUserConfig - loads config input", async (t) => { await withTmpDir(async (tmpDir) => { const logger = new RecordingLogger(); const env = util.getEnv(DEFAULT_ACTIONS_VARS); + const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + const inputs = createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: undefined, + }); const result = await configUtils.determineUserConfig( logger, env, createFeatures([]), tmpDir, - createTestInitConfigInputs({ - configInput: simpleConfigFileContents, - configFile: undefined, - }), + inputs, ); // The loaded configuration should match `simpleConfigFileContents`. @@ -2368,13 +2373,13 @@ test("determineUserConfig - loads config input", async (t) => { name: "my config", queries: [{ uses: "./foo_file" }], }); + // The `configFile` input should have been mutated to the generated path. + t.is(inputs.configFile, expectedConfigPath); // And the input source and path of the generated config file should have been logged, // while the message about no configuration input should not have been logged. t.true(logger.hasMessage("Using config from action input:")); t.true( - logger.hasMessage( - `Using configuration file: ${configUtils.userConfigFromActionPath(tmpDir)}`, - ), + logger.hasMessage(`Using configuration file: ${expectedConfigPath}`), ); t.false(logger.hasMessage("No configuration file was provided")); // But the warning about both inputs should not have been logged. @@ -2393,15 +2398,16 @@ test("determineUserConfig - ignores config file input when both specified", asyn const configFilePath = createConfigFile(otherConfigFileContents, tmpDir); const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + const inputs = createTestInitConfigInputs({ + configInput: simpleConfigFileContents, + configFile: configFilePath, + }); const result = await configUtils.determineUserConfig( logger, env, createFeatures([]), tmpDir, - createTestInitConfigInputs({ - configInput: simpleConfigFileContents, - configFile: configFilePath, - }), + inputs, ); // The loaded configuration should match `simpleConfigFileContents`. @@ -2409,6 +2415,8 @@ test("determineUserConfig - ignores config file input when both specified", asyn name: "my config", queries: [{ uses: "./foo_file" }], }); + // The `configFile` input should have been mutated to the generated path. + t.is(inputs.configFile, expectedConfigPath); // And the path of the generated config file should have been logged. t.true( logger.hasMessage( From 9a06fa6b38118b9ffd97b60f9aff0095f01b2521 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 14:10:30 +0100 Subject: [PATCH 15/18] Set the required `workspacePath` input --- src/config-utils.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index fbbf1de761..f6eaa155af 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2292,6 +2292,7 @@ test("determineUserConfig - empty config when neither input is specified", async createTestInitConfigInputs({ configInput: undefined, configFile: undefined, + workspacePath: tmpDir, }), ); @@ -2320,6 +2321,7 @@ test("determineUserConfig - loads config file", async (t) => { const inputs = createTestInitConfigInputs({ configInput: undefined, configFile: configFilePath, + workspacePath: tmpDir, }); const result = await configUtils.determineUserConfig( logger, @@ -2359,6 +2361,7 @@ test("determineUserConfig - loads config input", async (t) => { const inputs = createTestInitConfigInputs({ configInput: simpleConfigFileContents, configFile: undefined, + workspacePath: tmpDir, }); const result = await configUtils.determineUserConfig( logger, @@ -2401,6 +2404,7 @@ test("determineUserConfig - ignores config file input when both specified", asyn const inputs = createTestInitConfigInputs({ configInput: simpleConfigFileContents, configFile: configFilePath, + workspacePath: tmpDir, }); const result = await configUtils.determineUserConfig( logger, @@ -2461,6 +2465,7 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a createTestInitConfigInputs({ configInput: defaultSetupConfigInput, configFile: configFilePath, + workspacePath: tmpDir, }), ); @@ -2517,6 +2522,7 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is createTestInitConfigInputs({ configInput: simpleConfigFileContents, configFile: configFilePath, + workspacePath: tmpDir, }), ); @@ -2559,6 +2565,7 @@ test("determineUserConfig - ignores config file input outside Default Setup if F createTestInitConfigInputs({ configInput: simpleConfigFileContents, configFile: configFilePath, + workspacePath: tmpDir, }), ); From 44d6b3b016bc23976b6f231b4f49ae03755e6585 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 14:41:55 +0100 Subject: [PATCH 16/18] Initialise `env` in `actions-util` when not provided This makes the implementations more consistent for now, and less error-prone --- lib/entry-points.js | 55 ++++++++++++++++----------------- src/actions-util.ts | 75 ++++++++++++++++++--------------------------- 2 files changed, 56 insertions(+), 74 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index c51d62a0b1..4cc50722b8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144994,34 +144994,31 @@ var getOptionalInput = function(name) { const value = core3.getInput(name); return value.length > 0 ? value : void 0; }; -function getTemporaryDirectory(env) { - const value = env?.getOptional("CODEQL_ACTION_TEMP" /* TEMP */) || process.env["CODEQL_ACTION_TEMP" /* TEMP */]; - return value !== void 0 && value !== "" ? value : env?.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */) || getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */); +function getTemporaryDirectory(env = getEnv()) { + const value = env.getOptional("CODEQL_ACTION_TEMP" /* TEMP */); + return value !== void 0 && value !== "" ? value : env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); } var PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -function getDiffRangesJsonFilePath(env) { +function getDiffRangesJsonFilePath(env = getEnv()) { return path2.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } function getActionVersion() { return "4.36.4"; } -function getWorkflowEventName(env) { - if (env) { - return env.getRequired("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); - } - return getRequiredEnvParam("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); +function getWorkflowEventName(env = getEnv()) { + return env.getRequired("GITHUB_EVENT_NAME" /* GITHUB_EVENT_NAME */); } -function isRunningLocalAction(env) { +function isRunningLocalAction(env = getEnv()) { const relativeScriptPath = getRelativeScriptPath(env); return relativeScriptPath.startsWith("..") || path2.isAbsolute(relativeScriptPath); } function getRelativeScriptPath(env) { - const runnerTemp = env === void 0 ? getRequiredEnvParam("RUNNER_TEMP" /* RUNNER_TEMP */) : env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); + const runnerTemp = env.getRequired("RUNNER_TEMP" /* RUNNER_TEMP */); const actionsDirectory = path2.join(path2.dirname(runnerTemp), "_actions"); return path2.relative(actionsDirectory, __filename); } -function getWorkflowEvent(env) { - const eventJsonFile = env === void 0 ? getRequiredEnvParam("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */) : env.getRequired("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); +function getWorkflowEvent(env = getEnv()) { + const eventJsonFile = env.getRequired("GITHUB_EVENT_PATH" /* GITHUB_EVENT_PATH */); try { return JSON.parse(fs2.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -145077,8 +145074,8 @@ function getUploadValue(input) { return "always"; } } -function getWorkflowRunID(env) { - const workflowRunIdString = env === void 0 ? getRequiredEnvParam("GITHUB_RUN_ID" /* GITHUB_RUN_ID */) : env.getRequired("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); +function getWorkflowRunID(env = getEnv()) { + const workflowRunIdString = env.getRequired("GITHUB_RUN_ID" /* GITHUB_RUN_ID */); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -145092,8 +145089,10 @@ function getWorkflowRunID(env) { } return workflowRunID; } -function getWorkflowRunAttempt(env) { - const workflowRunAttemptString = env === void 0 ? getRequiredEnvParam("GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */) : env.getRequired("GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */); +function getWorkflowRunAttempt(env = getEnv()) { + const workflowRunAttemptString = env.getRequired( + "GITHUB_RUN_ATTEMPT" /* GITHUB_RUN_ATTEMPT */ + ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( @@ -145144,13 +145143,13 @@ var getFileType = async (filePath) => { throw e; } }; -function isSelfHostedRunner() { - return process.env.RUNNER_ENVIRONMENT === "self-hosted"; +function isSelfHostedRunner(env = getEnv()) { + return env.getOptional("RUNNER_ENVIRONMENT") === "self-hosted"; } -function isDynamicWorkflow(env) { +function isDynamicWorkflow(env = getEnv()) { return getWorkflowEventName(env) === "dynamic"; } -function isDefaultSetup(env) { +function isDefaultSetup(env = getEnv()) { return isDynamicWorkflow(env); } function prettyPrintInvocation(cmd, args) { @@ -145215,8 +145214,8 @@ async function runTool(cmd, args = [], opts = {}) { return stdout; } var persistedInputsKey = "persisted_inputs"; -var persistInputs = function(env) { - const entries = env?.entries() || Object.entries(process.env); +var persistInputs = function(env = getEnv()) { + const entries = env.entries(); const inputEnvironmentVariables = entries.filter( ([name]) => name.startsWith("INPUT_") ); @@ -145230,7 +145229,7 @@ var restoreInputs = function() { } } }; -function getPullRequestBranches(env) { +function getPullRequestBranches(env = getEnv()) { const pullRequest = github.context.payload.pull_request; if (pullRequest) { return { @@ -145241,8 +145240,8 @@ function getPullRequestBranches(env) { head: pullRequest.head.label }; } - const codeScanningRef = env?.getOptional("CODE_SCANNING_REF") || process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = env?.getOptional("CODE_SCANNING_BASE_BRANCH") || process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = env.getOptional("CODE_SCANNING_REF"); + const codeScanningBaseBranch = env.getOptional("CODE_SCANNING_BASE_BRANCH"); if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -145253,7 +145252,7 @@ function getPullRequestBranches(env) { } return void 0; } -function isAnalyzingPullRequest(env) { +function isAnalyzingPullRequest(env = getEnv()) { return getPullRequestBranches(env) !== void 0; } var qualityCategoryMapping = { @@ -145266,7 +145265,7 @@ var qualityCategoryMapping = { typescript: "javascript-typescript", kotlin: "java-kotlin" }; -function fixCodeQualityCategory(logger, category, env) { +function fixCodeQualityCategory(logger, category, env = getEnv()) { if (category !== void 0 && isDefaultSetup(env) && category.startsWith("/language:")) { const language = category.substring("/language:".length); const mappedLanguage = qualityCategoryMapping[language]; diff --git a/src/actions-util.ts b/src/actions-util.ts index 5d7e3d91ef..251e804e70 100644 --- a/src/actions-util.ts +++ b/src/actions-util.ts @@ -12,8 +12,8 @@ import { Logger } from "./logging"; import { doesDirectoryExist, getCodeQLDatabasePath, - getRequiredEnvParam, ConfigurationError, + getEnv, } from "./util"; /** @@ -89,17 +89,16 @@ export const getOptionalInput = function (name: string): string | undefined { * directory that has been set in `CODEQL_ACTION_TEMP` by e.g. a previous step, or the * value of `RUNNER_TEMP` otherwise. */ -export function getTemporaryDirectory(env?: Env): string { - const value = env?.getOptional(EnvVar.TEMP) || process.env[EnvVar.TEMP]; +export function getTemporaryDirectory(env: Env = getEnv()): string { + const value = env.getOptional(EnvVar.TEMP); return value !== undefined && value !== "" ? value - : env?.getRequired(ActionsEnvVars.RUNNER_TEMP) || - getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP); + : env.getRequired(ActionsEnvVars.RUNNER_TEMP); } const PR_DIFF_RANGE_JSON_FILENAME = "pr-diff-range.json"; -export function getDiffRangesJsonFilePath(env?: Env): string { +export function getDiffRangesJsonFilePath(env: Env = getEnv()): string { return path.join(getTemporaryDirectory(env), PR_DIFF_RANGE_JSON_FILENAME); } @@ -112,18 +111,15 @@ export function getActionVersion(): string { * * This will be "dynamic" for default setup workflow runs. */ -export function getWorkflowEventName(env?: Env) { - if (env) { - return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME); - } - return getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_NAME); +export function getWorkflowEventName(env: Env = getEnv()) { + return env.getRequired(ActionsEnvVars.GITHUB_EVENT_NAME); } /** * Returns whether the current workflow is executing a local copy of the Action, e.g. we're running * a workflow on the codeql-action repo itself. */ -export function isRunningLocalAction(env?: Env): boolean { +export function isRunningLocalAction(env: Env = getEnv()): boolean { const relativeScriptPath = getRelativeScriptPath(env); return ( relativeScriptPath.startsWith("..") || path.isAbsolute(relativeScriptPath) @@ -135,21 +131,15 @@ export function isRunningLocalAction(env?: Env): boolean { * * This can be used to get the Action's name or tell if we're running a local Action. */ -function getRelativeScriptPath(env?: Env): string { - const runnerTemp = - env === undefined - ? getRequiredEnvParam(ActionsEnvVars.RUNNER_TEMP) - : env.getRequired(ActionsEnvVars.RUNNER_TEMP); +function getRelativeScriptPath(env: Env): string { + const runnerTemp = env.getRequired(ActionsEnvVars.RUNNER_TEMP); const actionsDirectory = path.join(path.dirname(runnerTemp), "_actions"); return path.relative(actionsDirectory, __filename); } /** Returns the contents of `GITHUB_EVENT_PATH` as a JSON object. */ -export function getWorkflowEvent(env?: Env): any { - const eventJsonFile = - env === undefined - ? getRequiredEnvParam(ActionsEnvVars.GITHUB_EVENT_PATH) - : env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH); +export function getWorkflowEvent(env: Env = getEnv()): any { + const eventJsonFile = env.getRequired(ActionsEnvVars.GITHUB_EVENT_PATH); try { return JSON.parse(fs.readFileSync(eventJsonFile, "utf-8")); } catch (e) { @@ -218,11 +208,8 @@ export function getUploadValue(input: string | undefined): UploadKind { /** * Get the workflow run ID. */ -export function getWorkflowRunID(env?: Env): number { - const workflowRunIdString = - env === undefined - ? getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ID) - : env.getRequired(ActionsEnvVars.GITHUB_RUN_ID); +export function getWorkflowRunID(env: Env = getEnv()): number { + const workflowRunIdString = env.getRequired(ActionsEnvVars.GITHUB_RUN_ID); const workflowRunID = parseInt(workflowRunIdString, 10); if (Number.isNaN(workflowRunID)) { throw new Error( @@ -240,11 +227,10 @@ export function getWorkflowRunID(env?: Env): number { /** * Get the workflow run attempt number. */ -export function getWorkflowRunAttempt(env?: Env): number { - const workflowRunAttemptString = - env === undefined - ? getRequiredEnvParam(ActionsEnvVars.GITHUB_RUN_ATTEMPT) - : env.getRequired(ActionsEnvVars.GITHUB_RUN_ATTEMPT); +export function getWorkflowRunAttempt(env: Env = getEnv()): number { + const workflowRunAttemptString = env.getRequired( + ActionsEnvVars.GITHUB_RUN_ATTEMPT, + ); const workflowRunAttempt = parseInt(workflowRunAttemptString, 10); if (Number.isNaN(workflowRunAttempt)) { throw new Error( @@ -310,17 +296,17 @@ export const getFileType = async (filePath: string): Promise => { } }; -export function isSelfHostedRunner() { - return process.env.RUNNER_ENVIRONMENT === "self-hosted"; +export function isSelfHostedRunner(env: Env = getEnv()) { + return env.getOptional("RUNNER_ENVIRONMENT") === "self-hosted"; } /** Determines whether the workflow trigger is `dynamic`. */ -export function isDynamicWorkflow(env?: Env): boolean { +export function isDynamicWorkflow(env: Env = getEnv()): boolean { return getWorkflowEventName(env) === "dynamic"; } /** Determines whether we are running in default setup. */ -export function isDefaultSetup(env?: Env): boolean { +export function isDefaultSetup(env: Env = getEnv()): boolean { return isDynamicWorkflow(env); } @@ -419,8 +405,8 @@ const persistedInputsKey = "persisted_inputs"; * This would be simplified if actions/runner#3514 is addressed. * https://github.com/actions/runner/issues/3514 */ -export const persistInputs = function (env?: Env) { - const entries = env?.entries() || Object.entries(process.env); +export const persistInputs = function (env: Env = getEnv()) { + const entries = env.entries(); const inputEnvironmentVariables = entries.filter(([name]) => name.startsWith("INPUT_"), ); @@ -451,7 +437,7 @@ export interface PullRequestBranches { * we are not analyzing a pull request. */ export function getPullRequestBranches( - env?: Env, + env: Env = getEnv(), ): PullRequestBranches | undefined { const pullRequest = github.context.payload.pull_request; if (pullRequest) { @@ -466,11 +452,8 @@ export function getPullRequestBranches( // PR analysis under Default Setup does not have the pull_request context, // but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH. - const codeScanningRef = - env?.getOptional("CODE_SCANNING_REF") || process.env.CODE_SCANNING_REF; - const codeScanningBaseBranch = - env?.getOptional("CODE_SCANNING_BASE_BRANCH") || - process.env.CODE_SCANNING_BASE_BRANCH; + const codeScanningRef = env.getOptional("CODE_SCANNING_REF"); + const codeScanningBaseBranch = env.getOptional("CODE_SCANNING_BASE_BRANCH"); if (codeScanningRef && codeScanningBaseBranch) { return { base: codeScanningBaseBranch, @@ -485,7 +468,7 @@ export function getPullRequestBranches( /** * Returns whether we are analyzing a pull request. */ -export function isAnalyzingPullRequest(env?: Env): boolean { +export function isAnalyzingPullRequest(env: Env = getEnv()): boolean { return getPullRequestBranches(env) !== undefined; } @@ -510,7 +493,7 @@ const qualityCategoryMapping: Record = { export function fixCodeQualityCategory( logger: Logger, category?: string, - env?: Env, + env: Env = getEnv(), ): string | undefined { // The `category` should always be set by Default Setup. We perform this check // to avoid potential issues if Code Quality supports Advanced Setup in the future From 4509fb3c4baeb63842f1dc57312d28cc73a624ce Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 14:43:49 +0100 Subject: [PATCH 17/18] Fix JSDoc for `determineUserConfig` --- src/config-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config-utils.ts b/src/config-utils.ts index 8f71009e8d..1a79c38c1d 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1152,7 +1152,7 @@ export async function applyIncrementalAnalysisSettings( /** * Determines where to load the `UserConfig` for the CLI from and loads it. * - * @param inputs The Action inputs. The `inputConfigFile` value will be mutated + * @param inputs The Action inputs. The `configFile` value will be mutated * if a CodeQL Action-generated file should be used. * * @returns The loaded `UserConfig`, which might be empty if no configuration From 8476401b09c6ee83a4921c8364cefa0ec83f626d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 6 Jul 2026 14:45:41 +0100 Subject: [PATCH 18/18] Make lazy FF check less ambiguous --- lib/entry-points.js | 4 ++-- src/config-utils.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 4cc50722b8..fc806bf78e 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148705,8 +148705,8 @@ async function determineUserConfig(logger, env, features, tempDir, inputs) { const validateConfig = await features.getValue("validate_db_config" /* ValidateDbConfig */); if (inputs.configInput) { const computedConfigPath = userConfigFromActionPath(tempDir); - const allowMergeConfigs = features.getValue("allow_merge_config_files" /* AllowMergeConfigFiles */); - if (inputs.configFile && isDefaultSetup(env) && await allowMergeConfigs) { + const allowMergeConfigs = () => features.getValue("allow_merge_config_files" /* AllowMergeConfigFiles */); + if (inputs.configFile && isDefaultSetup(env) && await allowMergeConfigs()) { const fromConfigInput = parseUserConfig( logger, "`config` input", diff --git a/src/config-utils.ts b/src/config-utils.ts index 1a79c38c1d..9d7d89a579 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1175,13 +1175,18 @@ export async function determineUserConfig( if (inputs.configInput) { const computedConfigPath = userConfigFromActionPath(tempDir); - // Get a promise which enables us to determine whether the FF that allows us to + // Get a function which enables us to determine whether the FF that allows us to // merge supported configuration file properties is enabled. We only execute - // the promise lazily if the other checks pass. - const allowMergeConfigs = features.getValue(Feature.AllowMergeConfigFiles); + // this lazily if the other checks pass. + const allowMergeConfigs = () => + features.getValue(Feature.AllowMergeConfigFiles); // Check whether we also have a `config-file` input and decide what to do. - if (inputs.configFile && isDefaultSetup(env) && (await allowMergeConfigs)) { + if ( + inputs.configFile && + isDefaultSetup(env) && + (await allowMergeConfigs()) + ) { // If the FF is enabled and we are in Default Setup, combine the supported // configuration file properties and write the result to disk. const fromConfigInput = parseUserConfig(