Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c512861
Extend `UserConfig` type to be aware of Default Setup properties
mbg Jul 3, 2026
cd604f8
Refactor `determineUserConfig` out of `initConfig`
mbg Jul 3, 2026
a52a966
Add `mergeUserConfigs` with tests
mbg Jul 3, 2026
132634d
Add FF for configuration merging
mbg Jul 6, 2026
53a488c
Add JSDoc for `loadUserConfig`
mbg Jul 6, 2026
6860cc1
Move `validateConfig` FF query
mbg Jul 6, 2026
0188f39
Add unit tests for existing behaviour of `determineUserConfig`
mbg Jul 6, 2026
d1db924
Allow `env` input for relevant functions in `actions-util.ts`
mbg Jul 6, 2026
18d20b7
Allow setting environment variables
mbg Jul 6, 2026
3157774
Give `determineUserConfig` access to the environment
mbg Jul 6, 2026
42c0c6a
Allow merging Default Setup `config` with config file
mbg Jul 6, 2026
3707b44
Document that `inputs` might be mutated
mbg Jul 6, 2026
d149f93
Return `mergedConfig` instead of loading it again
mbg Jul 6, 2026
764f470
Test `inputs.configFile` mutation in tests
mbg Jul 6, 2026
9a06fa6
Set the required `workspacePath` input
mbg Jul 6, 2026
44d6b3b
Initialise `env` in `actions-util` when not provided
mbg Jul 6, 2026
4509fb3
Fix JSDoc for `determineUserConfig`
mbg Jul 6, 2026
8476401
Make lazy FF check less ambiguous
mbg Jul 6, 2026
8be707b
Merge remote-tracking branch 'origin/main' into mbg/config/merge
mbg Jul 6, 2026
7ccd988
Merge branch 'main' into mbg/config/merge
mbg Jul 8, 2026
06898d7
Rename `mergeUserConfigs`
mbg Jul 8, 2026
d2472aa
Merge remote-tracking branch 'origin/main' into mbg/config/merge
mbg Jul 9, 2026
6829d6c
Remove obsolete JSDoc parameter for `loadUserConfig`
mbg Jul 9, 2026
6973946
Check file on disk and mutated inputs
mbg Jul 9, 2026
dc2d916
Add `checkSchema` function
mbg Jul 9, 2026
1f91ec8
Add an `array` `Validator`
mbg Jul 9, 2026
98dbd66
Add an `object` `Validator`
mbg Jul 9, 2026
9573f05
Improve type inference for `object` and `validateSchema`
mbg Jul 9, 2026
847e7af
Log warning for unrecognised keys in Default Setup config
mbg Jul 9, 2026
7c961fa
Add diagnostic for unrecognised keys
mbg Jul 9, 2026
cee0056
Allow nested checking
mbg Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,100 changes: 648 additions & 452 deletions lib/entry-points.js

Large diffs are not rendered by default.

375 changes: 354 additions & 21 deletions src/config-utils.test.ts

Large diffs are not rendered by default.

140 changes: 118 additions & 22 deletions src/config-utils.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had/have a hard time understanding the difference between configInput and configFileInput. I think it would be clearer to refer to the config-file as a repository property. Or, make the distinction clearer somehow.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getActionVersion,
getOptionalInput,
isAnalyzingPullRequest,
isDefaultSetup,
isDynamicWorkflow,
} from "./actions-util";
import {
Expand All @@ -26,6 +27,7 @@ import {
calculateAugmentation,
ExcludeQueryFilter,
generateCodeScanningConfig,
mergeDefaultSetupAndUserConfigs,
parseUserConfig,
UserConfig,
} from "./config/db-config";
Expand Down Expand Up @@ -466,6 +468,16 @@ async function downloadCacheWithTime(
return { trapCaches, trapCacheDownloadTime };
}

/**
* Loads a CLI configuration file from `configFile`.
*
* @param actionState The Action state.
* @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.
* @returns The loaded configuration file, if successful.
*/
async function loadUserConfig(
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
configFile: string,
Expand Down Expand Up @@ -936,7 +948,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");
}

Expand Down Expand Up @@ -997,43 +1013,123 @@ 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.
* @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
* was specified.
*/
export async function initConfig(
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
export async function determineUserConfig(
action: ActionState<["Logger", "Env", "FeatureFlags"]>,
tempDir: string,
inputs: InitConfigInputs,
): Promise<Config> {
const { logger, features } = actionState;
const { tempDir } = inputs;
): Promise<UserConfig> {
const validateConfig = await action.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 `mergeDefaultSetupAndUserConfigs`,
// 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 function which enables us to determine whether the FF that allows us to
// merge supported configuration file properties is enabled. We only execute
// this lazily if the other checks pass.
const allowMergeConfigs = () =>
action.features.getValue(Feature.AllowMergeConfigFiles);

// Check whether we also have a `config-file` input and decide what to do.
if (
inputs.configFile &&
isDefaultSetup(action.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(
action.logger,
"`config` input",
inputs.configInput,
validateConfig,
);
const fromConfigFile = await loadUserConfig(
action,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
);

// Write the merged configuration to disk so that it can be loaded subsequently by
// the CLI or other CodeQL Action steps.
const mergedConfig = mergeDefaultSetupAndUserConfigs(
action.logger,
fromConfigInput,
fromConfigFile,
);
fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig));
action.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
// that the configuration file will be ignored.
if (inputs.configFile) {
action.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);
inputs.configFile = computedConfigPath;
action.logger.debug(
`Using config from action input: ${inputs.configFile}`,
);
}
inputs.configFile = userConfigFromActionPath(tempDir);
fs.writeFileSync(inputs.configFile, inputs.configInput);
logger.debug(`Using config from action input: ${inputs.configFile}`);
}

let userConfig: UserConfig = {};
// Load whatever configuration file we have, if any.
if (!inputs.configFile) {
logger.debug("No configuration file was provided");
action.logger.debug("No configuration file was provided");
return {};
} else {
logger.debug(`Using configuration file: ${inputs.configFile}`);
userConfig = await loadUserConfig(
actionState,
action.logger.debug(`Using configuration file: ${inputs.configFile}`);
return await loadUserConfig(
action,
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
);
}
}

/**
* 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(
actionState: ActionState<["Logger", "Env", "FeatureFlags"]>,
inputs: InitConfigInputs,
): Promise<Config> {
const { logger, features } = actionState;
const { tempDir } = inputs;

const userConfig = await determineUserConfig(actionState, tempDir, inputs);

const config = await initActionState(inputs, userConfig);

Expand Down Expand Up @@ -1190,7 +1286,7 @@ function isLocal(configPath: string): boolean {
return configPath.indexOf("@") === -1;
}

function getLocalConfig(
export function getLocalConfig(
logger: Logger,
configFile: string,
validateConfig: boolean,
Expand Down
94 changes: 94 additions & 0 deletions src/config/db-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getRecordingLogger,
LoggedMessage,
makeMacro,
RecordingLogger,
} from "../testing-utils";
import { ConfigurationError, prettyPrintPack } from "../util";

Expand Down Expand Up @@ -488,3 +489,96 @@ test("parseUserConfig - throws no ConfigurationError if validation should fail,
),
);
});

test("mergeDefaultSetupAndUserConfigs - combines threat models", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
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"]);
}
Comment thread
mbg marked this conversation as resolved.
});

test("mergeDefaultSetupAndUserConfigs - warns if user-supplied config contains default setup key", async (t) => {
const logger = new RecordingLogger();
const result = dbConfig.mergeDefaultSetupAndUserConfigs(
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("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.mergeDefaultSetupAndUserConfigs(
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("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.mergeDefaultSetupAndUserConfigs(
logger,
{},
configFile,
);

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",
]);
});
Loading
Loading