Merge branch 'main' into mbg/improve-checkout-path

This commit is contained in:
Michael B. Gale
2026-10-14 10:57:16 +01:00
106 changed files with 37610 additions and 12531 deletions

View File

@@ -131,3 +131,38 @@ export interface Config {
*/
enableFileCoverageInformation: boolean;
}
/** Properties available in `Config`. */
export type ActionConfigProperties = keyof Config;
/**
* The CodeQL Action configuration state that is persisted
* between steps of the CodeQL Action in a CodeQL workflow.
*
* By default, this is equivalent to `Config`. By explicitly
* providing required property names for `T` this can be restricted
* to requiring only those. For example, in `ActionConfig<"codeQLCmd">`
* all properties except `codeqlCmd` are optional.
*/
export type ActionConfig<
T extends ActionConfigProperties = ActionConfigProperties,
> = Partial<Config> & Pick<Config, T>;
/** Helper function to check whether `config` has `key` to narrow the type of `config`. */
export function hasConfigKey<Key extends ActionConfigProperties>(
config: Partial<Config>,
key: Key,
): config is ActionConfig<Key> {
return config[key] !== undefined;
}
/** A minimal configuration that we can make available even when `init` aborts. */
export type AbortedInitConfig = ActionConfig<
| "version"
| "tempDir"
| "gitHubVersion"
| "debugMode"
| "languages"
| "analysisKinds"
| "codeQLCmd"
>;

View File

@@ -625,3 +625,73 @@ test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Se
`Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`,
]);
});
/** Parses `contents` as a configuration without validating it. */
function parseUnvalidatedConfig(contents: string): dbConfig.UserConfig {
return dbConfig.parseUserConfig(
getRunnerLogger(true),
"test",
contents,
false,
);
}
test("matchesDefaultSetupConfigSchema - returns true for configurations that only use Default Setup properties", (t) => {
for (const contents of [
[
"default-setup:",
" org:",
" model-packs: [ github/immutable-actions-list@0.0.1 ]",
"threat-models: [ ]",
].join("\n"),
"threat-models: [ local ]",
"{}",
]) {
t.true(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for configurations that use other properties", (t) => {
for (const contents of [
"queries: [ { uses: ./queries/show_ifs.ql } ]",
"paths-ignore: [ tests ]",
"default-setup: { org: { model-packs: [], queries: [] } }",
]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for invalid Default Setup properties", (t) => {
for (const contents of [
"threat-models: local",
"default-setup: { org: { model-packs: [ 1 ] } }",
]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});
test("matchesDefaultSetupConfigSchema - returns false for configurations that aren't mappings", (t) => {
for (const contents of ["- threat-models", "local", "null"]) {
t.false(
dbConfig.matchesDefaultSetupConfigSchema(
parseUnvalidatedConfig(contents),
),
contents,
);
}
});

View File

@@ -33,6 +33,15 @@ export interface QuerySpec {
uses: string;
}
// A set of default query suite names that are understood by the CLI.
export const defaultSuites: Set<string> = new Set([
"security-experimental",
"security-extended",
"security-and-quality",
"code-quality",
"code-scanning",
]);
const ORG_SCHEMA = {
/** An array of model pack names. */
"model-packs": json.optional(json.array(json.string)),
@@ -77,7 +86,11 @@ export interface UserConfig {
"default-setup"?: DefaultSetupConfig;
}
/** A subset of the `UserConfig` schema that is used by Default Setup. */
/**
* A subset of the `UserConfig` schema that is known to be used by Default Setup. None of these
* properties may add queries, since a per-language CodeQL bundle can be used with a `config` input
* that only sets them.
*/
const DEFAULT_SETUP_CONFIG_SCHEMA = {
"threat-models": json.optional(json.array(json.string)),
"default-setup": json.optional<DefaultSetupConfig>(
@@ -85,6 +98,24 @@ const DEFAULT_SETUP_CONFIG_SCHEMA = {
),
} as const satisfies json.Schema;
/**
* Returns whether `config` matches what we expect Default Setup to send in the `config` input: a
* mapping that only sets properties in `DEFAULT_SETUP_CONFIG_SCHEMA`, with values of the expected
* types.
*/
export function matchesDefaultSetupConfigSchema(config: UserConfig): boolean {
// Unless validation is enabled, `parseUserConfig` doesn't check that the YAML is a mapping.
if (!json.isObject(config)) {
return false;
}
const result = json.checkSchema(
DEFAULT_SETUP_CONFIG_SCHEMA,
config as json.UnvalidatedObject<any>,
);
// `valid` doesn't account for unknown properties.
return result.valid && result.unknownKeys.length === 0;
}
/**
* 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
@@ -437,63 +468,60 @@ export async function calculateAugmentation(
languages,
packsInputCombines,
);
const queriesInputCombines = shouldCombine(rawQueriesInput);
const queriesInput = parseQueriesFromInput(
rawQueriesInput,
queriesInputCombines,
const queries = parseQueriesFromInput(rawQueriesInput);
const repoPropertyQueries = parseQueriesFromInput(
repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES],
RepositoryPropertyName.EXTRA_QUERIES,
);
const repoExtraQueries =
repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES];
const repoExtraQueriesCombines = shouldCombine(repoExtraQueries);
const repoPropertyQueries = {
combines: repoExtraQueriesCombines,
input: parseQueriesFromInput(
repoExtraQueries,
repoExtraQueriesCombines,
new ConfigurationError(
errorMessages.getRepoPropertyError(
RepositoryPropertyName.EXTRA_QUERIES,
errorMessages.getEmptyCombinesError(),
),
),
),
};
return {
packsInputCombines,
packsInput: packsInput?.[languages[0]],
queriesInput,
queriesInputCombines,
queriesInput: queries.input,
queriesInputCombines: queries.combines,
repoPropertyQueries,
};
}
function parseQueriesFromInput(
rawQueriesInput: string | undefined,
queriesInputCombines: boolean,
errorToThrow?: ConfigurationError,
) {
if (!rawQueriesInput) {
return undefined;
/**
* Parses a comma-separated list of queries, which may have a '+' prefix. Entries aren't validated,
* so an empty entry becomes `{ uses: "" }`.
*
* @param value The list of queries.
* @param repositoryProperty The repository property that `value` comes from, if any. Error messages
* refer to this property, or to the `queries` input if no property is given.
* @returns An `Augmentation` containing the parsed queries and whether `value` has a '+' prefix.
* The queries are `undefined` if `value` is unset or empty.
* @throws A `ConfigurationError` if `value` is a '+' with no queries after it.
*/
export function parseQueriesFromInput(
value: string | undefined,
repositoryProperty?: RepositoryPropertyName,
): Augmentation<QuerySpec[]> {
if (!value) {
return { combines: false, input: undefined };
}
const trimmedInput = queriesInputCombines
? rawQueriesInput.trim().slice(1).trim()
: (rawQueriesInput?.trim() ?? "");
if (queriesInputCombines && trimmedInput.length === 0) {
if (errorToThrow) {
throw errorToThrow;
}
const combines = shouldCombine(value);
const trimmedInput = combines ? value.trim().slice(1).trim() : value.trim();
if (combines && trimmedInput.length === 0) {
throw new ConfigurationError(
errorMessages.getConfigFilePropertyError(
undefined,
"queries",
"A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs.",
),
repositoryProperty !== undefined
? errorMessages.getRepoPropertyError(
repositoryProperty,
errorMessages.getEmptyCombinesError(),
)
: errorMessages.getConfigFilePropertyError(
undefined,
"queries",
"A '+' was used in the 'queries' input to specify that you wished to add some packs to your CodeQL analysis. However, no packs were specified. Please either remove the '+' or specify some packs.",
),
);
}
return trimmedInput.split(",").map((query) => ({ uses: query.trim() }));
return {
combines,
input: trimmedInput.split(",").map((query) => ({ uses: query.trim() })),
};
}
/**
@@ -610,11 +638,13 @@ export function generateCodeScanningConfig(
* Attempts to parse `contents` into a `UserConfig` value.
*
* @param logger The logger to use.
* @param pathInput The path to the file where `contents` was obtained from, for use in error messages.
* @param contents The string contents of a YAML file to try and parse as a `UserConfig`.
* @param validateConfig Whether to validate the configuration file against the schema.
* @returns The `UserConfig` corresponding to `contents`, if parsing was successful.
* @throws A `ConfigurationError` if parsing failed.
* @param pathInput Where `contents` came from, such as the path to a file, for use in error messages.
* @param contents The YAML to try and parse as a `UserConfig`.
* @param validateConfig Whether to validate the configuration against the schema.
* @returns The `UserConfig` corresponding to `contents`, if parsing was successful. Unless
* `validateConfig` is set, the result might not be a mapping.
* @throws A `ConfigurationError` if `contents` isn't valid YAML or, when `validateConfig` is set,
* isn't a valid configuration.
*/
export function parseUserConfig(
logger: Logger,