Skip to content

Commit 29d5e4c

Browse files
authored
Merge pull request #3994 from github/mbg/config/merge
Allow merging Default Setup `config` with config file
2 parents f133996 + 3d2713f commit 29d5e4c

7 files changed

Lines changed: 1297 additions & 464 deletions

File tree

lib/entry-points.js

Lines changed: 605 additions & 418 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config-utils.test.ts

Lines changed: 291 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
createTestConfig,
3838
makeMacro,
3939
initAllState,
40+
callee,
4041
} from "./testing-utils";
4142
import {
4243
GitHubVariant,
@@ -462,6 +463,24 @@ test.serial("load non-existent input", async (t) => {
462463
});
463464
});
464465

466+
/** A non-empty, but fairly minimal configuration file. */
467+
const simpleConfigFileContents = `
468+
name: my config
469+
queries:
470+
- uses: ./foo_file`;
471+
472+
/** A less minimal configuration file. */
473+
const otherConfigFileContents = `
474+
name: my config
475+
disable-default-queries: true
476+
queries:
477+
- uses: ./foo
478+
paths-ignore:
479+
- a
480+
- b
481+
paths:
482+
- c/d`;
483+
465484
test.serial("load non-empty input", async (t) => {
466485
return await withTmpDir(async (tempDir) => {
467486
setupActionsVars(tempDir, tempDir);
@@ -476,18 +495,6 @@ test.serial("load non-empty input", async (t) => {
476495
},
477496
});
478497

479-
// Just create a generic config object with non-default values for all fields
480-
const inputFileContents = `
481-
name: my config
482-
disable-default-queries: true
483-
queries:
484-
- uses: ./foo
485-
paths-ignore:
486-
- a
487-
- b
488-
paths:
489-
- c/d`;
490-
491498
fs.mkdirSync(path.join(tempDir, "foo"));
492499

493500
const userConfig: UserConfig = {
@@ -514,7 +521,7 @@ test.serial("load non-empty input", async (t) => {
514521
});
515522

516523
const languagesInput = "javascript";
517-
const configFilePath = createConfigFile(inputFileContents, tempDir);
524+
const configFilePath = createConfigFile(otherConfigFileContents, tempDir);
518525

519526
const state = initAllState();
520527
const actualConfig = await configUtils.initConfig(
@@ -540,14 +547,12 @@ test.serial(
540547
"Using config input and file together, config input should be used.",
541548
async (t) => {
542549
return await withTmpDir(async (tempDir) => {
543-
process.env["RUNNER_TEMP"] = tempDir;
544-
process.env["GITHUB_WORKSPACE"] = tempDir;
550+
setupActionsVars(tempDir, tempDir);
545551

546-
const inputFileContents = `
547-
name: my config
548-
queries:
549-
- uses: ./foo_file`;
550-
const configFilePath = createConfigFile(inputFileContents, tempDir);
552+
const configFilePath = createConfigFile(
553+
simpleConfigFileContents,
554+
tempDir,
555+
);
551556

552557
const configInput = `
553558
name: my config
@@ -576,7 +581,7 @@ test.serial(
576581
// Only JS, python packs will be ignored
577582
const languagesInput = "javascript";
578583

579-
const state = initAllState();
584+
const state = initAllState({ env: util.getEnv() });
580585
const config = await configUtils.initConfig(
581586
state,
582587
createTestInitConfigInputs({
@@ -2259,3 +2264,268 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r
22592264
{ exclude: { tags: "exclude-from-incremental" } },
22602265
]);
22612266
});
2267+
2268+
test("determineUserConfig - empty config when neither input is specified", async (t) => {
2269+
await withTmpDir(async (tmpDir) => {
2270+
const target = callee(configUtils.determineUserConfig)
2271+
.withDefaultActionsEnv()
2272+
.withFeatures([])
2273+
.withArgs(
2274+
tmpDir,
2275+
createTestInitConfigInputs({
2276+
configInput: undefined,
2277+
configFile: undefined,
2278+
workspacePath: tmpDir,
2279+
}),
2280+
);
2281+
2282+
// The returned configuration should be empty.
2283+
await target
2284+
// The fact that no configuration was provided should have been logged,
2285+
.logs(t, "No configuration file was provided")
2286+
// But not the messages for the two input sources
2287+
// or the warning about both inputs.
2288+
.notLogs(
2289+
t,
2290+
"Using config from action input:",
2291+
"Using configuration file:",
2292+
"Both a config file and config input were provided. Ignoring config file.",
2293+
)
2294+
.passes(t.deepEqual, {});
2295+
});
2296+
});
2297+
2298+
test("determineUserConfig - loads config file", async (t) => {
2299+
await withTmpDir(async (tmpDir) => {
2300+
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
2301+
2302+
const inputs = createTestInitConfigInputs({
2303+
configInput: undefined,
2304+
configFile: configFilePath,
2305+
workspacePath: tmpDir,
2306+
});
2307+
const target = callee(configUtils.determineUserConfig)
2308+
.withDefaultActionsEnv()
2309+
.withArgs(tmpDir, inputs);
2310+
2311+
await target
2312+
// The path of the input config file should have been logged,
2313+
.logs(t, `Using configuration file: ${configFilePath}`)
2314+
.notLogs(
2315+
t,
2316+
// The other two origin messages and the warning about both inputs should
2317+
// not have been logged.
2318+
"No configuration file was provided",
2319+
"Using config from action input:",
2320+
"Both a config file and config input were provided. Ignoring config file.",
2321+
)
2322+
// The loaded configuration should match `simpleConfigFileContents`.
2323+
.passes(t.deepEqual, {
2324+
name: "my config",
2325+
queries: [{ uses: "./foo_file" }],
2326+
});
2327+
2328+
// The `configFile` input should not have changed.
2329+
t.is(inputs.configFile, configFilePath);
2330+
});
2331+
});
2332+
2333+
test("determineUserConfig - loads config input", async (t) => {
2334+
await withTmpDir(async (tmpDir) => {
2335+
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
2336+
2337+
const inputs = createTestInitConfigInputs({
2338+
configInput: simpleConfigFileContents,
2339+
configFile: undefined,
2340+
workspacePath: tmpDir,
2341+
});
2342+
const target = callee(configUtils.determineUserConfig)
2343+
.withDefaultActionsEnv()
2344+
.withArgs(tmpDir, inputs);
2345+
2346+
await target
2347+
// The input source and path of the generated config file should have been logged.
2348+
.logs(
2349+
t,
2350+
"Using config from action input:",
2351+
`Using configuration file: ${expectedConfigPath}`,
2352+
)
2353+
// The message about no configuration input and
2354+
// the warning about both inputs should not have been logged.
2355+
.notLogs(
2356+
t,
2357+
"No configuration file was provided",
2358+
"Both a config file and config input were provided. Ignoring config file.",
2359+
)
2360+
// The loaded configuration should match `simpleConfigFileContents`.
2361+
.passes(t.deepEqual, {
2362+
name: "my config",
2363+
queries: [{ uses: "./foo_file" }],
2364+
});
2365+
2366+
// The `configFile` input should have been mutated to the generated path.
2367+
t.is(inputs.configFile, expectedConfigPath);
2368+
});
2369+
});
2370+
2371+
test("determineUserConfig - ignores config file input when both specified", async (t) => {
2372+
await withTmpDir(async (tmpDir) => {
2373+
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
2374+
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
2375+
2376+
const inputs = createTestInitConfigInputs({
2377+
configInput: simpleConfigFileContents,
2378+
configFile: configFilePath,
2379+
workspacePath: tmpDir,
2380+
});
2381+
const target = callee(configUtils.determineUserConfig)
2382+
.withDefaultActionsEnv()
2383+
.withArgs(tmpDir, inputs);
2384+
2385+
await target
2386+
// The path of the generated config file and
2387+
// the warning about both inputs should have been logged.
2388+
.logs(
2389+
t,
2390+
`Using config from action input: ${expectedConfigPath}`,
2391+
`Using configuration file: ${expectedConfigPath}`,
2392+
"Both a config file and config input were provided. Ignoring config file.",
2393+
)
2394+
.notLogs(t, "No configuration file was provided")
2395+
// The loaded configuration should match `simpleConfigFileContents`.
2396+
.passes(t.deepEqual, {
2397+
name: "my config",
2398+
queries: [{ uses: "./foo_file" }],
2399+
});
2400+
2401+
// The `configFile` input should have been mutated to the generated path.
2402+
t.is(inputs.configFile, expectedConfigPath);
2403+
});
2404+
});
2405+
2406+
/** A `config` input that we might get from Default Setup. */
2407+
const defaultSetupConfigInput = `
2408+
threat-models: [local, remote]
2409+
default-setup:
2410+
org:
2411+
model-packs: [foo, bar]`;
2412+
2413+
test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => {
2414+
await withTmpDir(async (tmpDir) => {
2415+
const configFilePath = createConfigFile(simpleConfigFileContents, tmpDir);
2416+
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
2417+
2418+
const inputs = createTestInitConfigInputs({
2419+
configInput: defaultSetupConfigInput,
2420+
configFile: configFilePath,
2421+
workspacePath: tmpDir,
2422+
});
2423+
const target = callee(configUtils.determineUserConfig)
2424+
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
2425+
.withFeatures([Feature.AllowMergeConfigFiles])
2426+
.withArgs(tmpDir, inputs);
2427+
2428+
// The loaded configuration should match the result of merging
2429+
// `defaultSetupConfigInput` and `simpleConfigFileContents`.
2430+
const expectedConfig = {
2431+
name: "my config",
2432+
queries: [{ uses: "./foo_file" }],
2433+
"threat-models": ["local", "remote"],
2434+
"default-setup": {
2435+
org: {
2436+
"model-packs": ["foo", "bar"],
2437+
},
2438+
},
2439+
} satisfies UserConfig;
2440+
2441+
await target
2442+
.logs(
2443+
t,
2444+
`Using merged configurations from 'config' input with configuration from '${configFilePath}': ${expectedConfigPath}`,
2445+
)
2446+
.notLogs(
2447+
t,
2448+
`Using configuration file: ${expectedConfigPath}`,
2449+
"No configuration file was provided",
2450+
`Using config from action input: ${expectedConfigPath}`,
2451+
"Both a config file and config input were provided. Ignoring config file.",
2452+
)
2453+
.passes(t.deepEqual, expectedConfig);
2454+
2455+
// The `configFile` input should have been mutated to the generated path.
2456+
t.is(inputs.configFile, expectedConfigPath);
2457+
2458+
// Since `result` is the result of merging the configurations in-memory,
2459+
// also check whether loading the configuration from disk that was written
2460+
// by `determineUserConfig` matches our expectations.
2461+
const loadedFromDisk = configUtils.getLocalConfig(
2462+
getRunnerLogger(true),
2463+
expectedConfigPath,
2464+
false,
2465+
);
2466+
t.deepEqual(loadedFromDisk, expectedConfig);
2467+
});
2468+
});
2469+
2470+
test("determineUserConfig - ignores config file input in Default Setup if FF is off", async (t) => {
2471+
await withTmpDir(async (tmpDir) => {
2472+
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
2473+
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
2474+
2475+
const target = callee(configUtils.determineUserConfig)
2476+
.withDefaultActionsEnv({ GITHUB_EVENT_NAME: "dynamic" })
2477+
.withArgs(
2478+
tmpDir,
2479+
createTestInitConfigInputs({
2480+
configInput: simpleConfigFileContents,
2481+
configFile: configFilePath,
2482+
workspacePath: tmpDir,
2483+
}),
2484+
);
2485+
2486+
await target
2487+
.logs(
2488+
t,
2489+
`Using config from action input: ${expectedConfigPath}`,
2490+
`Using configuration file: ${expectedConfigPath}`,
2491+
"Both a config file and config input were provided. Ignoring config file.",
2492+
)
2493+
.notLogs(t, "No configuration file was provided")
2494+
.passes(t.deepEqual, {
2495+
name: "my config",
2496+
queries: [{ uses: "./foo_file" }],
2497+
});
2498+
});
2499+
});
2500+
2501+
test("determineUserConfig - ignores config file input outside Default Setup if FF is on", async (t) => {
2502+
await withTmpDir(async (tmpDir) => {
2503+
const configFilePath = createConfigFile(otherConfigFileContents, tmpDir);
2504+
const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir);
2505+
2506+
const target = callee(configUtils.determineUserConfig)
2507+
.withDefaultActionsEnv()
2508+
.withFeatures([Feature.AllowMergeConfigFiles])
2509+
.withArgs(
2510+
tmpDir,
2511+
createTestInitConfigInputs({
2512+
configInput: simpleConfigFileContents,
2513+
configFile: configFilePath,
2514+
workspacePath: tmpDir,
2515+
}),
2516+
);
2517+
2518+
await target
2519+
.logs(
2520+
t,
2521+
`Using config from action input: ${expectedConfigPath}`,
2522+
`Using configuration file: ${expectedConfigPath}`,
2523+
"Both a config file and config input were provided. Ignoring config file.",
2524+
)
2525+
.notLogs(t, "No configuration file was provided")
2526+
.passes(t.deepEqual, {
2527+
name: "my config",
2528+
queries: [{ uses: "./foo_file" }],
2529+
});
2530+
});
2531+
});

0 commit comments

Comments
 (0)