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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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/35] 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( From 06898d7b8bf8f14f2ddfe70880553771f27d27e5 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Wed, 8 Jul 2026 17:46:02 +0100 Subject: [PATCH 19/35] Rename `mergeUserConfigs` --- lib/entry-points.js | 4 ++-- src/config-utils.ts | 7 ++++--- src/config/db-config.test.ts | 24 ++++++++++++++++-------- src/config/db-config.ts | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 2790198919..2ee1726a9d 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -147701,7 +147701,7 @@ function isKnownPropertyName(name) { } // src/config/db-config.ts -function mergeUserConfigs(logger, fromConfigInput, fromConfigFile) { +function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile) { logger.debug( "Combining configuration files from 'config' and 'config-file' inputs" ); @@ -149338,7 +149338,7 @@ async function determineUserConfig(action, tempDir, inputs) { inputs.apiDetails, tempDir ); - const mergedConfig = mergeUserConfigs( + const mergedConfig = mergeDefaultSetupAndUserConfigs( action.logger, fromConfigInput, fromConfigFile diff --git a/src/config-utils.ts b/src/config-utils.ts index 085881c957..2eb01da013 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -27,7 +27,7 @@ import { calculateAugmentation, ExcludeQueryFilter, generateCodeScanningConfig, - mergeUserConfigs, + mergeDefaultSetupAndUserConfigs, parseUserConfig, UserConfig, } from "./config/db-config"; @@ -1036,7 +1036,8 @@ export async function determineUserConfig( // 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. + // set of options, which are supported by `mergeDefaultSetupAndUserConfigs`, + // and we merge the two configs. if (inputs.configInput) { const computedConfigPath = userConfigFromActionPath(tempDir); @@ -1070,7 +1071,7 @@ export async function determineUserConfig( // Write the merged configuration to disk so that it can be loaded subsequently by // the CLI or other CodeQL Action steps. - const mergedConfig = mergeUserConfigs( + const mergedConfig = mergeDefaultSetupAndUserConfigs( action.logger, fromConfigInput, fromConfigFile, diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 4d523ad0a9..5783e3a241 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -490,9 +490,9 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail, ); }); -test("mergeUserConfigs - combines threat models", async (t) => { +test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => { const logger = new RecordingLogger(); - const result = dbConfig.mergeUserConfigs( + const result = dbConfig.mergeDefaultSetupAndUserConfigs( logger, { "threat-models": ["a", "b"] }, { "threat-models": ["local", "remote"] }, @@ -505,9 +505,13 @@ test("mergeUserConfigs - combines threat models", async (t) => { } }); -test("mergeUserConfigs - warns if user-supplied config contains default setup key", async (t) => { +test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => { const logger = new RecordingLogger(); - const result = dbConfig.mergeUserConfigs(logger, {}, { "default-setup": {} }); + const result = dbConfig.mergeDefaultSetupAndUserConfigs( + logger, + {}, + { "default-setup": {} }, + ); // User-supplied value is ignored. t.deepEqual(result, {}); @@ -520,12 +524,12 @@ test("mergeUserConfigs - warns if user-supplied config contains default setup ke ); }); -test("mergeUserConfigs - keeps default setup key from 'config' input", async (t) => { +test("mergeDefaultSetupAndUserConfigs - 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( + const result = dbConfig.mergeDefaultSetupAndUserConfigs( logger, { "default-setup": expected }, {}, @@ -542,14 +546,18 @@ test("mergeUserConfigs - keeps default setup key from 'config' input", async (t) ); }); -test("mergeUserConfigs - keeps other properties from user-supplied configuration", async (t) => { +test("mergeDefaultSetupAndUserConfigs - 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); + const result = dbConfig.mergeDefaultSetupAndUserConfigs( + logger, + {}, + configFile, + ); t.deepEqual(result, configFile); }); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index c24994476f..612a26a24b 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -75,7 +75,7 @@ export interface UserConfig { * @param fromConfigFile The user-supplied configuration. * @returns The combination of both configuration files. */ -export function mergeUserConfigs( +export function mergeDefaultSetupAndUserConfigs( logger: Logger, fromConfigInput: UserConfig, fromConfigFile: UserConfig, From 6829d6ce38022aeb8932ec73c02d9d861cdc9401 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:02:58 +0100 Subject: [PATCH 20/35] Remove obsolete JSDoc parameter for `loadUserConfig` --- src/config-utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config-utils.ts b/src/config-utils.ts index 93289547bd..ae3a623708 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -476,7 +476,6 @@ async function downloadCacheWithTime( * @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( From 697394646540aa91131090d8d2692ba92728ce20 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:09:14 +0100 Subject: [PATCH 21/35] Check file on disk and mutated inputs --- src/config-utils.test.ts | 30 +++++++++++++++++++++++------- src/config-utils.ts | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 17c1f68ff2..b7251fa720 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2439,6 +2439,11 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir); const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); + const inputs = createTestInitConfigInputs({ + configInput: defaultSetupConfigInput, + configFile: configFilePath, + workspacePath: tmpDir, + }); const result = await configUtils.determineUserConfig( initAllState({ logger, @@ -2446,16 +2451,12 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a features: createFeatures([Feature.AllowMergeConfigFiles]), }), tmpDir, - createTestInitConfigInputs({ - configInput: defaultSetupConfigInput, - configFile: configFilePath, - workspacePath: tmpDir, - }), + inputs, ); // The loaded configuration should match the result of merging // `defaultSetupConfigInput` and `simpleConfigFileContents`. - t.deepEqual(result, { + const expectedConfig = { name: "my config", queries: [{ uses: "./foo_file" }], "threat-models": ["local", "remote"], @@ -2464,7 +2465,22 @@ test("determineUserConfig - merges configs if FF is enabled in Default Setup", a "model-packs": ["foo", "bar"], }, }, - } satisfies UserConfig); + } satisfies UserConfig; + t.deepEqual(result, expectedConfig); + + // The `configFile` input should have been mutated to the generated path. + t.is(inputs.configFile, expectedConfigPath); + + // Since `result` is the result of merging the configurations in-memory, + // also check whether loading the configuration from disk that was written + // by `determineUserConfig` matches our expectations. + const loadedFromDisk = configUtils.getLocalConfig( + logger, + expectedConfigPath, + false, + ); + t.deepEqual(loadedFromDisk, expectedConfig); + // And the appropriate origin messages should have been logged. t.true( logger.hasMessage( diff --git a/src/config-utils.ts b/src/config-utils.ts index ae3a623708..b48d5c1d7c 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -1286,7 +1286,7 @@ function isLocal(configPath: string): boolean { return configPath.indexOf("@") === -1; } -function getLocalConfig( +export function getLocalConfig( logger: Logger, configFile: string, validateConfig: boolean, From dc2d916874262ddca7df395a7cc72b9ba424d108 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:27:42 +0100 Subject: [PATCH 22/35] Add `checkSchema` function --- lib/entry-points.js | 30 +++++++++++++++++++--- src/json/index.test.ts | 10 ++++++++ src/json/index.ts | 56 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index e9d524358a..628f26fe59 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144518,19 +144518,41 @@ function optionalOrNull(validator) { }; } function validateSchema(schema, obj) { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} +function checkSchema(schema, obj, options = {}, path29 = "") { + const result = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; if (validator.required && !hasKey) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (validator.required && (obj[key] === void 0 || obj[key] === null)) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } + inputKeys.delete(key); } - return true; + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path29}${remainingKey}`); + } + return result; } // src/util.ts diff --git a/src/json/index.test.ts b/src/json/index.test.ts index eb259c757e..5234872e5a 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -67,3 +67,13 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "" })); t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" })); }); + +test("validateSchema - checkSchema reports unknown keys", async (t) => { + const result = json.checkSchema(testSchema, { + requiredKey: "foo", + extraKey: "bar", + }); + + t.true(result.valid); + t.deepEqual(result.unknownKeys, ["extraKey"]); +}); diff --git a/src/json/index.ts b/src/json/index.ts index a55de5b589..d0a8edb519 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -121,24 +121,72 @@ export function validateSchema( schema: S, obj: UnvalidatedObject, ): obj is FromSchema { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} + +export interface CheckSchemaOptions { + /** Whether to stop validation after the first error. */ + failFast?: boolean; +} + +export interface CheckSchemaResult { + /** Whether the `obj` satisfies the schema. */ + valid: boolean; + /** Unknown keys that were found during validation. */ + unknownKeys: string[]; +} + +export function checkSchema( + schema: S, + obj: UnvalidatedObject, + options: CheckSchemaOptions = {}, + path: string = "", +): CheckSchemaResult { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); + for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; // If the property is required, but absent, fail. if (validator.required && !hasKey) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is required, but undefined or null, fail. if (validator.required && (obj[key] === undefined || obj[key] === null)) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is present, validate it. if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } + + // If we reach this point, the key has been successfully validated. + inputKeys.delete(key); + } + + // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path}${remainingKey}`); } - return true; + return result; } From 1f91ec8f3c4a842fbb127d8a37f1c55873c2a8f2 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:36:50 +0100 Subject: [PATCH 23/35] Add an `array` `Validator` --- src/json/index.test.ts | 17 +++++++++++++++++ src/json/index.ts | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 5234872e5a..a238d80716 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -68,6 +68,23 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" })); }); +const arraySchema = { + arrayKey: json.array(json.number), +}; + +test("validateSchema - validates arrays", async (t) => { + // Arrays of numeric elements are accepted. + t.true(json.validateSchema(arraySchema, { arrayKey: [] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8] })); + t.true(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15] })); + + // Other array elements are not accepted. + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, "bar"] })); + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, undefined] })); + t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] })); +}); + test("validateSchema - checkSchema reports unknown keys", async (t) => { const result = json.checkSchema(testSchema, { requiredKey: "foo", diff --git a/src/json/index.ts b/src/json/index.ts index d0a8edb519..e9fbbd8a71 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -66,6 +66,16 @@ export const number = { required: true, } as const satisfies Validator; +/** A validator for arrays. */ +export function array(validator: Validator) { + return { + validate: (val: unknown) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }, + required: true, + } as const satisfies Validator; +} + /** * Transforms a validator to be optional, accepting `undefined` or `null` for an * absent value. From 98dbd66eaecef1e3ec97ec06cf7bbdf53fafc4ae Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Thu, 9 Jul 2026 23:42:28 +0100 Subject: [PATCH 24/35] Add an `object` `Validator` --- src/json/index.test.ts | 18 ++++++++++++++++++ src/json/index.ts | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/json/index.test.ts b/src/json/index.test.ts index a238d80716..71f8000031 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -85,6 +85,24 @@ test("validateSchema - validates arrays", async (t) => { t.false(json.validateSchema(arraySchema, { arrayKey: [4, 8, 15, null] })); }); +const objectSchema = { + objectKey: json.object(arraySchema), +}; + +test("validateSchema - validates objects", async (t) => { + // Objects of the given schema are accepted. + t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [] } })); + t.true(json.validateSchema(objectSchema, { objectKey: { arrayKey: [4] } })); + + // Other values are not accepted. + t.false(json.validateSchema(objectSchema, {})); + t.false(json.validateSchema(objectSchema, { objectKey: [] })); + t.false(json.validateSchema(objectSchema, { objectKey: undefined })); + t.false(json.validateSchema(objectSchema, { objectKey: null })); + t.false(json.validateSchema(objectSchema, { objectKey: "foo" })); + t.false(json.validateSchema(objectSchema, { objectKey: 123 })); +}); + test("validateSchema - checkSchema reports unknown keys", async (t) => { const result = json.checkSchema(testSchema, { requiredKey: "foo", diff --git a/src/json/index.ts b/src/json/index.ts index e9fbbd8a71..083bd49bbe 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -76,6 +76,16 @@ export function array(validator: Validator) { } as const satisfies Validator; } +/** A validator for objects. */ +export function object(schema: Schema) { + return { + validate: (val: unknown) => { + return isObject(val) && validateSchema(schema, val); + }, + required: true, + } as const satisfies Validator>; +} + /** * Transforms a validator to be optional, accepting `undefined` or `null` for an * absent value. From 9573f056544e84ef235d61129fcee001ddea292d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:01:13 +0100 Subject: [PATCH 25/35] Improve type inference for `object` and `validateSchema` --- src/json/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/json/index.ts b/src/json/index.ts index 083bd49bbe..325689d33e 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -77,13 +77,16 @@ export function array(validator: Validator) { } /** A validator for objects. */ -export function object(schema: Schema) { +export function object< + S extends Schema, + T extends UnvalidatedObject = FromSchema, +>(schema: S) { return { validate: (val: unknown) => { - return isObject(val) && validateSchema(schema, val); + return isObject(val) && validateSchema(schema, val); }, required: true, - } as const satisfies Validator>; + } as const satisfies Validator; } /** @@ -137,10 +140,10 @@ export type FromSchema = { * @param obj The object to validate. * @returns Asserts that `obj` is of the `schema`'s type if validation is successful. */ -export function validateSchema( - schema: S, - obj: UnvalidatedObject, -): obj is FromSchema { +export function validateSchema< + S extends Schema, + T extends UnvalidatedObject = FromSchema, +>(schema: S, obj: UnvalidatedObject): obj is T { const result = checkSchema(schema, obj, { failFast: true }); return result.valid; } From 847e7af6993501e27e0c15c472d12100a6ec2f8a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:11:17 +0100 Subject: [PATCH 26/35] Log warning for unrecognised keys in Default Setup config --- lib/entry-points.js | 650 +++++++++++++++++++---------------- src/config/db-config.test.ts | 21 ++ src/config/db-config.ts | 42 ++- 3 files changed, 405 insertions(+), 308 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 628f26fe59..61da930264 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -1301,16 +1301,16 @@ var require_util = __commonJS({ function isStream2(obj) { return obj && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.on === "function"; } - function isBlobLike(object) { - if (object === null) { + function isBlobLike(object2) { + if (object2 === null) { return false; - } else if (object instanceof Blob2) { + } else if (object2 instanceof Blob2) { return true; - } else if (typeof object !== "object") { + } else if (typeof object2 !== "object") { return false; } else { - const sTag = object[Symbol.toStringTag]; - return (sTag === "Blob" || sTag === "File") && ("stream" in object && typeof object.stream === "function" || "arrayBuffer" in object && typeof object.arrayBuffer === "function"); + const sTag = object2[Symbol.toStringTag]; + return (sTag === "Blob" || sTag === "File") && ("stream" in object2 && typeof object2.stream === "function" || "arrayBuffer" in object2 && typeof object2.arrayBuffer === "function"); } } function buildURL(url2, queryParams) { @@ -1592,8 +1592,8 @@ var require_util = __commonJS({ } ); } - function isFormDataLike(object) { - return object && typeof object === "object" && typeof object.append === "function" && typeof object.delete === "function" && typeof object.get === "function" && typeof object.getAll === "function" && typeof object.has === "function" && typeof object.set === "function" && object[Symbol.toStringTag] === "FormData"; + function isFormDataLike(object2) { + return object2 && typeof object2 === "object" && typeof object2.append === "function" && typeof object2.delete === "function" && typeof object2.get === "function" && typeof object2.getAll === "function" && typeof object2.has === "function" && typeof object2.set === "function" && object2[Symbol.toStringTag] === "FormData"; } function addAbortListener(signal, listener) { if ("addEventListener" in signal) { @@ -4355,8 +4355,8 @@ var require_util2 = __commonJS({ } return "allowed"; } - function isErrorLike(object) { - return object instanceof Error || (object?.constructor?.name === "Error" || object?.constructor?.name === "DOMException"); + function isErrorLike(object2) { + return object2 instanceof Error || (object2?.constructor?.name === "Error" || object2?.constructor?.name === "DOMException"); } function isValidReasonPhrase(statusText) { for (let i = 0; i < statusText.length; ++i) { @@ -4781,7 +4781,7 @@ var require_util2 = __commonJS({ return new FastIterableIterator(target, kind); }; } - function iteratorMixin(name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) { + function iteratorMixin(name, object2, kInternalIterator, keyIndex = 0, valueIndex = 1) { const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex); const properties = { keys: { @@ -4789,7 +4789,7 @@ var require_util2 = __commonJS({ enumerable: true, configurable: true, value: function keys() { - webidl.brandCheck(this, object); + webidl.brandCheck(this, object2); return makeIterator(this, "key"); } }, @@ -4798,7 +4798,7 @@ var require_util2 = __commonJS({ enumerable: true, configurable: true, value: function values() { - webidl.brandCheck(this, object); + webidl.brandCheck(this, object2); return makeIterator(this, "value"); } }, @@ -4807,7 +4807,7 @@ var require_util2 = __commonJS({ enumerable: true, configurable: true, value: function entries() { - webidl.brandCheck(this, object); + webidl.brandCheck(this, object2); return makeIterator(this, "key+value"); } }, @@ -4816,7 +4816,7 @@ var require_util2 = __commonJS({ enumerable: true, configurable: true, value: function forEach(callbackfn, thisArg = globalThis) { - webidl.brandCheck(this, object); + webidl.brandCheck(this, object2); webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`); if (typeof callbackfn !== "function") { throw new TypeError( @@ -4829,7 +4829,7 @@ var require_util2 = __commonJS({ } } }; - return Object.defineProperties(object.prototype, { + return Object.defineProperties(object2.prototype, { ...properties, [Symbol.iterator]: { writable: true, @@ -5229,8 +5229,8 @@ var require_file = __commonJS({ } }; webidl.converters.Blob = webidl.interfaceConverter(Blob2); - function isFileLike(object) { - return object instanceof File2 || object && (typeof object.stream === "function" || typeof object.arrayBuffer === "function") && object[Symbol.toStringTag] === "File"; + function isFileLike(object2) { + return object2 instanceof File2 || object2 && (typeof object2.stream === "function" || typeof object2.arrayBuffer === "function") && object2[Symbol.toStringTag] === "File"; } module2.exports = { FileLike, isFileLike }; } @@ -5678,12 +5678,12 @@ var require_body = __commonJS({ } }); } - function extractBody(object, keepalive = false) { + function extractBody(object2, keepalive = false) { let stream2 = null; - if (object instanceof ReadableStream) { - stream2 = object; - } else if (isBlobLike(object)) { - stream2 = object.stream(); + if (object2 instanceof ReadableStream) { + stream2 = object2; + } else if (isBlobLike(object2)) { + stream2 = object2.stream(); } else { stream2 = new ReadableStream({ async pull(controller) { @@ -5703,17 +5703,17 @@ var require_body = __commonJS({ let source = null; let length = null; let type = null; - if (typeof object === "string") { - source = object; + if (typeof object2 === "string") { + source = object2; type = "text/plain;charset=UTF-8"; - } else if (object instanceof URLSearchParams) { - source = object.toString(); + } else if (object2 instanceof URLSearchParams) { + source = object2.toString(); type = "application/x-www-form-urlencoded;charset=UTF-8"; - } else if (isArrayBuffer(object)) { - source = new Uint8Array(object.slice()); - } else if (ArrayBuffer.isView(object)) { - source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)); - } else if (util3.isFormDataLike(object)) { + } else if (isArrayBuffer(object2)) { + source = new Uint8Array(object2.slice()); + } else if (ArrayBuffer.isView(object2)) { + source = new Uint8Array(object2.buffer.slice(object2.byteOffset, object2.byteOffset + object2.byteLength)); + } else if (util3.isFormDataLike(object2)) { const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, "0")}`; const prefix = `--${boundary}\r Content-Disposition: form-data`; @@ -5723,7 +5723,7 @@ Content-Disposition: form-data`; const rn = new Uint8Array([13, 10]); length = 0; let hasUnknownSizeValue = false; - for (const [name, value] of object) { + for (const [name, value] of object2) { if (typeof value === "string") { const chunk2 = textEncoder.encode(prefix + `; name="${escape3(normalizeLinefeeds(name))}"\r \r @@ -5751,7 +5751,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r if (hasUnknownSizeValue) { length = null; } - source = object; + source = object2; action = async function* () { for (const part of blobParts) { if (part.stream) { @@ -5762,22 +5762,22 @@ Content-Type: ${value.type || "application/octet-stream"}\r } }; type = `multipart/form-data; boundary=${boundary}`; - } else if (isBlobLike(object)) { - source = object; - length = object.size; - if (object.type) { - type = object.type; + } else if (isBlobLike(object2)) { + source = object2; + length = object2.size; + if (object2.type) { + type = object2.type; } - } else if (typeof object[Symbol.asyncIterator] === "function") { + } else if (typeof object2[Symbol.asyncIterator] === "function") { if (keepalive) { throw new TypeError("keepalive"); } - if (util3.isDisturbed(object) || object.locked) { + if (util3.isDisturbed(object2) || object2.locked) { throw new TypeError( "Response body object should not be disturbed or locked" ); } - stream2 = object instanceof ReadableStream ? object : ReadableStreamFrom(object); + stream2 = object2 instanceof ReadableStream ? object2 : ReadableStreamFrom(object2); } if (typeof source === "string" || util3.isBuffer(source)) { length = Buffer.byteLength(source); @@ -5786,7 +5786,7 @@ Content-Type: ${value.type || "application/octet-stream"}\r let iterator2; stream2 = new ReadableStream({ async start() { - iterator2 = action(object)[Symbol.asyncIterator](); + iterator2 = action(object2)[Symbol.asyncIterator](); }, async pull(controller) { const { value, done } = await iterator2.next(); @@ -5814,12 +5814,12 @@ Content-Type: ${value.type || "application/octet-stream"}\r const body = { stream: stream2, source, length }; return [body, type]; } - function safelyExtractBody(object, keepalive = false) { - if (object instanceof ReadableStream) { - assert(!util3.isDisturbed(object), "The body has already been consumed."); - assert(!object.locked, "The stream is locked."); + function safelyExtractBody(object2, keepalive = false) { + if (object2 instanceof ReadableStream) { + assert(!util3.isDisturbed(object2), "The body has already been consumed."); + assert(!object2.locked, "The stream is locked."); } - return extractBody(object, keepalive); + return extractBody(object2, keepalive); } function cloneBody(instance, body) { const [out1, out2] = body.stream.tee(); @@ -5899,12 +5899,12 @@ Content-Type: ${value.type || "application/octet-stream"}\r function mixinBody(prototype) { Object.assign(prototype.prototype, bodyMixinMethods(prototype)); } - async function consumeBody(object, convertBytesToJSValue, instance) { - webidl.brandCheck(object, instance); - if (bodyUnusable(object)) { + async function consumeBody(object2, convertBytesToJSValue, instance) { + webidl.brandCheck(object2, instance); + if (bodyUnusable(object2)) { throw new TypeError("Body is unusable: Body has already been read"); } - throwIfAborted(object[kState]); + throwIfAborted(object2[kState]); const promise = createDeferredPromise(); const errorSteps = (error3) => promise.reject(error3); const successSteps = (data) => { @@ -5914,15 +5914,15 @@ Content-Type: ${value.type || "application/octet-stream"}\r errorSteps(e); } }; - if (object[kState].body == null) { + if (object2[kState].body == null) { successSteps(Buffer.allocUnsafe(0)); return promise.promise; } - await fullyReadBody(object[kState].body, successSteps, errorSteps); + await fullyReadBody(object2[kState].body, successSteps, errorSteps); return promise.promise; } - function bodyUnusable(object) { - const body = object[kState].body; + function bodyUnusable(object2) { + const body = object2[kState].body; return body != null && (body.stream.locked || util3.isDisturbed(body.stream)); } function parseJSONFromBytes(bytes) { @@ -12066,10 +12066,10 @@ var require_headers = __commonJS({ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i; return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j); } - function fill(headers, object) { - if (Array.isArray(object)) { - for (let i = 0; i < object.length; ++i) { - const header = object[i]; + function fill(headers, object2) { + if (Array.isArray(object2)) { + for (let i = 0; i < object2.length; ++i) { + const header = object2[i]; if (header.length !== 2) { throw webidl.errors.exception({ header: "Headers constructor", @@ -12078,10 +12078,10 @@ var require_headers = __commonJS({ } appendHeader(headers, header[0], header[1]); } - } else if (typeof object === "object" && object !== null) { - const keys = Object.keys(object); + } else if (typeof object2 === "object" && object2 !== null) { + const keys = Object.keys(object2); for (let i = 0; i < keys.length; ++i) { - appendHeader(headers, keys[i], object[keys[i]]); + appendHeader(headers, keys[i], object2[keys[i]]); } } else { throw webidl.errors.conversionFailed({ @@ -12234,24 +12234,24 @@ var require_headers = __commonJS({ // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set toSortedArray() { const size = this[kHeadersMap].size; - const array = new Array(size); + const array2 = new Array(size); if (size <= 32) { if (size === 0) { - return array; + return array2; } const iterator2 = this[kHeadersMap][Symbol.iterator](); const firstValue = iterator2.next().value; - array[0] = [firstValue[0], firstValue[1].value]; + array2[0] = [firstValue[0], firstValue[1].value]; assert(firstValue[1].value !== null); for (let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value; i < size; ++i) { value = iterator2.next().value; - x = array[i] = [value[0], value[1].value]; + x = array2[i] = [value[0], value[1].value]; assert(x[1] !== null); left = 0; right = i; while (left < right) { pivot = left + (right - left >> 1); - if (array[pivot][0] <= x[0]) { + if (array2[pivot][0] <= x[0]) { left = pivot + 1; } else { right = pivot; @@ -12260,22 +12260,22 @@ var require_headers = __commonJS({ if (i !== pivot) { j = i; while (j > left) { - array[j] = array[--j]; + array2[j] = array2[--j]; } - array[left] = x; + array2[left] = x; } } if (!iterator2.next().done) { throw new TypeError("Unreachable"); } - return array; + return array2; } else { let i = 0; for (const { 0: name, 1: { value } } of this[kHeadersMap]) { - array[i++] = [name, value]; + array2[i++] = [name, value]; assert(value !== null); } - return array.sort(compareHeaderName); + return array2.sort(compareHeaderName); } } }; @@ -22049,12 +22049,12 @@ var init_universal_user_agent2 = __esm({ }); // node_modules/@octokit/endpoint/dist-bundle/index.js -function lowercaseKeys(object) { - if (!object) { +function lowercaseKeys(object2) { + if (!object2) { return {}; } - return Object.keys(object).reduce((newObj, key) => { - newObj[key.toLowerCase()] = object[key]; + return Object.keys(object2).reduce((newObj, key) => { + newObj[key.toLowerCase()] = object2[key]; return newObj; }, {}); } @@ -22130,11 +22130,11 @@ function extractUrlVariableNames(url2) { } return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []); } -function omit(object, keysToOmit) { +function omit(object2, keysToOmit) { const result = { __proto__: null }; - for (const key of Object.keys(object)) { + for (const key of Object.keys(object2)) { if (keysToOmit.indexOf(key) === -1) { - result[key] = object[key]; + result[key] = object2[key]; } } return result; @@ -29616,9 +29616,9 @@ var require_helpers = __commonJS({ } } function deepMerge(target, src) { - var array = Array.isArray(src); - var dst = array && [] || {}; - if (array) { + var array2 = Array.isArray(src); + var dst = array2 && [] || {}; + if (array2) { target = target || []; dst = dst.concat(target); src.forEach(deepMerger.bind(null, target, dst)); @@ -29853,11 +29853,11 @@ var require_attribute = __commonJS({ } return result; }; - function getEnumerableProperty(object, key) { - if (Object.hasOwnProperty.call(object, key)) return object[key]; - if (!(key in object)) return; - while (object = Object.getPrototypeOf(object)) { - if (Object.propertyIsEnumerable.call(object, key)) return object[key]; + function getEnumerableProperty(object2, key) { + if (Object.hasOwnProperty.call(object2, key)) return object2[key]; + if (!(key in object2)) return; + while (object2 = Object.getPrototypeOf(object2)) { + if (Object.propertyIsEnumerable.call(object2, key)) return object2[key]; } } validators.propertyNames = function validatePropertyNames(instance, schema, options, ctx) { @@ -41829,7 +41829,7 @@ var require_serializer = __commonJS({ * * @returns A valid serialized Javascript object */ - serialize(mapper, object, objectName, options = { xml: {} }) { + serialize(mapper, object2, objectName, options = { xml: {} }) { const updatedOptions = { xml: { rootName: options.xml.rootName ?? "", @@ -41846,40 +41846,40 @@ var require_serializer = __commonJS({ payload = []; } if (mapper.isConstant) { - object = mapper.defaultValue; + object2 = mapper.defaultValue; } const { required, nullable } = mapper; - if (required && nullable && object === void 0) { + if (required && nullable && object2 === void 0) { throw new Error(`${objectName} cannot be undefined.`); } - if (required && !nullable && (object === void 0 || object === null)) { + if (required && !nullable && (object2 === void 0 || object2 === null)) { throw new Error(`${objectName} cannot be null or undefined.`); } - if (!required && nullable === false && object === null) { + if (!required && nullable === false && object2 === null) { throw new Error(`${objectName} cannot be null.`); } - if (object === void 0 || object === null) { - payload = object; + if (object2 === void 0 || object2 === null) { + payload = object2; } else { if (mapperType.match(/^any$/i) !== null) { - payload = object; + payload = object2; } else if (mapperType.match(/^(Number|String|Boolean|Object|Stream|Uuid)$/i) !== null) { - payload = serializeBasicTypes(mapperType, objectName, object); + payload = serializeBasicTypes(mapperType, objectName, object2); } else if (mapperType.match(/^Enum$/i) !== null) { const enumMapper = mapper; - payload = serializeEnumType(objectName, enumMapper.type.allowedValues, object); + payload = serializeEnumType(objectName, enumMapper.type.allowedValues, object2); } else if (mapperType.match(/^(Date|DateTime|TimeSpan|DateTimeRfc1123|UnixTime)$/i) !== null) { - payload = serializeDateTypes(mapperType, object, objectName); + payload = serializeDateTypes(mapperType, object2, objectName); } else if (mapperType.match(/^ByteArray$/i) !== null) { - payload = serializeByteArrayType(objectName, object); + payload = serializeByteArrayType(objectName, object2); } else if (mapperType.match(/^Base64Url$/i) !== null) { - payload = serializeBase64UrlType(objectName, object); + payload = serializeBase64UrlType(objectName, object2); } else if (mapperType.match(/^Sequence$/i) !== null) { - payload = serializeSequenceType(this, mapper, object, objectName, Boolean(this.isXML), updatedOptions); + payload = serializeSequenceType(this, mapper, object2, objectName, Boolean(this.isXML), updatedOptions); } else if (mapperType.match(/^Dictionary$/i) !== null) { - payload = serializeDictionaryType(this, mapper, object, objectName, Boolean(this.isXML), updatedOptions); + payload = serializeDictionaryType(this, mapper, object2, objectName, Boolean(this.isXML), updatedOptions); } else if (mapperType.match(/^Composite$/i) !== null) { - payload = serializeCompositeType(this, mapper, object, objectName, Boolean(this.isXML), updatedOptions); + payload = serializeCompositeType(this, mapper, object2, objectName, Boolean(this.isXML), updatedOptions); } } return payload; @@ -42119,8 +42119,8 @@ var require_serializer = __commonJS({ } return value; } - function serializeSequenceType(serializer, mapper, object, objectName, isXml, options) { - if (!Array.isArray(object)) { + function serializeSequenceType(serializer, mapper, object2, objectName, isXml, options) { + if (!Array.isArray(object2)) { throw new Error(`${objectName} must be of type Array.`); } let elementType = mapper.type.element; @@ -42131,8 +42131,8 @@ var require_serializer = __commonJS({ elementType = serializer.modelMappers[elementType.type.className] ?? elementType; } const tempArray = []; - for (let i = 0; i < object.length; i++) { - const serializedValue = serializer.serialize(elementType, object[i], objectName, options); + for (let i = 0; i < object2.length; i++) { + const serializedValue = serializer.serialize(elementType, object2[i], objectName, options); if (isXml && elementType.xmlNamespace) { const xmlnsKey = elementType.xmlNamespacePrefix ? `xmlns:${elementType.xmlNamespacePrefix}` : "xmlns"; if (elementType.type.name === "Composite") { @@ -42149,8 +42149,8 @@ var require_serializer = __commonJS({ } return tempArray; } - function serializeDictionaryType(serializer, mapper, object, objectName, isXml, options) { - if (typeof object !== "object") { + function serializeDictionaryType(serializer, mapper, object2, objectName, isXml, options) { + if (typeof object2 !== "object") { throw new Error(`${objectName} must be of type object.`); } const valueType = mapper.type.value; @@ -42158,8 +42158,8 @@ var require_serializer = __commonJS({ throw new Error(`"value" metadata for a Dictionary must be defined in the mapper and it must of type "object" in ${objectName}.`); } const tempDictionary = {}; - for (const key of Object.keys(object)) { - const serializedValue = serializer.serialize(valueType, object[key], objectName, options); + for (const key of Object.keys(object2)) { + const serializedValue = serializer.serialize(valueType, object2[key], objectName, options); tempDictionary[key] = getXmlObjectValue(valueType, serializedValue, isXml, options); } if (isXml && mapper.xmlNamespace) { @@ -42199,11 +42199,11 @@ var require_serializer = __commonJS({ } return modelProps; } - function serializeCompositeType(serializer, mapper, object, objectName, isXml, options) { + function serializeCompositeType(serializer, mapper, object2, objectName, isXml, options) { if (getPolymorphicDiscriminatorRecursively(serializer, mapper)) { - mapper = getPolymorphicMapper(serializer, mapper, object, "clientName"); + mapper = getPolymorphicMapper(serializer, mapper, object2, "clientName"); } - if (object !== void 0 && object !== null) { + if (object2 !== void 0 && object2 !== null) { const payload = {}; const modelProps = resolveModelProperties(serializer, mapper, objectName); for (const key of Object.keys(modelProps)) { @@ -42224,7 +42224,7 @@ var require_serializer = __commonJS({ propName = paths.pop(); for (const pathName of paths) { const childObject = parentObject[pathName]; - if ((childObject === void 0 || childObject === null) && (object[key] !== void 0 && object[key] !== null || propertyMapper.defaultValue !== void 0)) { + if ((childObject === void 0 || childObject === null) && (object2[key] !== void 0 && object2[key] !== null || propertyMapper.defaultValue !== void 0)) { parentObject[pathName] = {}; } parentObject = parentObject[pathName]; @@ -42239,7 +42239,7 @@ var require_serializer = __commonJS({ }; } const propertyObjectName = propertyMapper.serializedName !== "" ? objectName + "." + propertyMapper.serializedName : objectName; - let toSerialize = object[key]; + let toSerialize = object2[key]; const polymorphicDiscriminator = getPolymorphicDiscriminatorRecursively(serializer, mapper); if (polymorphicDiscriminator && polymorphicDiscriminator.clientName === key && (toSerialize === void 0 || toSerialize === null)) { toSerialize = mapper.serializedName; @@ -42261,16 +42261,16 @@ var require_serializer = __commonJS({ const additionalPropertiesMapper = resolveAdditionalProperties(serializer, mapper, objectName); if (additionalPropertiesMapper) { const propNames = Object.keys(modelProps); - for (const clientPropName in object) { + for (const clientPropName in object2) { const isAdditionalProperty = propNames.every((pn) => pn !== clientPropName); if (isAdditionalProperty) { - payload[clientPropName] = serializer.serialize(additionalPropertiesMapper, object[clientPropName], objectName + '["' + clientPropName + '"]', options); + payload[clientPropName] = serializer.serialize(additionalPropertiesMapper, object2[clientPropName], objectName + '["' + clientPropName + '"]', options); } } } return payload; } - return object; + return object2; } function getXmlObjectValue(propertyMapper, serializedValue, isXml, options) { if (!isXml || !propertyMapper.xmlNamespace) { @@ -42454,7 +42454,7 @@ var require_serializer = __commonJS({ } return void 0; } - function getPolymorphicMapper(serializer, mapper, object, polymorphicPropertyName) { + function getPolymorphicMapper(serializer, mapper, object2, polymorphicPropertyName) { const polymorphicDiscriminator = getPolymorphicDiscriminatorRecursively(serializer, mapper); if (polymorphicDiscriminator) { let discriminatorName = polymorphicDiscriminator[polymorphicPropertyName]; @@ -42462,7 +42462,7 @@ var require_serializer = __commonJS({ if (polymorphicPropertyName === "serializedName") { discriminatorName = discriminatorName.replace(/\\/gi, ""); } - const discriminatorValue = object[discriminatorName]; + const discriminatorValue = object2[discriminatorName]; const typeName = mapper.type.uberParent ?? mapper.type.className; if (typeof discriminatorValue === "string" && typeName) { const polymorphicMapper = getIndexDiscriminator(serializer.modelMappers.discriminators, discriminatorValue, typeName); @@ -47295,8 +47295,8 @@ var require_StorageSharedKeyCredentialPolicy = __commonJS({ headersArray.sort((a, b) => { return (0, SharedKeyComparator_js_1.compareHeader)(a.name.toLowerCase(), b.name.toLowerCase()); }); - headersArray = headersArray.filter((value, index2, array) => { - if (index2 > 0 && value.name.toLowerCase() === array[index2 - 1].name.toLowerCase()) { + headersArray = headersArray.filter((value, index2, array2) => { + if (index2 > 0 && value.name.toLowerCase() === array2[index2 - 1].name.toLowerCase()) { return false; } return true; @@ -49047,8 +49047,8 @@ var require_StorageSharedKeyCredentialPolicy2 = __commonJS({ headersArray.sort((a, b) => { return (0, SharedKeyComparator_js_1.compareHeader)(a.name.toLowerCase(), b.name.toLowerCase()); }); - headersArray = headersArray.filter((value, index2, array) => { - if (index2 > 0 && value.name.toLowerCase() === array[index2 - 1].name.toLowerCase()) { + headersArray = headersArray.filter((value, index2, array2) => { + if (index2 > 0 && value.name.toLowerCase() === array2[index2 - 1].name.toLowerCase()) { return false; } return true; @@ -49684,8 +49684,8 @@ var require_StorageSharedKeyCredentialPolicyV2 = __commonJS({ headersArray.sort((a, b) => { return (0, SharedKeyComparator_js_1.compareHeader)(a.name.toLowerCase(), b.name.toLowerCase()); }); - headersArray = headersArray.filter((value, index2, array) => { - if (index2 > 0 && value.name.toLowerCase() === array[index2 - 1].name.toLowerCase()) { + headersArray = headersArray.filter((value, index2, array2) => { + if (index2 > 0 && value.name.toLowerCase() === array2[index2 - 1].name.toLowerCase()) { return false; } return true; @@ -50031,8 +50031,8 @@ var require_StorageSharedKeyCredentialPolicyV22 = __commonJS({ headersArray.sort((a, b) => { return (0, SharedKeyComparator_js_1.compareHeader)(a.name.toLowerCase(), b.name.toLowerCase()); }); - headersArray = headersArray.filter((value, index2, array) => { - if (index2 > 0 && value.name.toLowerCase() === array[index2 - 1].name.toLowerCase()) { + headersArray = headersArray.filter((value, index2, array2) => { + if (index2 > 0 && value.name.toLowerCase() === array2[index2 - 1].name.toLowerCase()) { return false; } return true; @@ -77795,14 +77795,14 @@ var require_reflection_json_writer = __commonJS({ /** * Returns `null` as the default for google.protobuf.NullValue. */ - enum(type, value, fieldName, optional, emitDefaultValues, enumAsInteger) { + enum(type, value, fieldName, optional2, emitDefaultValues, enumAsInteger) { if (type[0] == "google.protobuf.NullValue") - return !emitDefaultValues && !optional ? void 0 : null; + return !emitDefaultValues && !optional2 ? void 0 : null; if (value === void 0) { - assert_1.assert(optional); + assert_1.assert(optional2); return void 0; } - if (value === 0 && !emitDefaultValues && !optional) + if (value === 0 && !emitDefaultValues && !optional2) return void 0; assert_1.assert(typeof value == "number"); assert_1.assert(Number.isInteger(value)); @@ -77817,12 +77817,12 @@ var require_reflection_json_writer = __commonJS({ return options.emitDefaultValues ? null : void 0; return type.internalJsonWrite(value, options); } - scalar(type, value, fieldName, optional, emitDefaultValues) { + scalar(type, value, fieldName, optional2, emitDefaultValues) { if (value === void 0) { - assert_1.assert(optional); + assert_1.assert(optional2); return void 0; } - const ed = emitDefaultValues || optional; + const ed = emitDefaultValues || optional2; switch (type) { // int32, fixed32, uint32: JSON value will be a decimal number. Either numbers or strings are accepted. case reflection_info_1.ScalarType.INT32: @@ -91326,8 +91326,8 @@ var require_async = __commonJS({ } } var race$1 = awaitify(race, 2); - function reduceRight(array, memo, iteratee, callback) { - var reversed = [...array].reverse(); + function reduceRight(array2, memo, iteratee, callback) { + var reversed = [...array2].reverse(); return reduce$1(reversed, memo, iteratee, callback); } function reflect(fn) { @@ -93098,15 +93098,15 @@ var require_stream_writable = __commonJS({ if (typeof Symbol === "function" && Symbol.hasInstance && typeof Function.prototype[Symbol.hasInstance] === "function") { realHasInstance = Function.prototype[Symbol.hasInstance]; Object.defineProperty(Writable, Symbol.hasInstance, { - value: function(object) { - if (realHasInstance.call(this, object)) return true; + value: function(object2) { + if (realHasInstance.call(this, object2)) return true; if (this !== Writable) return false; - return object && object._writableState instanceof WritableState; + return object2 && object2._writableState instanceof WritableState; } }); } else { - realHasInstance = function(object) { - return object instanceof this; + realHasInstance = function(object2) { + return object2 instanceof this; }; } function Writable(options) { @@ -94701,16 +94701,16 @@ var require_overRest = __commonJS({ function overRest(func, start, transform) { start = nativeMax(start === void 0 ? func.length - 1 : start, 0); return function() { - var args = arguments, index2 = -1, length = nativeMax(args.length - start, 0), array = Array(length); + var args = arguments, index2 = -1, length = nativeMax(args.length - start, 0), array2 = Array(length); while (++index2 < length) { - array[index2] = args[start + index2]; + array2[index2] = args[start + index2]; } index2 = -1; var otherArgs = Array(start + 1); while (++index2 < start) { otherArgs[index2] = args[index2]; } - otherArgs[start] = transform(array); + otherArgs[start] = transform(array2); return apply(func, this, otherArgs); }; } @@ -94924,8 +94924,8 @@ var require_baseIsNative = __commonJS({ // node_modules/lodash/_getValue.js var require_getValue = __commonJS({ "node_modules/lodash/_getValue.js"(exports2, module2) { - function getValue(object, key) { - return object == null ? void 0 : object[key]; + function getValue(object2, key) { + return object2 == null ? void 0 : object2[key]; } module2.exports = getValue; } @@ -94936,8 +94936,8 @@ var require_getNative = __commonJS({ "node_modules/lodash/_getNative.js"(exports2, module2) { var baseIsNative = require_baseIsNative(); var getValue = require_getValue(); - function getNative(object, key) { - var value = getValue(object, key); + function getNative(object2, key) { + var value = getValue(object2, key); return baseIsNative(value) ? value : void 0; } module2.exports = getNative; @@ -95080,13 +95080,13 @@ var require_isIterateeCall = __commonJS({ var isArrayLike = require_isArrayLike(); var isIndex = require_isIndex(); var isObject2 = require_isObject(); - function isIterateeCall(value, index2, object) { - if (!isObject2(object)) { + function isIterateeCall(value, index2, object2) { + if (!isObject2(object2)) { return false; } var type = typeof index2; - if (type == "number" ? isArrayLike(object) && isIndex(index2, object.length) : type == "string" && index2 in object) { - return eq(object[index2], value); + if (type == "number" ? isArrayLike(object2) && isIndex(index2, object2.length) : type == "string" && index2 in object2) { + return eq(object2[index2], value); } return false; } @@ -95310,10 +95310,10 @@ var require_isPrototype = __commonJS({ // node_modules/lodash/_nativeKeysIn.js var require_nativeKeysIn = __commonJS({ "node_modules/lodash/_nativeKeysIn.js"(exports2, module2) { - function nativeKeysIn(object) { + function nativeKeysIn(object2) { var result = []; - if (object != null) { - for (var key in Object(object)) { + if (object2 != null) { + for (var key in Object(object2)) { result.push(key); } } @@ -95331,13 +95331,13 @@ var require_baseKeysIn = __commonJS({ var nativeKeysIn = require_nativeKeysIn(); var objectProto = Object.prototype; var hasOwnProperty = objectProto.hasOwnProperty; - function baseKeysIn(object) { - if (!isObject2(object)) { - return nativeKeysIn(object); + function baseKeysIn(object2) { + if (!isObject2(object2)) { + return nativeKeysIn(object2); } - var isProto = isPrototype(object), result = []; - for (var key in object) { - if (!(key == "constructor" && (isProto || !hasOwnProperty.call(object, key)))) { + var isProto = isPrototype(object2), result = []; + for (var key in object2) { + if (!(key == "constructor" && (isProto || !hasOwnProperty.call(object2, key)))) { result.push(key); } } @@ -95353,8 +95353,8 @@ var require_keysIn = __commonJS({ var arrayLikeKeys = require_arrayLikeKeys(); var baseKeysIn = require_baseKeysIn(); var isArrayLike = require_isArrayLike(); - function keysIn(object) { - return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); + function keysIn(object2) { + return isArrayLike(object2) ? arrayLikeKeys(object2, true) : baseKeysIn(object2); } module2.exports = keysIn; } @@ -95369,8 +95369,8 @@ var require_defaults = __commonJS({ var keysIn = require_keysIn(); var objectProto = Object.prototype; var hasOwnProperty = objectProto.hasOwnProperty; - var defaults2 = baseRest(function(object, sources) { - object = Object(object); + var defaults2 = baseRest(function(object2, sources) { + object2 = Object(object2); var index2 = -1; var length = sources.length; var guard = length > 2 ? sources[2] : void 0; @@ -95384,13 +95384,13 @@ var require_defaults = __commonJS({ var propsLength = props.length; while (++propsIndex < propsLength) { var key = props[propsIndex]; - var value = object[key]; - if (value === void 0 || eq(value, objectProto[key]) && !hasOwnProperty.call(object, key)) { - object[key] = source[key]; + var value = object2[key]; + if (value === void 0 || eq(value, objectProto[key]) && !hasOwnProperty.call(object2, key)) { + object2[key] = source[key]; } } } - return object; + return object2; }); module2.exports = defaults2; } @@ -99487,10 +99487,10 @@ var require_writable = __commonJS({ } ObjectDefineProperty(Writable, SymbolHasInstance, { __proto__: null, - value: function(object) { - if (FunctionPrototypeSymbolHasInstance(this, object)) return true; + value: function(object2) { + if (FunctionPrototypeSymbolHasInstance(this, object2)) return true; if (this !== Writable) return false; - return object && object._writableState instanceof WritableState; + return object2 && object2._writableState instanceof WritableState; } }); Writable.prototype.pipe = function() { @@ -101833,12 +101833,12 @@ var require_ours = __commonJS({ // node_modules/lodash/_arrayPush.js var require_arrayPush = __commonJS({ "node_modules/lodash/_arrayPush.js"(exports2, module2) { - function arrayPush(array, values) { - var index2 = -1, length = values.length, offset = array.length; + function arrayPush(array2, values) { + var index2 = -1, length = values.length, offset = array2.length; while (++index2 < length) { - array[offset + index2] = values[index2]; + array2[offset + index2] = values[index2]; } - return array; + return array2; } module2.exports = arrayPush; } @@ -101863,12 +101863,12 @@ var require_baseFlatten = __commonJS({ "node_modules/lodash/_baseFlatten.js"(exports2, module2) { var arrayPush = require_arrayPush(); var isFlattenable = require_isFlattenable(); - function baseFlatten(array, depth, predicate, isStrict, result) { - var index2 = -1, length = array.length; + function baseFlatten(array2, depth, predicate, isStrict, result) { + var index2 = -1, length = array2.length; predicate || (predicate = isFlattenable); result || (result = []); while (++index2 < length) { - var value = array[index2]; + var value = array2[index2]; if (depth > 0 && predicate(value)) { if (depth > 1) { baseFlatten(value, depth - 1, predicate, isStrict, result); @@ -101889,9 +101889,9 @@ var require_baseFlatten = __commonJS({ var require_flatten = __commonJS({ "node_modules/lodash/flatten.js"(exports2, module2) { var baseFlatten = require_baseFlatten(); - function flatten(array) { - var length = array == null ? 0 : array.length; - return length ? baseFlatten(array, 1) : []; + function flatten(array2) { + var length = array2 == null ? 0 : array2.length; + return length ? baseFlatten(array2, 1) : []; } module2.exports = flatten; } @@ -102018,10 +102018,10 @@ var require_listCacheClear = __commonJS({ var require_assocIndexOf = __commonJS({ "node_modules/lodash/_assocIndexOf.js"(exports2, module2) { var eq = require_eq2(); - function assocIndexOf(array, key) { - var length = array.length; + function assocIndexOf(array2, key) { + var length = array2.length; while (length--) { - if (eq(array[length][0], key)) { + if (eq(array2[length][0], key)) { return length; } } @@ -102290,10 +102290,10 @@ var require_SetCache = __commonJS({ // node_modules/lodash/_baseFindIndex.js var require_baseFindIndex = __commonJS({ "node_modules/lodash/_baseFindIndex.js"(exports2, module2) { - function baseFindIndex(array, predicate, fromIndex, fromRight) { - var length = array.length, index2 = fromIndex + (fromRight ? 1 : -1); + function baseFindIndex(array2, predicate, fromIndex, fromRight) { + var length = array2.length, index2 = fromIndex + (fromRight ? 1 : -1); while (fromRight ? index2-- : ++index2 < length) { - if (predicate(array[index2], index2, array)) { + if (predicate(array2[index2], index2, array2)) { return index2; } } @@ -102316,10 +102316,10 @@ var require_baseIsNaN = __commonJS({ // node_modules/lodash/_strictIndexOf.js var require_strictIndexOf = __commonJS({ "node_modules/lodash/_strictIndexOf.js"(exports2, module2) { - function strictIndexOf(array, value, fromIndex) { - var index2 = fromIndex - 1, length = array.length; + function strictIndexOf(array2, value, fromIndex) { + var index2 = fromIndex - 1, length = array2.length; while (++index2 < length) { - if (array[index2] === value) { + if (array2[index2] === value) { return index2; } } @@ -102335,8 +102335,8 @@ var require_baseIndexOf = __commonJS({ var baseFindIndex = require_baseFindIndex(); var baseIsNaN = require_baseIsNaN(); var strictIndexOf = require_strictIndexOf(); - function baseIndexOf(array, value, fromIndex) { - return value === value ? strictIndexOf(array, value, fromIndex) : baseFindIndex(array, baseIsNaN, fromIndex); + function baseIndexOf(array2, value, fromIndex) { + return value === value ? strictIndexOf(array2, value, fromIndex) : baseFindIndex(array2, baseIsNaN, fromIndex); } module2.exports = baseIndexOf; } @@ -102346,9 +102346,9 @@ var require_baseIndexOf = __commonJS({ var require_arrayIncludes = __commonJS({ "node_modules/lodash/_arrayIncludes.js"(exports2, module2) { var baseIndexOf = require_baseIndexOf(); - function arrayIncludes(array, value) { - var length = array == null ? 0 : array.length; - return !!length && baseIndexOf(array, value, 0) > -1; + function arrayIncludes(array2, value) { + var length = array2 == null ? 0 : array2.length; + return !!length && baseIndexOf(array2, value, 0) > -1; } module2.exports = arrayIncludes; } @@ -102357,10 +102357,10 @@ var require_arrayIncludes = __commonJS({ // node_modules/lodash/_arrayIncludesWith.js var require_arrayIncludesWith = __commonJS({ "node_modules/lodash/_arrayIncludesWith.js"(exports2, module2) { - function arrayIncludesWith(array, value, comparator) { - var index2 = -1, length = array == null ? 0 : array.length; + function arrayIncludesWith(array2, value, comparator) { + var index2 = -1, length = array2 == null ? 0 : array2.length; while (++index2 < length) { - if (comparator(value, array[index2])) { + if (comparator(value, array2[index2])) { return true; } } @@ -102373,10 +102373,10 @@ var require_arrayIncludesWith = __commonJS({ // node_modules/lodash/_arrayMap.js var require_arrayMap = __commonJS({ "node_modules/lodash/_arrayMap.js"(exports2, module2) { - function arrayMap(array, iteratee) { - var index2 = -1, length = array == null ? 0 : array.length, result = Array(length); + function arrayMap(array2, iteratee) { + var index2 = -1, length = array2 == null ? 0 : array2.length, result = Array(length); while (++index2 < length) { - result[index2] = iteratee(array[index2], index2, array); + result[index2] = iteratee(array2[index2], index2, array2); } return result; } @@ -102404,8 +102404,8 @@ var require_baseDifference = __commonJS({ var baseUnary = require_baseUnary(); var cacheHas = require_cacheHas(); var LARGE_ARRAY_SIZE = 200; - function baseDifference(array, values, iteratee, comparator) { - var index2 = -1, includes = arrayIncludes, isCommon = true, length = array.length, result = [], valuesLength = values.length; + function baseDifference(array2, values, iteratee, comparator) { + var index2 = -1, includes = arrayIncludes, isCommon = true, length = array2.length, result = [], valuesLength = values.length; if (!length) { return result; } @@ -102422,7 +102422,7 @@ var require_baseDifference = __commonJS({ } outer: while (++index2 < length) { - var value = array[index2], computed = iteratee == null ? value : iteratee(value); + var value = array2[index2], computed = iteratee == null ? value : iteratee(value); value = comparator || value !== 0 ? value : 0; if (isCommon && computed === computed) { var valuesIndex = valuesLength; @@ -102461,8 +102461,8 @@ var require_difference = __commonJS({ var baseFlatten = require_baseFlatten(); var baseRest = require_baseRest(); var isArrayLikeObject = require_isArrayLikeObject(); - var difference = baseRest(function(array, values) { - return isArrayLikeObject(array) ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true)) : []; + var difference = baseRest(function(array2, values) { + return isArrayLikeObject(array2) ? baseDifference(array2, baseFlatten(values, 1, isArrayLikeObject, true)) : []; }); module2.exports = difference; } @@ -102525,13 +102525,13 @@ var require_baseUniq = __commonJS({ var createSet = require_createSet(); var setToArray = require_setToArray(); var LARGE_ARRAY_SIZE = 200; - function baseUniq(array, iteratee, comparator) { - var index2 = -1, includes = arrayIncludes, length = array.length, isCommon = true, result = [], seen = result; + function baseUniq(array2, iteratee, comparator) { + var index2 = -1, includes = arrayIncludes, length = array2.length, isCommon = true, result = [], seen = result; if (comparator) { isCommon = false; includes = arrayIncludesWith; } else if (length >= LARGE_ARRAY_SIZE) { - var set = iteratee ? null : createSet(array); + var set = iteratee ? null : createSet(array2); if (set) { return setToArray(set); } @@ -102543,7 +102543,7 @@ var require_baseUniq = __commonJS({ } outer: while (++index2 < length) { - var value = array[index2], computed = iteratee ? iteratee(value) : value; + var value = array2[index2], computed = iteratee ? iteratee(value) : value; value = comparator || value !== 0 ? value : 0; if (isCommon && computed === computed) { var seenIndex = seen.length; @@ -105849,7 +105849,7 @@ var require_archiver_utils = __commonJS({ } return dateish; }; - utils.defaults = function(object, source, guard) { + utils.defaults = function(object2, source, guard) { var args = arguments; args[0] = args[0] || {}; return defaults2(...args); @@ -111731,12 +111731,12 @@ var require_dist_node2 = __commonJS({ format: "" } }; - function lowercaseKeys2(object) { - if (!object) { + function lowercaseKeys2(object2) { + if (!object2) { return {}; } - return Object.keys(object).reduce((newObj, key) => { - newObj[key.toLowerCase()] = object[key]; + return Object.keys(object2).reduce((newObj, key) => { + newObj[key.toLowerCase()] = object2[key]; return newObj; }, {}); } @@ -111818,11 +111818,11 @@ var require_dist_node2 = __commonJS({ } return matches.map(removeNonChars2).reduce((a, b) => a.concat(b), []); } - function omit2(object, keysToOmit) { + function omit2(object2, keysToOmit) { const result = { __proto__: null }; - for (const key of Object.keys(object)) { + for (const key of Object.keys(object2)) { if (keysToOmit.indexOf(key) === -1) { - result[key] = object[key]; + result[key] = object2[key]; } } return result; @@ -141654,7 +141654,7 @@ var nullCoreTag = defineScalarTag("tag:yaml.org,2002:null", { if (NULL_VALUES$1.indexOf(source) !== -1) return null; return NOT_RESOLVED; }, - identify: (object) => object === null, + identify: (object2) => object2 === null, represent: () => "null" }); var nullJsonTag = defineScalarTag("tag:yaml.org,2002:null", { @@ -141664,7 +141664,7 @@ var nullJsonTag = defineScalarTag("tag:yaml.org,2002:null", { if (source === "null" || isExplicit && source === "") return null; return NOT_RESOLVED; }, - identify: (object) => object === null, + identify: (object2) => object2 === null, represent: () => "null" }); var NULL_VALUES = [ @@ -141686,7 +141686,7 @@ var nullYaml11Tag = defineScalarTag("tag:yaml.org,2002:null", { if (NULL_VALUES.indexOf(source) !== -1) return null; return NOT_RESOLVED; }, - identify: (object) => object === null, + identify: (object2) => object2 === null, represent: () => "null" }); var TRUE_VALUES$2 = [ @@ -141712,8 +141712,8 @@ var boolCoreTag = defineScalarTag("tag:yaml.org,2002:bool", { if (FALSE_VALUES$2.indexOf(source) !== -1) return false; return NOT_RESOLVED; }, - identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]", - represent: (object) => object ? "true" : "false" + identify: (object2) => Object.prototype.toString.call(object2) === "[object Boolean]", + represent: (object2) => object2 ? "true" : "false" }); var TRUE_VALUES$1 = ["true"]; var FALSE_VALUES$1 = ["false"]; @@ -141725,8 +141725,8 @@ var boolJsonTag = defineScalarTag("tag:yaml.org,2002:bool", { if (FALSE_VALUES$1.indexOf(source) !== -1) return false; return NOT_RESOLVED; }, - identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]", - represent: (object) => object ? "true" : "false" + identify: (object2) => Object.prototype.toString.call(object2) === "[object Boolean]", + represent: (object2) => object2 ? "true" : "false" }); var TRUE_VALUES = [ "true", @@ -141773,8 +141773,8 @@ var boolYaml11Tag = defineScalarTag("tag:yaml.org,2002:bool", { if (FALSE_VALUES.indexOf(source) !== -1) return false; return NOT_RESOLVED; }, - identify: (object) => Object.prototype.toString.call(object) === "[object Boolean]", - represent: (object) => object ? "true" : "false" + identify: (object2) => Object.prototype.toString.call(object2) === "[object Boolean]", + represent: (object2) => object2 ? "true" : "false" }); var YAML_INTEGER_IMPLICIT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:0o[0-7]+|0x[0-9a-fA-F]+|[-+]?[0-9]+)$"); var YAML_INTEGER_EXPLICIT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1]+|[-+]?0o[0-7]+|[-+]?0x[0-9a-fA-F]+|[-+]?[0-9]+)$"); @@ -141805,8 +141805,8 @@ var intCoreTag = defineScalarTag("tag:yaml.org,2002:int", { ..."0123456789" ], resolve: resolveYamlInteger$2, - identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, - represent: (object) => object.toString(10) + identify: (object2) => Number.isInteger(object2) && !Object.is(object2, -0) && object2.toString(10).indexOf("e") < 0, + represent: (object2) => object2.toString(10) }); var YAML_INTEGER_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)$"); var YAML_INTEGER_EXPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1]+|[-+]?0o[0-7]+|[-+]?0x[0-9a-fA-F]+|[-+]?[0-9]+)$"); @@ -141833,8 +141833,8 @@ var intJsonTag = defineScalarTag("tag:yaml.org,2002:int", { implicit: true, implicitFirstChars: ["-", ..."0123456789"], resolve: resolveYamlInteger$1, - identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, - represent: (object) => object.toString(10) + identify: (object2) => Number.isInteger(object2) && !Object.is(object2, -0) && object2.toString(10).indexOf("e") < 0, + represent: (object2) => object2.toString(10) }); var YAML_INTEGER_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?0b[0-1_]+|[-+]?0[0-7_]+|[-+]?0x[0-9a-fA-F_]+|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+|[-+]?(?:0|[1-9][0-9_]*))$"); function parseYamlInteger(source) { @@ -141867,8 +141867,8 @@ var intYaml11Tag = defineScalarTag("tag:yaml.org,2002:int", { ..."0123456789" ], resolve: resolveYamlInteger, - identify: (object) => Number.isInteger(object) && !Object.is(object, -0) && object.toString(10).indexOf("e") < 0, - represent: (object) => object.toString(10) + identify: (object2) => Number.isInteger(object2) && !Object.is(object2, -0) && object2.toString(10).indexOf("e") < 0, + represent: (object2) => object2.toString(10) }); var YAML_FLOAT_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?[0-9]+(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?|[-+]?\\.[0-9]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); var YAML_FLOAT_SPECIAL_PATTERN$1 = /* @__PURE__ */ new RegExp("^(?:[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); @@ -141883,12 +141883,12 @@ function resolveYamlFloat$2(source) { if (Number.isFinite(result) || YAML_FLOAT_SPECIAL_PATTERN$1.test(source)) return result; return NOT_RESOLVED; } -function representYamlFloat$2(object) { - if (isNaN(object)) return ".nan"; - if (object === Number.POSITIVE_INFINITY) return ".inf"; - if (object === Number.NEGATIVE_INFINITY) return "-.inf"; - if (Object.is(object, -0)) return "-0.0"; - const result = object.toString(10); +function representYamlFloat$2(object2) { + if (isNaN(object2)) return ".nan"; + if (object2 === Number.POSITIVE_INFINITY) return ".inf"; + if (object2 === Number.NEGATIVE_INFINITY) return "-.inf"; + if (Object.is(object2, -0)) return "-0.0"; + const result = object2.toString(10); return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result; } var floatCoreTag = defineScalarTag("tag:yaml.org,2002:float", { @@ -141900,7 +141900,7 @@ var floatCoreTag = defineScalarTag("tag:yaml.org,2002:float", { ..."0123456789" ], resolve: resolveYamlFloat$2, - identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), + identify: (object2) => typeof object2 === "number" && (!Number.isInteger(object2) || Object.is(object2, -0) || object2.toString(10).indexOf("e") >= 0), represent: representYamlFloat$2 }); var YAML_FLOAT_IMPLICIT_PATTERN = /* @__PURE__ */ new RegExp("^-?(?:0|[1-9][0-9]*)(?:\\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$"); @@ -141921,19 +141921,19 @@ function resolveYamlFloat$1(source, isExplicit) { if (Number.isFinite(result)) return result; return NOT_RESOLVED; } -function representYamlFloat$1(object) { - if (isNaN(object)) return ".nan"; - if (object === Number.POSITIVE_INFINITY) return ".inf"; - if (object === Number.NEGATIVE_INFINITY) return "-.inf"; - if (Object.is(object, -0)) return "-0.0"; - const result = object.toString(10); +function representYamlFloat$1(object2) { + if (isNaN(object2)) return ".nan"; + if (object2 === Number.POSITIVE_INFINITY) return ".inf"; + if (object2 === Number.NEGATIVE_INFINITY) return "-.inf"; + if (Object.is(object2, -0)) return "-0.0"; + const result = object2.toString(10); return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result; } var floatJsonTag = defineScalarTag("tag:yaml.org,2002:float", { implicit: true, implicitFirstChars: ["-", ..."0123456789"], resolve: resolveYamlFloat$1, - identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), + identify: (object2) => typeof object2 === "number" && (!Number.isInteger(object2) || Object.is(object2, -0) || object2.toString(10).indexOf("e") >= 0), represent: representYamlFloat$1 }); var YAML_FLOAT_PATTERN = /* @__PURE__ */ new RegExp("^(?:[-+]?(?:(?:[0-9][0-9_]*)?\\.[0-9_]*)(?:[eE][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"); @@ -141953,12 +141953,12 @@ function resolveYamlFloat(source) { if (Number.isFinite(result) || YAML_FLOAT_SPECIAL_PATTERN.test(source)) return result; return NOT_RESOLVED; } -function representYamlFloat(object) { - if (isNaN(object)) return ".nan"; - if (object === Number.POSITIVE_INFINITY) return ".inf"; - if (object === Number.NEGATIVE_INFINITY) return "-.inf"; - if (Object.is(object, -0)) return "-0.0"; - const result = object.toString(10); +function representYamlFloat(object2) { + if (isNaN(object2)) return ".nan"; + if (object2 === Number.POSITIVE_INFINITY) return ".inf"; + if (object2 === Number.NEGATIVE_INFINITY) return "-.inf"; + if (Object.is(object2, -0)) return "-0.0"; + const result = object2.toString(10); return /^[-+]?[0-9]+e/.test(result) ? result.replace("e", ".e") : result; } var floatYaml11Tag = defineScalarTag("tag:yaml.org,2002:float", { @@ -141970,7 +141970,7 @@ var floatYaml11Tag = defineScalarTag("tag:yaml.org,2002:float", { ..."0123456789" ], resolve: resolveYamlFloat, - identify: (object) => typeof object === "number" && (!Number.isInteger(object) || Object.is(object, -0) || object.toString(10).indexOf("e") >= 0), + identify: (object2) => typeof object2 === "number" && (!Number.isInteger(object2) || Object.is(object2, -0) || object2.toString(10).indexOf("e") >= 0), represent: representYamlFloat }); var mergeTag = defineScalarTag("tag:yaml.org,2002:merge", { @@ -141990,14 +141990,14 @@ function resolveYamlBinary(source) { for (let index2 = 0; index2 < binary.length; index2++) result[index2] = binary.charCodeAt(index2); return result; } -function representYamlBinary(object) { +function representYamlBinary(object2) { let binary = ""; - for (let index2 = 0; index2 < object.length; index2++) binary += String.fromCharCode(object[index2]); + for (let index2 = 0; index2 < object2.length; index2++) binary += String.fromCharCode(object2[index2]); return btoa(binary); } var binaryTag = defineScalarTag("tag:yaml.org,2002:binary", { resolve: resolveYamlBinary, - identify: (object) => Object.prototype.toString.call(object) === "[object Uint8Array]", + identify: (object2) => Object.prototype.toString.call(object2) === "[object Uint8Array]", represent: representYamlBinary }); var YAML_DATE_REGEXP = /* @__PURE__ */ new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"); @@ -142039,8 +142039,8 @@ var timestampTag = defineScalarTag("tag:yaml.org,2002:timestamp", { implicit: true, implicitFirstChars: [..."0123456789"], resolve: resolveYamlTimestamp, - identify: (object) => object instanceof Date, - represent: (object) => object.toISOString() + identify: (object2) => object2 instanceof Date, + represent: (object2) => object2.toISOString() }); var seqTag = defineSequenceTag("tag:yaml.org,2002:seq", { create: () => [], @@ -142053,11 +142053,11 @@ var omapTag = defineSequenceTag("tag:yaml.org,2002:omap", { create: () => [], addItem: (container, item) => { if (Object.prototype.toString.call(item) !== "[object Object]") return "cannot resolve an ordered map item"; - const object = item; - const itemKeys = Object.keys(object); + const object2 = item; + const itemKeys = Object.keys(object2); if (itemKeys.length !== 1) return "cannot resolve an ordered map item"; for (const existing of container) if (Object.prototype.hasOwnProperty.call(existing, itemKeys[0])) return "cannot resolve an ordered map item"; - container.push(object); + container.push(object2); return ""; } }); @@ -142070,10 +142070,10 @@ var pairsTag = defineSequenceTag("tag:yaml.org,2002:pairs", { return ""; } if (Object.prototype.toString.call(item) !== "[object Object]") return "cannot resolve a pairs item"; - const object = item; - const keys = Object.keys(object); + const object2 = item; + const keys = Object.keys(object2); if (keys.length !== 1) return "cannot resolve a pairs item"; - container.push([keys[0], object[keys[0]]]); + container.push([keys[0], object2[keys[0]]]); return ""; } }); @@ -142082,9 +142082,9 @@ function isPlainObject3(data) { const prototype = Object.getPrototypeOf(data); return prototype === null || prototype === Object.prototype; } -function pick(object, keys) { +function pick(object2, keys) { const result = {}; - for (const key of keys) if (object[key] !== void 0) result[key] = object[key]; + for (const key of keys) if (object2[key] !== void 0) result[key] = object2[key]; return result; } var mapTag = defineMappingTag("tag:yaml.org,2002:map", { @@ -142270,12 +142270,12 @@ var realMapTag = defineMappingTag("tag:yaml.org,2002:map", { }); function normalizeKey(key) { if (Array.isArray(key)) { - const array = Array.prototype.slice.call(key); - for (let index2 = 0; index2 < array.length; index2++) { - if (Array.isArray(array[index2])) return null; - if (typeof array[index2] === "object" && Object.prototype.toString.call(array[index2]) === "[object Object]") array[index2] = "[object Object]"; + const array2 = Array.prototype.slice.call(key); + for (let index2 = 0; index2 < array2.length; index2++) { + if (Array.isArray(array2[index2])) return null; + if (typeof array2[index2] === "object" && Object.prototype.toString.call(array2[index2]) === "[object Object]") array2[index2] = "[object Object]"; } - return String(array); + return String(array2); } if (typeof key === "object" && Object.prototype.toString.call(key) === "[object Object]") return "[object Object]"; return String(key); @@ -143784,12 +143784,12 @@ function buildRepresentTypes(schema) { })) ]; } -function matchTag(state, object) { +function matchTag(state, object2) { for (let index2 = 0, length = state.representTypes.length; index2 < length; index2 += 1) { const { tag, implicitTag } = state.representTypes[index2]; - if (tag.identify && tag.identify(object)) { + if (tag.identify && tag.identify(object2)) { let tagName; - if (tag.matchByTagPrefix && tag.representTagName) tagName = tag.representTagName(object); + if (tag.matchByTagPrefix && tag.representTagName) tagName = tag.representTagName(object2); else tagName = tag.tagName; return { tag, @@ -143800,9 +143800,9 @@ function matchTag(state, object) { } return null; } -function build(state, object) { - if (!state.noRefs && object !== null && typeof object === "object") { - const existing = state.refs.get(object); +function build(state, object2) { + if (!state.noRefs && object2 !== null && typeof object2 === "object") { + const existing = state.refs.get(object2); if (existing) { if (existing.anchor === void 0) existing.anchor = `ref_${state.refCounter++}`; return { @@ -143813,11 +143813,11 @@ function build(state, object) { }; } } - const matched = matchTag(state, object); + const matched = matchTag(state, object2); if (!matched) { - if (object === void 0) return INVALID; + if (object2 === void 0) return INVALID; if (state.skipInvalid) return INVALID; - throw new YAMLException(`unacceptable kind of an object to dump ${Object.prototype.toString.call(object)}`); + throw new YAMLException(`unacceptable kind of an object to dump ${Object.prototype.toString.call(object2)}`); } const { tag, tagName, implicitTag } = matched; const nodeTagName = implicitTag ? tagName : tagNameShort(tagName); @@ -143828,11 +143828,11 @@ function build(state, object) { kind: "scalar", tag: nodeTagName, style: style2, - value: tag.represent(object) + value: tag.represent(object2) }; } if (tag.nodeKind === "sequence") { - const container = tag.represent(object); + const container = tag.represent(object2); const style2 = new Style(); style2.tagged = !implicitTag; const node2 = { @@ -143841,7 +143841,7 @@ function build(state, object) { style: style2, items: [] }; - if (!state.noRefs) state.refs.set(object, node2); + if (!state.noRefs) state.refs.set(object2, node2); for (let index2 = 0, length = container.length; index2 < length; index2 += 1) { let item = build(state, container[index2]); if (item === INVALID && container[index2] === void 0) item = build(state, null); @@ -143850,7 +143850,7 @@ function build(state, object) { } return node2; } - const map = tag.represent(object); + const map = tag.represent(object2); const style = new Style(); style.tagged = !implicitTag; const node = { @@ -143859,7 +143859,7 @@ function build(state, object) { style, items: [] }; - if (!state.noRefs) state.refs.set(object, node); + if (!state.noRefs) state.refs.set(object2, node); for (const [objectKey, objectValue] of map) { const key = build(state, objectKey); if (key === INVALID) continue; @@ -144509,6 +144509,22 @@ var string = { validate: isString, required: true }; +function array(validator) { + return { + validate: (val) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }, + required: true + }; +} +function object(schema) { + return { + validate: (val) => { + return isObject(val) && validateSchema(schema, val); + }, + required: true + }; +} function optionalOrNull(validator) { return { validate: (val) => { @@ -144517,6 +144533,14 @@ function optionalOrNull(validator) { required: false }; } +function optional(validator) { + return { + validate: (val) => { + return val === void 0 || validator.validate(val); + }, + required: false + }; +} function validateSchema(schema, obj) { const result = checkSchema(schema, obj, { failFast: true }); return result.valid; @@ -145132,28 +145156,28 @@ async function isBinaryAccessible(binary, logger) { return false; } } -async function asyncFilter(array, predicate) { - const results = await Promise.all(array.map(predicate)); - return array.filter((_2, index2) => results[index2]); +async function asyncFilter(array2, predicate) { + const results = await Promise.all(array2.map(predicate)); + return array2.filter((_2, index2) => results[index2]); } -async function asyncSome(array, predicate) { - const results = await Promise.all(array.map(predicate)); +async function asyncSome(array2, predicate) { + const results = await Promise.all(array2.map(predicate)); return results.some((result) => result); } function isDefined2(value) { return value !== void 0 && value !== null; } -function unsafeEntriesInvariant(object) { - return Object.entries(object).filter( +function unsafeEntriesInvariant(object2) { + return Object.entries(object2).filter( ([_2, val]) => val !== void 0 ); } -function joinAtMost(array, separator, limit) { - if (limit > 0 && array.length > limit) { - array = array.slice(0, limit); - array.push("..."); +function joinAtMost(array2, separator, limit) { + if (limit > 0 && array2.length > limit) { + array2 = array2.slice(0, limit); + array2.push("..."); } - return array.join(separator); + return array2.join(separator); } var Success = class { constructor(value) { @@ -147748,10 +147772,32 @@ function isKnownPropertyName(name) { } // src/config/db-config.ts +var ORG_SCHEMA = { + /** An array of model pack names. */ + "model-packs": optional(array(string)) +}; +var DEFAULT_SETUP_SCHEMA = { + org: optional(object(ORG_SCHEMA)) +}; +var DEFAULT_SETUP_CONFIG_SCHEMA = { + "threat-models": optional(array(string)), + "default-setup": optional( + object(DEFAULT_SETUP_SCHEMA) + ) +}; function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile) { logger.debug( "Combining configuration files from 'config' and 'config-file' inputs" ); + const schemaCheckResult = checkSchema( + DEFAULT_SETUP_CONFIG_SCHEMA, + fromConfigInput + ); + if (schemaCheckResult.unknownKeys.length > 0) { + logger.warning( + `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}` + ); + } const threatModels = new Set(fromConfigInput["threat-models"] || []); for (const configFileThreatModel of fromConfigFile["threat-models"] || []) { threatModels.add(configFileThreatModel); diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 5783e3a241..0b4de3b436 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -561,3 +561,24 @@ test("mergeDefaultSetupAndUserConfigs - keeps other properties from user-supplie t.deepEqual(result, configFile); }); + +test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys from Default Setup", async (t) => { + const logger = new RecordingLogger(); + const configFile: dbConfig.UserConfig = { + "query-filters": [{ exclude: { a: "b" } }], + "paths-ignore": ["path"], + }; + + const result = dbConfig.mergeDefaultSetupAndUserConfigs( + logger, + { + "paths-ignore": ["other-path"], + }, + configFile, + ); + + t.deepEqual(result, configFile); + checkExpectedLogMessages(t, logger.messages, [ + "Unrecognised keys in Default Setup configuration: paths-ignore", + ]); +}); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 612a26a24b..51cfe63b06 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -9,6 +9,7 @@ import { RepositoryProperties, RepositoryPropertyName, } from "../feature-flags/properties"; +import * as json from "../json"; import { Language } from "../languages"; import { Logger } from "../logging"; import { cloneObject, ConfigurationError, prettyPrintPack } from "../util"; @@ -28,13 +29,20 @@ export interface QuerySpec { uses: string; } +const ORG_SCHEMA = { + /** An array of model pack names. */ + "model-packs": json.optional(json.array(json.string)), +} as const satisfies json.Schema; + /** Not intended to be provided directly by a user. */ -export interface DefaultSetupConfig { - org?: { - /** An array of model pack names. */ - "model-packs"?: string[]; - }; -} +export type OrgType = json.FromSchema; + +const DEFAULT_SETUP_SCHEMA = { + org: json.optional(json.object(ORG_SCHEMA)), +} as const satisfies json.Schema; + +/** Not intended to be provided directly by a user. */ +export type DefaultSetupConfig = json.FromSchema; /** * Format of the config file supplied by the user. @@ -65,6 +73,14 @@ export interface UserConfig { "default-setup"?: DefaultSetupConfig; } +/** A subset of the `UserConfig` schema that is used by Default Setup. */ +const DEFAULT_SETUP_CONFIG_SCHEMA = { + "threat-models": json.optional(json.array(json.string)), + "default-setup": json.optional( + json.object(DEFAULT_SETUP_SCHEMA), + ), +} as const satisfies json.Schema; + /** * 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 @@ -84,6 +100,20 @@ export function mergeDefaultSetupAndUserConfigs( "Combining configuration files from 'config' and 'config-file' inputs", ); + // Check for unexpected keys in the configuration from the `config` input + // that was provided by Default Setup. This should only contain the keys + // we would expect to receive from Default Setup. + const schemaCheckResult = json.checkSchema( + DEFAULT_SETUP_CONFIG_SCHEMA, + fromConfigInput as json.UnvalidatedObject, + ); + + if (schemaCheckResult.unknownKeys.length > 0) { + logger.warning( + `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`, + ); + } + // Combine all specified threat models from both sources. const threatModels = new Set(fromConfigInput["threat-models"] || []); for (const configFileThreatModel of fromConfigFile["threat-models"] || []) { From 7c961fa35788ac1b3c29a1bf8f5ad097b2259697 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:14:19 +0100 Subject: [PATCH 27/35] Add diagnostic for unrecognised keys --- lib/entry-points.js | 214 +++++++++++++++++++++------------------- src/config/db-config.ts | 14 +++ 2 files changed, 126 insertions(+), 102 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 61da930264..ccfb1da9f6 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -147603,10 +147603,109 @@ function getDependencyCachingEnabled() { } // src/config/db-config.ts -var path6 = __toESM(require("path")); +var path7 = __toESM(require("path")); var jsonschema = __toESM(require_lib2()); var semver5 = __toESM(require_semver2()); +// src/diagnostics.ts +var import_fs = require("fs"); +var import_path = __toESM(require("path")); +var unwrittenDiagnostics = []; +var unwrittenDefaultLanguageDiagnostics = []; +var diagnosticCounter = 0; +function makeDiagnostic(id, name, data = void 0) { + return { + ...data, + timestamp: data?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(), + source: { ...data?.source, id, name } + }; +} +function addDiagnostic(config, language, diagnostic) { + const logger = getActionsLogger(); + const databasePath = language ? getCodeQLDatabasePath(config, language) : config.dbLocation; + if ((0, import_fs.existsSync)(databasePath)) { + writeDiagnostic(config, language, diagnostic); + } else { + logger.debug( + `Writing a diagnostic for ${language}, but the database at ${databasePath} does not exist yet.` + ); + unwrittenDiagnostics.push({ diagnostic, language }); + } +} +function addNoLanguageDiagnostic(config, diagnostic) { + if (config !== void 0) { + addDiagnostic( + config, + // Arbitrarily choose the first language. We could also choose all languages, but that + // increases the risk of misinterpreting the data. + config.languages[0], + diagnostic + ); + } else { + unwrittenDefaultLanguageDiagnostics.push(diagnostic); + } +} +function writeDiagnostic(config, language, diagnostic) { + const logger = getActionsLogger(); + const databasePath = language ? getCodeQLDatabasePath(config, language) : config.dbLocation; + const diagnosticsPath = import_path.default.resolve( + databasePath, + "diagnostic", + "codeql-action" + ); + try { + (0, import_fs.mkdirSync)(diagnosticsPath, { recursive: true }); + const uniqueSuffix = (diagnosticCounter++).toString(); + const sanitizedTimestamp = diagnostic.timestamp.replace( + /[^a-zA-Z0-9.-]/g, + "" + ); + const jsonPath = import_path.default.resolve( + diagnosticsPath, + `codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json` + ); + (0, import_fs.writeFileSync)(jsonPath, JSON.stringify(diagnostic)); + } catch (err) { + logger.warning(`Unable to write diagnostic message to database: ${err}`); + logger.debug(JSON.stringify(diagnostic)); + } +} +function logUnwrittenDiagnostics() { + const logger = getActionsLogger(); + const num = unwrittenDiagnostics.length; + if (num > 0) { + logger.warning( + `${num} diagnostic(s) could not be written to the database and will not appear on the Tool Status Page.` + ); + for (const unwritten of unwrittenDiagnostics) { + logger.debug(JSON.stringify(unwritten.diagnostic)); + } + } +} +function flushDiagnostics(config) { + const logger = getActionsLogger(); + const diagnosticsCount = unwrittenDiagnostics.length + unwrittenDefaultLanguageDiagnostics.length; + logger.debug(`Writing ${diagnosticsCount} diagnostic(s) to database.`); + for (const unwritten of unwrittenDiagnostics) { + writeDiagnostic(config, unwritten.language, unwritten.diagnostic); + } + for (const unwritten of unwrittenDefaultLanguageDiagnostics) { + addNoLanguageDiagnostic(config, unwritten); + } + unwrittenDiagnostics = []; + unwrittenDefaultLanguageDiagnostics = []; +} +function makeTelemetryDiagnostic(id, name, attributes) { + return makeDiagnostic(id, name, { + attributes, + visibility: { + cliSummaryTable: false, + statusPage: false, + telemetry: true + } + }); +} + // src/error-messages.ts var PACKS_PROPERTY = "packs"; function getConfigFileOutsideWorkspaceErrorMessage(configFile) { @@ -147797,6 +147896,16 @@ function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile logger.warning( `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}` ); + addNoLanguageDiagnostic( + void 0, + makeTelemetryDiagnostic( + "codeql-action/unrecognised-default-setup-config-keys", + "Unrecognised Default Setup configuration keys", + { + unrecognisedKeys: schemaCheckResult.unknownKeys + } + ) + ); } const threatModels = new Set(fromConfigInput["threat-models"] || []); for (const configFileThreatModel of fromConfigFile["threat-models"] || []) { @@ -147857,11 +147966,11 @@ function parsePacksSpecification(packStr) { throw new ConfigurationError(getPacksStrInvalid(packStr)); } } - if (packPath && (path6.isAbsolute(packPath) || // Permit using "/" instead of "\" on Windows + if (packPath && (path7.isAbsolute(packPath) || // Permit using "/" instead of "\" on Windows // Use `x.split(y).join(z)` as a polyfill for `x.replaceAll(y, z)` since // if we used a regex we'd need to escape the path separator on Windows // which seems more awkward. - path6.normalize(packPath).split(path6.sep).join("/") !== packPath.split(path6.sep).join("/"))) { + path7.normalize(packPath).split(path7.sep).join("/") !== packPath.split(path7.sep).join("/"))) { throw new ConfigurationError(getPacksStrInvalid(packStr)); } if (!packPath && pathStart) { @@ -148183,105 +148292,6 @@ async function getRemoteConfig(actionState, configFile, apiDetails) { ); } -// src/diagnostics.ts -var import_fs = require("fs"); -var import_path = __toESM(require("path")); -var unwrittenDiagnostics = []; -var unwrittenDefaultLanguageDiagnostics = []; -var diagnosticCounter = 0; -function makeDiagnostic(id, name, data = void 0) { - return { - ...data, - timestamp: data?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(), - source: { ...data?.source, id, name } - }; -} -function addDiagnostic(config, language, diagnostic) { - const logger = getActionsLogger(); - const databasePath = language ? getCodeQLDatabasePath(config, language) : config.dbLocation; - if ((0, import_fs.existsSync)(databasePath)) { - writeDiagnostic(config, language, diagnostic); - } else { - logger.debug( - `Writing a diagnostic for ${language}, but the database at ${databasePath} does not exist yet.` - ); - unwrittenDiagnostics.push({ diagnostic, language }); - } -} -function addNoLanguageDiagnostic(config, diagnostic) { - if (config !== void 0) { - addDiagnostic( - config, - // Arbitrarily choose the first language. We could also choose all languages, but that - // increases the risk of misinterpreting the data. - config.languages[0], - diagnostic - ); - } else { - unwrittenDefaultLanguageDiagnostics.push(diagnostic); - } -} -function writeDiagnostic(config, language, diagnostic) { - const logger = getActionsLogger(); - const databasePath = language ? getCodeQLDatabasePath(config, language) : config.dbLocation; - const diagnosticsPath = import_path.default.resolve( - databasePath, - "diagnostic", - "codeql-action" - ); - try { - (0, import_fs.mkdirSync)(diagnosticsPath, { recursive: true }); - const uniqueSuffix = (diagnosticCounter++).toString(); - const sanitizedTimestamp = diagnostic.timestamp.replace( - /[^a-zA-Z0-9.-]/g, - "" - ); - const jsonPath = import_path.default.resolve( - diagnosticsPath, - `codeql-action-${sanitizedTimestamp}-${uniqueSuffix}.json` - ); - (0, import_fs.writeFileSync)(jsonPath, JSON.stringify(diagnostic)); - } catch (err) { - logger.warning(`Unable to write diagnostic message to database: ${err}`); - logger.debug(JSON.stringify(diagnostic)); - } -} -function logUnwrittenDiagnostics() { - const logger = getActionsLogger(); - const num = unwrittenDiagnostics.length; - if (num > 0) { - logger.warning( - `${num} diagnostic(s) could not be written to the database and will not appear on the Tool Status Page.` - ); - for (const unwritten of unwrittenDiagnostics) { - logger.debug(JSON.stringify(unwritten.diagnostic)); - } - } -} -function flushDiagnostics(config) { - const logger = getActionsLogger(); - const diagnosticsCount = unwrittenDiagnostics.length + unwrittenDefaultLanguageDiagnostics.length; - logger.debug(`Writing ${diagnosticsCount} diagnostic(s) to database.`); - for (const unwritten of unwrittenDiagnostics) { - writeDiagnostic(config, unwritten.language, unwritten.diagnostic); - } - for (const unwritten of unwrittenDefaultLanguageDiagnostics) { - addNoLanguageDiagnostic(config, unwritten); - } - unwrittenDiagnostics = []; - unwrittenDefaultLanguageDiagnostics = []; -} -function makeTelemetryDiagnostic(id, name, attributes) { - return makeDiagnostic(id, name, { - attributes, - visibility: { - cliSummaryTable: false, - statusPage: false, - telemetry: true - } - }); -} - // src/diff-informed-analysis-utils.ts var fs6 = __toESM(require("fs")); async function getDiffInformedAnalysisBranches(codeql, features, logger) { diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 51cfe63b06..c88c4ad097 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -4,6 +4,10 @@ import * as yaml from "js-yaml"; import * as jsonschema from "jsonschema"; import * as semver from "semver"; +import { + addNoLanguageDiagnostic, + makeTelemetryDiagnostic, +} from "../diagnostics"; import * as errorMessages from "../error-messages"; import { RepositoryProperties, @@ -112,6 +116,16 @@ export function mergeDefaultSetupAndUserConfigs( logger.warning( `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`, ); + addNoLanguageDiagnostic( + undefined, + makeTelemetryDiagnostic( + "codeql-action/unrecognised-default-setup-config-keys", + "Unrecognised Default Setup configuration keys", + { + unrecognisedKeys: schemaCheckResult.unknownKeys, + }, + ), + ); } // Combine all specified threat models from both sources. From cee0056fc0174e7e545ffc4b48c8bb2ec138657f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 00:43:05 +0100 Subject: [PATCH 28/35] Allow nested checking --- lib/entry-points.js | 124 +++++++++++++++++++++++++++++------------ src/json/index.test.ts | 18 +++++- src/json/index.ts | 90 +++++++++++++++++++++++++----- 3 files changed, 178 insertions(+), 54 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ccfb1da9f6..648e9c50a2 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -29647,13 +29647,13 @@ var require_helpers = __commonJS({ exports2.encodePath = function encodePointer(a) { return a.map(pathEncoder).join(""); }; - exports2.getDecimalPlaces = function getDecimalPlaces(number) { + exports2.getDecimalPlaces = function getDecimalPlaces(number2) { var decimalPlaces = 0; - if (isNaN(number)) return decimalPlaces; - if (typeof number !== "number") { - number = Number(number); + if (isNaN(number2)) return decimalPlaces; + if (typeof number2 !== "number") { + number2 = Number(number2); } - var parts = number.toString().split("e"); + var parts = number2.toString().split("e"); if (parts.length === 2) { if (parts[1][0] !== "-") { return decimalPlaces; @@ -78733,9 +78733,9 @@ var require_enum_object = __commonJS({ if (!isEnumObject(enumObject)) throw new Error("not a typescript enum object"); let values = []; - for (let [name, number] of Object.entries(enumObject)) - if (typeof number == "number") - values.push({ name, number }); + for (let [name, number2] of Object.entries(enumObject)) + if (typeof number2 == "number") + values.push({ name, number: number2 }); return values; } exports2.listEnumValues = listEnumValues; @@ -92751,10 +92751,10 @@ var require_util12 = __commonJS({ return arg == null; } exports2.isNullOrUndefined = isNullOrUndefined; - function isNumber(arg) { + function isNumber2(arg) { return typeof arg === "number"; } - exports2.isNumber = isNumber; + exports2.isNumber = isNumber2; function isString3(arg) { return typeof arg === "string"; } @@ -101524,24 +101524,24 @@ var require_operators = __commonJS({ } }.call(this); } - function toIntegerOrInfinity(number) { - number = Number2(number); - if (NumberIsNaN(number)) { + function toIntegerOrInfinity(number2) { + number2 = Number2(number2); + if (NumberIsNaN(number2)) { return 0; } - if (number < 0) { - throw new ERR_OUT_OF_RANGE("number", ">= 0", number); + if (number2 < 0) { + throw new ERR_OUT_OF_RANGE("number", ">= 0", number2); } - return number; + return number2; } - function drop(number, options = void 0) { + function drop(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* drop2() { var _options$signal5; if (options !== null && options !== void 0 && (_options$signal5 = options.signal) !== null && _options$signal5 !== void 0 && _options$signal5.aborted) { @@ -101552,20 +101552,20 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal6 = options.signal) !== null && _options$signal6 !== void 0 && _options$signal6.aborted) { throw new AbortError(); } - if (number-- <= 0) { + if (number2-- <= 0) { yield val; } } }.call(this); } - function take(number, options = void 0) { + function take(number2, options = void 0) { if (options != null) { validateObject(options, "options"); } if ((options === null || options === void 0 ? void 0 : options.signal) != null) { validateAbortSignal(options.signal, "options.signal"); } - number = toIntegerOrInfinity(number); + number2 = toIntegerOrInfinity(number2); return async function* take2() { var _options$signal7; if (options !== null && options !== void 0 && (_options$signal7 = options.signal) !== null && _options$signal7 !== void 0 && _options$signal7.aborted) { @@ -101576,10 +101576,10 @@ var require_operators = __commonJS({ if (options !== null && options !== void 0 && (_options$signal8 = options.signal) !== null && _options$signal8 !== void 0 && _options$signal8.aborted) { throw new AbortError(); } - if (number-- > 0) { + if (number2-- > 0) { yield val; } - if (number <= 0) { + if (number2 <= 0) { return; } } @@ -124893,8 +124893,8 @@ var require_util16 = __commonJS({ parts.push(format.substring(last)); return parts.join(""); }; - util3.formatNumber = function(number, decimals, dec_point, thousands_sep) { - var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; + util3.formatNumber = function(number2, decimals, dec_point, thousands_sep) { + var n = number2, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; var d = dec_point === void 0 ? "," : dec_point; var t = thousands_sep === void 0 ? "." : thousands_sep, s = n < 0 ? "-" : ""; var i = parseInt(n = Math.abs(+n || 0).toFixed(c), 10) + ""; @@ -144502,17 +144502,47 @@ function isArray(value) { function isString(value) { return typeof value === "string"; } +function isNumber(value) { + return typeof value === "number"; +} function isStringOrUndefined(value) { return value === void 0 || isString(value); } -var string = { - validate: isString, - required: true -}; +function defaultCheck(validate) { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} +function makeValidator(validate, required = true) { + return { + validate, + check: defaultCheck(validate), + required + }; +} +var string = makeValidator(isString); +var number = makeValidator(isNumber); function array(validator) { + const validate = (val) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }; return { - validate: (val) => { - return isArray(val) && val.every((e) => validator.validate(e)); + validate, + check: (val, path29) => { + const result = { valid: true, unknownKeys: [] }; + if (!isArray(val)) { + result.valid = false; + return result; + } + let index2 = 0; + for (const e of val) { + const eResult = validator.check(e, `${path29}[${index2}].`); + result.unknownKeys.push(...eResult.unknownKeys); + index2++; + if (!eResult.valid) { + result.valid = false; + continue; + } + } + return result; }, required: true }; @@ -144522,6 +144552,12 @@ function object(schema) { validate: (val) => { return isObject(val) && validateSchema(schema, val); }, + check: (val, path29) => { + if (!isObject(val)) { + return { valid: false, unknownKeys: [] }; + } + return checkSchema(schema, val, {}, path29); + }, required: true }; } @@ -144530,6 +144566,12 @@ function optionalOrNull(validator) { validate: (val) => { return val === void 0 || val === null || validator.validate(val); }, + check: (val, path29) => { + if (val === void 0 || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path29); + }, required: false }; } @@ -144538,6 +144580,12 @@ function optional(validator) { validate: (val) => { return val === void 0 || validator.validate(val); }, + check: (val, path29) => { + if (val === void 0) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path29); + }, required: false }; } @@ -144564,12 +144612,16 @@ function checkSchema(schema, obj, options = {}, path29 = "") { } continue; } - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; - if (options.failFast) { - return result; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path29}${key}.`); + result.unknownKeys.push(...checkResult.unknownKeys); + if (!checkResult.valid) { + result.valid = false; + if (options.failFast) { + return result; + } + continue; } - continue; } inputKeys.delete(key); } diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 71f8000031..47956c9387 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -103,12 +103,24 @@ test("validateSchema - validates objects", async (t) => { t.false(json.validateSchema(objectSchema, { objectKey: 123 })); }); +const checkSchemaTestSchema = { + rootKey: json.object(objectSchema), +}; + test("validateSchema - checkSchema reports unknown keys", async (t) => { - const result = json.checkSchema(testSchema, { - requiredKey: "foo", + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: [], + }, + nestedExtraKey: "foo", + }, extraKey: "bar", }); t.true(result.valid); - t.deepEqual(result.unknownKeys, ["extraKey"]); + t.deepEqual( + result.unknownKeys.sort(), + ["extraKey", "rootKey.nestedExtraKey"].sort(), + ); }); diff --git a/src/json/index.ts b/src/json/index.ts index 325689d33e..032952ad58 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,29 +48,65 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; + check: (val: unknown, path: string) => CheckSchemaResult; required: boolean; }; +function defaultCheck( + validate: (val: unknown) => val is any, +): (arg: unknown) => CheckSchemaResult { + return (arg) => ({ unknownKeys: [], valid: validate(arg) }); +} + +function makeValidator( + validate: (arg: unknown) => arg is T, + required: boolean = true, +) { + return { + validate, + check: defaultCheck(validate), + required, + } as const satisfies Validator; +} + /** Extracts `T` from `Validator`. */ export type UnwrapValidator = V extends Validator ? A : never; /** A validator for string fields in schemas. */ -export const string = { - validate: isString, - required: true, -} as const satisfies Validator; +export const string = makeValidator(isString); /** A validator for number fields in schemas. */ -export const number = { - validate: isNumber, - required: true, -} as const satisfies Validator; +export const number = makeValidator(isNumber); /** A validator for arrays. */ export function array(validator: Validator) { + const validate = (val: unknown) => { + return isArray(val) && val.every((e) => validator.validate(e)); + }; return { - validate: (val: unknown) => { - return isArray(val) && val.every((e) => validator.validate(e)); + validate, + check: (val: unknown, path: string) => { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + + if (!isArray(val)) { + result.valid = false; + return result; + } + + let index = 0; + for (const e of val) { + const eResult = validator.check(e, `${path}[${index}].`); + + result.unknownKeys.push(...eResult.unknownKeys); + index++; + + if (!eResult.valid) { + result.valid = false; + continue; + } + } + + return result; }, required: true, } as const satisfies Validator; @@ -85,6 +121,12 @@ export function object< validate: (val: unknown) => { return isObject(val) && validateSchema(schema, val); }, + check: (val, path) => { + if (!isObject(val)) { + return { valid: false, unknownKeys: [] }; + } + return checkSchema(schema, val, {}, path); + }, required: true, } as const satisfies Validator; } @@ -98,6 +140,12 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, + check: (val, path) => { + if (val === undefined || val === null) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -111,6 +159,12 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, + check: (val, path) => { + if (val === undefined) { + return { valid: true, unknownKeys: [] }; + } + return validator.check(val, path); + }, required: false, } as const satisfies Validator; } @@ -193,13 +247,19 @@ export function checkSchema( } // If the property is present, validate it. - if (hasKey && !validator.validate(obj[key])) { - result.valid = false; + if (hasKey) { + const checkResult = validator.check(obj[key], `${path}${key}.`); - if (options.failFast) { - return result; + result.unknownKeys.push(...checkResult.unknownKeys); + + if (!checkResult.valid) { + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } - continue; } // If we reach this point, the key has been successfully validated. From e4752e74e120235edeeb1f7ad4eef4b5c4d45b4b Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 13:10:25 +0100 Subject: [PATCH 29/35] Remove keys from unrecognised set as soon as found --- lib/entry-points.js | 2 +- src/json/index.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 648e9c50a2..1dd73bc08b 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144598,6 +144598,7 @@ function checkSchema(schema, obj, options = {}, path29 = "") { const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; + inputKeys.delete(key); if (validator.required && !hasKey) { result.valid = false; if (options.failFast) { @@ -144623,7 +144624,6 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } } - inputKeys.delete(key); } for (const remainingKey of inputKeys) { result.unknownKeys.push(`${path29}${remainingKey}`); diff --git a/src/json/index.ts b/src/json/index.ts index 032952ad58..b5c9650989 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -226,6 +226,9 @@ export function checkSchema( for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; + // Remove key from set of unrecognised keys. + inputKeys.delete(key); + // If the property is required, but absent, fail. if (validator.required && !hasKey) { result.valid = false; @@ -263,7 +266,6 @@ export function checkSchema( } // If we reach this point, the key has been successfully validated. - inputKeys.delete(key); } // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. From 8d8f054dfa427a739604040d7d0edc2f4c531af0 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 13:18:45 +0100 Subject: [PATCH 30/35] Add convenience functions to produce `CheckSchemaResult` values --- lib/entry-points.js | 22 +++++++++++++++++----- src/json/index.ts | 30 +++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 1dd73bc08b..47047bb3bd 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144527,7 +144527,7 @@ function array(validator) { return { validate, check: (val, path29) => { - const result = { valid: true, unknownKeys: [] }; + const result = successfulCheckSchema(); if (!isArray(val)) { result.valid = false; return result; @@ -144554,7 +144554,7 @@ function object(schema) { }, check: (val, path29) => { if (!isObject(val)) { - return { valid: false, unknownKeys: [] }; + return invalidCheckSchema(); } return checkSchema(schema, val, {}, path29); }, @@ -144568,7 +144568,7 @@ function optionalOrNull(validator) { }, check: (val, path29) => { if (val === void 0 || val === null) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path29); }, @@ -144582,7 +144582,7 @@ function optional(validator) { }, check: (val, path29) => { if (val === void 0) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path29); }, @@ -144593,8 +144593,20 @@ function validateSchema(schema, obj) { const result = checkSchema(schema, obj, { failFast: true }); return result.valid; } +function successfulCheckSchema() { + return { + valid: true, + unknownKeys: [] + }; +} +function invalidCheckSchema() { + return { + valid: false, + unknownKeys: [] + }; +} function checkSchema(schema, obj, options = {}, path29 = "") { - const result = { valid: true, unknownKeys: [] }; + const result = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; diff --git a/src/json/index.ts b/src/json/index.ts index b5c9650989..7bc23271e5 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -86,7 +86,7 @@ export function array(validator: Validator) { return { validate, check: (val: unknown, path: string) => { - const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const result: CheckSchemaResult = successfulCheckSchema(); if (!isArray(val)) { result.valid = false; @@ -123,7 +123,7 @@ export function object< }, check: (val, path) => { if (!isObject(val)) { - return { valid: false, unknownKeys: [] }; + return invalidCheckSchema(); } return checkSchema(schema, val, {}, path); }, @@ -142,7 +142,7 @@ export function optionalOrNull(validator: Validator) { }, check: (val, path) => { if (val === undefined || val === null) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path); }, @@ -161,7 +161,7 @@ export function optional(validator: Validator) { }, check: (val, path) => { if (val === undefined) { - return { valid: true, unknownKeys: [] }; + return successfulCheckSchema(); } return validator.check(val, path); }, @@ -214,13 +214,33 @@ export interface CheckSchemaResult { unknownKeys: string[]; } +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: true`. + */ +function successfulCheckSchema(): CheckSchemaResult { + return { + valid: true, + unknownKeys: [], + }; +} + +/** + * Convenience function to produce a `CheckSchemaResult` where `valid: false`. + */ +function invalidCheckSchema(): CheckSchemaResult { + return { + valid: false, + unknownKeys: [], + }; +} + export function checkSchema( schema: S, obj: UnvalidatedObject, options: CheckSchemaOptions = {}, path: string = "", ): CheckSchemaResult { - const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const result: CheckSchemaResult = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { From 512bf6f1a36c22759b5ccfd1c3cf2d998d5b331f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:21:41 +0100 Subject: [PATCH 31/35] Track invalid keys, and use more standard JSON path notation --- lib/entry-points.js | 27 +++++++++++++++++++++------ src/config/db-config.ts | 1 + src/json/index.test.ts | 18 +++++++++++++++++- src/json/index.ts | 34 ++++++++++++++++++++++++++++++---- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 47047bb3bd..1226858390 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144509,7 +144509,7 @@ function isStringOrUndefined(value) { return value === void 0 || isString(value); } function defaultCheck(validate) { - return (arg) => ({ unknownKeys: [], valid: validate(arg) }); + return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); } function makeValidator(validate, required = true) { return { @@ -144534,11 +144534,13 @@ function array(validator) { } let index2 = 0; for (const e of val) { - const eResult = validator.check(e, `${path29}[${index2}].`); + const elementPath = `${path29}[${index2}]`; + const eResult = validator.check(e, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index2++; if (!eResult.valid) { result.valid = false; + result.invalidKeys.push(elementPath); continue; } } @@ -144596,21 +144598,25 @@ function validateSchema(schema, obj) { function successfulCheckSchema() { return { valid: true, - unknownKeys: [] + unknownKeys: [], + invalidKeys: [] }; } function invalidCheckSchema() { return { valid: false, - unknownKeys: [] + unknownKeys: [], + invalidKeys: [] }; } function checkSchema(schema, obj, options = {}, path29 = "") { const result = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); + const invalidKeys = /* @__PURE__ */ new Set(); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; inputKeys.delete(key); + invalidKeys.add(key); if (validator.required && !hasKey) { result.valid = false; if (options.failFast) { @@ -144626,8 +144632,12 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } if (hasKey) { - const checkResult = validator.check(obj[key], `${path29}${key}.`); + const checkResult = validator.check(obj[key], `${path29}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); + result.invalidKeys.push(...checkResult.invalidKeys); + if (checkResult.invalidKeys.length > 0) { + invalidKeys.delete(key); + } if (!checkResult.valid) { result.valid = false; if (options.failFast) { @@ -144636,9 +144646,13 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } } + invalidKeys.delete(key); } for (const remainingKey of inputKeys) { - result.unknownKeys.push(`${path29}${remainingKey}`); + result.unknownKeys.push(`${path29}.${remainingKey}`); + } + for (const invalidKey of invalidKeys) { + result.invalidKeys.push(`${path29}.${invalidKey}`); } return result; } @@ -147966,6 +147980,7 @@ function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile "codeql-action/unrecognised-default-setup-config-keys", "Unrecognised Default Setup configuration keys", { + invalidKeys: schemaCheckResult.invalidKeys, unrecognisedKeys: schemaCheckResult.unknownKeys } ) diff --git a/src/config/db-config.ts b/src/config/db-config.ts index c88c4ad097..4aeb26425e 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -122,6 +122,7 @@ export function mergeDefaultSetupAndUserConfigs( "codeql-action/unrecognised-default-setup-config-keys", "Unrecognised Default Setup configuration keys", { + invalidKeys: schemaCheckResult.invalidKeys, unrecognisedKeys: schemaCheckResult.unknownKeys, }, ), diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 47956c9387..df682fd8b9 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -121,6 +121,22 @@ test("validateSchema - checkSchema reports unknown keys", async (t) => { t.true(result.valid); t.deepEqual( result.unknownKeys.sort(), - ["extraKey", "rootKey.nestedExtraKey"].sort(), + [".extraKey", ".rootKey.nestedExtraKey"].sort(), + ); +}); + +test("validateSchema - checkSchema reports invalid keys", async (t) => { + const result = json.checkSchema(checkSchemaTestSchema, { + rootKey: { + objectKey: { + arrayKey: ["foo"], + }, + }, + }); + + t.false(result.valid); + t.deepEqual( + result.invalidKeys.sort(), + [".rootKey.objectKey.arrayKey[0]"].sort(), ); }); diff --git a/src/json/index.ts b/src/json/index.ts index 7bc23271e5..a5c89f722e 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -55,7 +55,7 @@ export type Validator = { function defaultCheck( validate: (val: unknown) => val is any, ): (arg: unknown) => CheckSchemaResult { - return (arg) => ({ unknownKeys: [], valid: validate(arg) }); + return (arg) => ({ unknownKeys: [], invalidKeys: [], valid: validate(arg) }); } function makeValidator( @@ -88,20 +88,24 @@ export function array(validator: Validator) { check: (val: unknown, path: string) => { const result: CheckSchemaResult = successfulCheckSchema(); + // The value must be an array. if (!isArray(val)) { result.valid = false; return result; } + // Validate all elements of the array. let index = 0; for (const e of val) { - const eResult = validator.check(e, `${path}[${index}].`); + const elementPath = `${path}[${index}]`; + const eResult = validator.check(e, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index++; if (!eResult.valid) { result.valid = false; + result.invalidKeys.push(elementPath); continue; } } @@ -212,6 +216,8 @@ export interface CheckSchemaResult { valid: boolean; /** Unknown keys that were found during validation. */ unknownKeys: string[]; + /** Known keys that failed validation. */ + invalidKeys: string[]; } /** @@ -221,6 +227,7 @@ function successfulCheckSchema(): CheckSchemaResult { return { valid: true, unknownKeys: [], + invalidKeys: [], }; } @@ -231,6 +238,7 @@ function invalidCheckSchema(): CheckSchemaResult { return { valid: false, unknownKeys: [], + invalidKeys: [], }; } @@ -242,6 +250,7 @@ export function checkSchema( ): CheckSchemaResult { const result: CheckSchemaResult = successfulCheckSchema(); const inputKeys = new Set(Object.keys(obj)); + const invalidKeys = new Set(); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; @@ -249,6 +258,10 @@ export function checkSchema( // Remove key from set of unrecognised keys. inputKeys.delete(key); + // Add the key to the set of invalid keys. We remove it later once + // it passes validation. + invalidKeys.add(key); + // If the property is required, but absent, fail. if (validator.required && !hasKey) { result.valid = false; @@ -271,9 +284,16 @@ export function checkSchema( // If the property is present, validate it. if (hasKey) { - const checkResult = validator.check(obj[key], `${path}${key}.`); + const checkResult = validator.check(obj[key], `${path}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); + result.invalidKeys.push(...checkResult.invalidKeys); + + // If we have invalid keys from the validator, then that means that + // we have a more specific key than `key`. Remove `key` from the results. + if (checkResult.invalidKeys.length > 0) { + invalidKeys.delete(key); + } if (!checkResult.valid) { result.valid = false; @@ -286,11 +306,17 @@ export function checkSchema( } // If we reach this point, the key has been successfully validated. + invalidKeys.delete(key); } // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. for (const remainingKey of inputKeys) { - result.unknownKeys.push(`${path}${remainingKey}`); + result.unknownKeys.push(`${path}.${remainingKey}`); + } + + // If there are any remaining keys in `invalidKeys`, add them to the result. + for (const invalidKey of invalidKeys) { + result.invalidKeys.push(`${path}.${invalidKey}`); } return result; From 0b10dee3eedd74fb04bf7e96b85cc5851eb684be Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:24:33 +0100 Subject: [PATCH 32/35] Propagate `CheckSchemaOptions` to allow `failFast` to work as intended --- lib/entry-points.js | 21 ++++++++++++--------- src/json/index.ts | 29 +++++++++++++++++++---------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 1226858390..8ef11482f4 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144526,7 +144526,7 @@ function array(validator) { }; return { validate, - check: (val, path29) => { + check: (val, opts, path29) => { const result = successfulCheckSchema(); if (!isArray(val)) { result.valid = false; @@ -144535,12 +144535,15 @@ function array(validator) { let index2 = 0; for (const e of val) { const elementPath = `${path29}[${index2}]`; - const eResult = validator.check(e, `${elementPath}`); + const eResult = validator.check(e, opts, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index2++; if (!eResult.valid) { result.valid = false; result.invalidKeys.push(elementPath); + if (opts.failFast) { + return result; + } continue; } } @@ -144554,11 +144557,11 @@ function object(schema) { validate: (val) => { return isObject(val) && validateSchema(schema, val); }, - check: (val, path29) => { + check: (val, opts, path29) => { if (!isObject(val)) { return invalidCheckSchema(); } - return checkSchema(schema, val, {}, path29); + return checkSchema(schema, val, opts, path29); }, required: true }; @@ -144568,11 +144571,11 @@ function optionalOrNull(validator) { validate: (val) => { return val === void 0 || val === null || validator.validate(val); }, - check: (val, path29) => { + check: (val, opts, path29) => { if (val === void 0 || val === null) { return successfulCheckSchema(); } - return validator.check(val, path29); + return validator.check(val, opts, path29); }, required: false }; @@ -144582,11 +144585,11 @@ function optional(validator) { validate: (val) => { return val === void 0 || validator.validate(val); }, - check: (val, path29) => { + check: (val, opts, path29) => { if (val === void 0) { return successfulCheckSchema(); } - return validator.check(val, path29); + return validator.check(val, opts, path29); }, required: false }; @@ -144632,7 +144635,7 @@ function checkSchema(schema, obj, options = {}, path29 = "") { continue; } if (hasKey) { - const checkResult = validator.check(obj[key], `${path29}.${key}`); + const checkResult = validator.check(obj[key], options, `${path29}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); result.invalidKeys.push(...checkResult.invalidKeys); if (checkResult.invalidKeys.length > 0) { diff --git a/src/json/index.ts b/src/json/index.ts index a5c89f722e..590a1eb6b3 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -48,7 +48,11 @@ export function isStringOrUndefined( */ export type Validator = { validate: (val: unknown) => val is T; - check: (val: unknown, path: string) => CheckSchemaResult; + check: ( + val: unknown, + opts: CheckSchemaOptions, + path: string, + ) => CheckSchemaResult; required: boolean; }; @@ -85,7 +89,7 @@ export function array(validator: Validator) { }; return { validate, - check: (val: unknown, path: string) => { + check: (val: unknown, opts: CheckSchemaOptions, path: string) => { const result: CheckSchemaResult = successfulCheckSchema(); // The value must be an array. @@ -98,7 +102,7 @@ export function array(validator: Validator) { let index = 0; for (const e of val) { const elementPath = `${path}[${index}]`; - const eResult = validator.check(e, `${elementPath}`); + const eResult = validator.check(e, opts, `${elementPath}`); result.unknownKeys.push(...eResult.unknownKeys); index++; @@ -106,6 +110,11 @@ export function array(validator: Validator) { if (!eResult.valid) { result.valid = false; result.invalidKeys.push(elementPath); + + if (opts.failFast) { + return result; + } + continue; } } @@ -125,11 +134,11 @@ export function object< validate: (val: unknown) => { return isObject(val) && validateSchema(schema, val); }, - check: (val, path) => { + check: (val, opts, path) => { if (!isObject(val)) { return invalidCheckSchema(); } - return checkSchema(schema, val, {}, path); + return checkSchema(schema, val, opts, path); }, required: true, } as const satisfies Validator; @@ -144,11 +153,11 @@ export function optionalOrNull(validator: Validator) { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, - check: (val, path) => { + check: (val, opts, path) => { if (val === undefined || val === null) { return successfulCheckSchema(); } - return validator.check(val, path); + return validator.check(val, opts, path); }, required: false, } as const satisfies Validator; @@ -163,11 +172,11 @@ export function optional(validator: Validator) { validate: (val: unknown): val is T | undefined => { return val === undefined || validator.validate(val); }, - check: (val, path) => { + check: (val, opts, path) => { if (val === undefined) { return successfulCheckSchema(); } - return validator.check(val, path); + return validator.check(val, opts, path); }, required: false, } as const satisfies Validator; @@ -284,7 +293,7 @@ export function checkSchema( // If the property is present, validate it. if (hasKey) { - const checkResult = validator.check(obj[key], `${path}.${key}`); + const checkResult = validator.check(obj[key], options, `${path}.${key}`); result.unknownKeys.push(...checkResult.unknownKeys); result.invalidKeys.push(...checkResult.invalidKeys); From e26215c5d8b1aaaad54820b8123959764e97636a Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:25:15 +0100 Subject: [PATCH 33/35] fixup! Track invalid keys, and use more standard JSON path notation --- src/config/db-config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 0b4de3b436..04fdf3322a 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -579,6 +579,6 @@ test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys f t.deepEqual(result, configFile); checkExpectedLogMessages(t, logger.messages, [ - "Unrecognised keys in Default Setup configuration: paths-ignore", + "Unrecognised keys in Default Setup configuration: .paths-ignore", ]); }); From 26cbf9f504358df24f6739b3f4cc8bc9dd16da80 Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:32:40 +0100 Subject: [PATCH 34/35] Don't include nested keys that are unknown --- lib/entry-points.js | 8 ++++++-- src/config/db-config.test.ts | 20 ++++++++++++++++++-- src/config/db-config.ts | 8 ++++++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 8ef11482f4..987502ccaa 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148001,8 +148001,12 @@ function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile const result = { ...fromConfigFile }; delete result["threat-models"]; delete result["default-setup"]; - if (fromConfigInput["default-setup"]) { - result["default-setup"] = fromConfigInput["default-setup"]; + if (fromConfigInput["default-setup"]?.org?.["model-packs"]) { + result["default-setup"] = { + org: { + "model-packs": fromConfigInput["default-setup"].org["model-packs"] + } + }; } if (threatModels.size > 0) { result["threat-models"] = Array.from(threatModels); diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 04fdf3322a..4d16fc716f 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -572,13 +572,29 @@ test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys f const result = dbConfig.mergeDefaultSetupAndUserConfigs( logger, { + "default-setup": { + borg: [], + org: { + unknown: "foo", + "model-packs": [], + }, + } as unknown as dbConfig.DefaultSetupConfig, "paths-ignore": ["other-path"], }, configFile, ); - t.deepEqual(result, configFile); + t.deepEqual(result, { + ...configFile, + "default-setup": { org: { "model-packs": [] } }, + }); + + const expectedUnrecognisedKeys = [ + ".default-setup.org.unknown", + ".default-setup.borg", + ".paths-ignore", + ].join(", "); checkExpectedLogMessages(t, logger.messages, [ - "Unrecognised keys in Default Setup configuration: .paths-ignore", + `Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`, ]); }); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 4aeb26425e..ec447b04f8 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -150,8 +150,12 @@ export function mergeDefaultSetupAndUserConfigs( delete result["threat-models"]; delete result["default-setup"]; - if (fromConfigInput["default-setup"]) { - result["default-setup"] = fromConfigInput["default-setup"]; + if (fromConfigInput["default-setup"]?.org?.["model-packs"]) { + result["default-setup"] = { + org: { + "model-packs": fromConfigInput["default-setup"].org["model-packs"], + }, + }; } if (threatModels.size > 0) { result["threat-models"] = Array.from(threatModels); From f630f8fd043520cb9d4e0ccda16c4ed4b635683d Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Fri, 10 Jul 2026 14:39:08 +0100 Subject: [PATCH 35/35] Report invalid keys separately --- lib/entry-points.js | 16 +++++++++++++++- src/config/db-config.test.ts | 27 +++++++++++++++++++++++++++ src/config/db-config.ts | 17 ++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 987502ccaa..817eb59172 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -147973,6 +147973,21 @@ function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile DEFAULT_SETUP_CONFIG_SCHEMA, fromConfigInput ); + if (schemaCheckResult.invalidKeys.length > 0) { + logger.warning( + `Invalid keys in Default Setup configuration: ${schemaCheckResult.invalidKeys.join(", ")}` + ); + addNoLanguageDiagnostic( + void 0, + makeTelemetryDiagnostic( + "codeql-action/invalid-default-setup-config-keys", + "Invalid Default Setup configuration keys", + { + invalidKeys: schemaCheckResult.invalidKeys + } + ) + ); + } if (schemaCheckResult.unknownKeys.length > 0) { logger.warning( `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}` @@ -147983,7 +147998,6 @@ function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile "codeql-action/unrecognised-default-setup-config-keys", "Unrecognised Default Setup configuration keys", { - invalidKeys: schemaCheckResult.invalidKeys, unrecognisedKeys: schemaCheckResult.unknownKeys } ) diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 4d16fc716f..63d9d2ffec 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -598,3 +598,30 @@ test("mergeDefaultSetupAndUserConfigs - ignores, but warns about, unknown keys f `Unrecognised keys in Default Setup configuration: ${expectedUnrecognisedKeys}`, ]); }); + +test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Setup", async (t) => { + const logger = new RecordingLogger(); + const configFile: dbConfig.UserConfig = {}; + + const result = dbConfig.mergeDefaultSetupAndUserConfigs( + logger, + { + "default-setup": { + org: { + "model-packs": [123], + }, + } as unknown as dbConfig.DefaultSetupConfig, + }, + configFile, + ); + + t.deepEqual(result, { + ...configFile, + "default-setup": { org: { "model-packs": [123] } }, + }); + + const expectedInvalidKeys = [".default-setup.org.model-packs[0]"].join(", "); + checkExpectedLogMessages(t, logger.messages, [ + `Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`, + ]); +}); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index ec447b04f8..582cfe8afc 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -112,6 +112,22 @@ export function mergeDefaultSetupAndUserConfigs( fromConfigInput as json.UnvalidatedObject, ); + // Report any invalid or unrecognised keys. + if (schemaCheckResult.invalidKeys.length > 0) { + logger.warning( + `Invalid keys in Default Setup configuration: ${schemaCheckResult.invalidKeys.join(", ")}`, + ); + addNoLanguageDiagnostic( + undefined, + makeTelemetryDiagnostic( + "codeql-action/invalid-default-setup-config-keys", + "Invalid Default Setup configuration keys", + { + invalidKeys: schemaCheckResult.invalidKeys, + }, + ), + ); + } if (schemaCheckResult.unknownKeys.length > 0) { logger.warning( `Unrecognised keys in Default Setup configuration: ${schemaCheckResult.unknownKeys.join(", ")}`, @@ -122,7 +138,6 @@ export function mergeDefaultSetupAndUserConfigs( "codeql-action/unrecognised-default-setup-config-keys", "Unrecognised Default Setup configuration keys", { - invalidKeys: schemaCheckResult.invalidKeys, unrecognisedKeys: schemaCheckResult.unknownKeys, }, ),