Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
e4752e7
Remove keys from unrecognised set as soon as found
mbg Jul 10, 2026
8d8f054
Add convenience functions to produce `CheckSchemaResult` values
mbg Jul 10, 2026
512bf6f
Track invalid keys, and use more standard JSON path notation
mbg Jul 10, 2026
0b10dee
Propagate `CheckSchemaOptions` to allow `failFast` to work as intended
mbg Jul 10, 2026
e26215c
fixup! Track invalid keys, and use more standard JSON path notation
mbg Jul 10, 2026
26cbf9f
Don't include nested keys that are unknown
mbg Jul 10, 2026
f630f8f
Report invalid keys separately
mbg Jul 10, 2026
66c15f4
Merge remote-tracking branch 'origin/main' into mbg/config/merge
mbg Jul 13, 2026
2773684
Update test for changed test API
mbg Jul 13, 2026
f181d70
Update `determineUserConfig` tests to use new framework
mbg Jul 13, 2026
7577328
Return function result from `passes` alongside assertion result
mbg Jul 13, 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,148 changes: 696 additions & 452 deletions lib/entry-points.js

Large diffs are not rendered by default.

312 changes: 291 additions & 21 deletions src/config-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
createTestConfig,
makeMacro,
initAllState,
callee,
} from "./testing-utils";
import {
GitHubVariant,
Expand Down Expand Up @@ -462,6 +463,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);
Expand All @@ -476,18 +495,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 = {
Expand All @@ -514,7 +521,7 @@ test.serial("load non-empty input", async (t) => {
});

const languagesInput = "javascript";
const configFilePath = createConfigFile(inputFileContents, tempDir);
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);

const state = initAllState();
const actualConfig = await configUtils.initConfig(
Expand All @@ -540,14 +547,12 @@ 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 inputFileContents = `
name: my config
queries:
- uses: ./foo_file`;
const configFilePath = createConfigFile(inputFileContents, tempDir);
const configFilePath = createConfigFile(
simpleConfigFileContents,
tempDir,
);

const configInput = `
name: my config
Expand Down Expand Up @@ -576,7 +581,7 @@ test.serial(
// Only JS, python packs will be ignored
const languagesInput = "javascript";

const state = initAllState();
const state = initAllState({ env: util.getEnv() });
const config = await configUtils.initConfig(
state,
createTestInitConfigInputs({
Expand Down Expand Up @@ -2259,3 +2264,268 @@ 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 target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withFeatures([])
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: undefined,
configFile: undefined,
workspacePath: tmpDir,
}),
);

// The returned configuration should be empty.
await target
// The fact that no configuration was provided should have been logged,
.logs(t, "No configuration file was provided")
// But not the messages for the two input sources
// or the warning about both inputs.
.notLogs(
t,
"Using config from action input:",
"Using configuration file:",
"Both a config file and config input were provided. Ignoring config file.",
)
.passes(t.deepEqual, {});
});
});

test("determineUserConfig - loads config file", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);

const inputs = createTestInitConfigInputs({
configInput: undefined,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);

await target
// The path of the input config file should have been logged,
.logs(t, `Using configuration file: ${configFilePath}`)
.notLogs(
t,
// The other two origin messages and the warning about both inputs should
// not have been logged.
"No configuration file was provided",
"Using config from action input:",
"Both a config file and config input were provided. Ignoring config file.",
)
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});

// The `configFile` input should not have changed.
t.is(inputs.configFile, configFilePath);
});
});

test("determineUserConfig - loads config input", async (t) => {
await withTmpDir(async (tmpDir) => {
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);

const inputs = createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: undefined,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);

await target
// The input source and path of the generated config file should have been logged.
.logs(
t,
"Using config from action input:",
`Using configuration file: ${expectedConfigPath}`,
)
// The message about no configuration input and
// the warning about both inputs should not have been logged.
.notLogs(
t,
"No configuration file was provided",
"Both a config file and config input were provided. Ignoring config file.",
)
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});

// The `configFile` input should have been mutated to the generated path.
t.is(inputs.configFile, expectedConfigPath);
});
});

test("determineUserConfig - ignores config file input when both specified", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);

const inputs = createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withArgs(tmpDir, inputs);

await target
// The path of the generated config file and
// the warning about both inputs should have been logged.
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
// The loaded configuration should match `simpleConfigFileContents`.
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});

// The `configFile` input should have been mutated to the generated path.
t.is(inputs.configFile, expectedConfigPath);
});
});

/** 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 configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);

const inputs = createTestInitConfigInputs({
configInput: defaultSetupConfigInput,
configFile: configFilePath,
workspacePath: tmpDir,
});
const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
.withFeatures([Feature.AllowMergeConfigFiles])
.withArgs(tmpDir, inputs);

// The loaded configuration should match the result of merging
// `defaultSetupConfigInput` and `simpleConfigFileContents`.
Comment thread
mbg marked this conversation as resolved.
const expectedConfig = {
name: "my config",
queries: [{ uses: "./foo_file" }],
"threat-models": ["local", "remote"],
"default-setup": {
org: {
"model-packs": ["foo", "bar"],
},
},
} satisfies UserConfig;

await target
.logs(
t,
`Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`,
)
.notLogs(
t,
`Using configuration file: ${expectedConfigPath}`,
"No configuration file was provided",
`Using config from action input: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.passes(t.deepEqual, 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(
getRunnerLogger(true),
expectedConfigPath,
false,
);
t.deepEqual(loadedFromDisk, expectedConfig);
});
});

test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);

const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
}),
);

await target
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
});
});

test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => {
await withTmpDir(async (tmpDir) => {
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);

const target = callee(configUtils.determineUserConfig)
.withDefaultActionsEnv()
.withFeatures([Feature.AllowMergeConfigFiles])
.withArgs(
tmpDir,
createTestInitConfigInputs({
configInput: simpleConfigFileContents,
configFile: configFilePath,
workspacePath: tmpDir,
}),
);

await target
.logs(
t,
`Using config from action input: ${expectedConfigPath}`,
`Using configuration file: ${expectedConfigPath}`,
"Both a config file and config input were provided. Ignoring config file.",
)
.notLogs(t, "No configuration file was provided")
.passes(t.deepEqual, {
name: "my config",
queries: [{ uses: "./foo_file" }],
});
});
});
Loading
Loading