From b5a476b47f8600358f172a642b28ee501005fd88 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 29 Sep 2026 19:31:48 +0100 Subject: [PATCH 01/18] Disable per-language bundles in the file baseline PR check Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/__export-file-baseline-information.yml | 1 + pr-checks/checks/export-file-baseline-information.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/__export-file-baseline-information.yml b/.github/workflows/__export-file-baseline-information.yml index 4ce8b40285..acdb099087 100644 --- a/.github/workflows/__export-file-baseline-information.yml +++ b/.github/workflows/__export-file-baseline-information.yml @@ -124,4 +124,5 @@ jobs: env: CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true + CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false CODEQL_ACTION_TEST_MODE: true diff --git a/pr-checks/checks/export-file-baseline-information.yml b/pr-checks/checks/export-file-baseline-information.yml index c5d5d12dda..5f409ef8fb 100644 --- a/pr-checks/checks/export-file-baseline-information.yml +++ b/pr-checks/checks/export-file-baseline-information.yml @@ -11,6 +11,8 @@ installDotNet: true env: CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true + # Per-language bundles only report file baseline information for their own language. + CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false steps: - uses: ./../action/init id: init From 5e3132db958789ae7c44f144f1e0e3fcadd23be3 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 29 Sep 2026 19:32:48 +0100 Subject: [PATCH 02/18] Move `defaultSuites` to `config/db-config.ts` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 14 +++++++------- src/analyze.test.ts | 2 +- src/analyze.ts | 10 +--------- src/config/db-config.ts | 9 +++++++++ 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 091132b1c8..95a5a92ba0 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149248,6 +149248,13 @@ async function loadRepositoryProperties(repositoryNwo, logger) { } // src/config/db-config.ts +var defaultSuites = /* @__PURE__ */ new Set([ + "security-experimental", + "security-extended", + "security-and-quality", + "code-quality", + "code-scanning" +]); var ORG_SCHEMA = { /** An array of model pack names. */ "model-packs": optional(array(string)) @@ -154002,13 +154009,6 @@ ${extensionContents}` ); return diffRangeDir; } -var defaultSuites = /* @__PURE__ */ new Set([ - "security-experimental", - "security-extended", - "security-and-quality", - "code-quality", - "code-scanning" -]); function resolveQuerySuiteAlias(language, maybeSuite) { if (defaultSuites.has(maybeSuite)) { return `${language}-${maybeSuite}.qls`; diff --git a/src/analyze.test.ts b/src/analyze.test.ts index 7523d239bf..80fc7a5474 100644 --- a/src/analyze.test.ts +++ b/src/analyze.test.ts @@ -7,12 +7,12 @@ import * as sinon from "sinon"; import { CodeQuality, CodeScanning, RiskAssessment } from "./analyses"; import { runQueries, - defaultSuites, resolveQuerySuiteAlias, addSarifExtension, diffRangeExtensionPackContents, } from "./analyze"; import { createStubCodeQL } from "./codeql"; +import { defaultSuites } from "./config/db-config"; import { Feature } from "./feature-flags"; import { BuiltInLanguage } from "./languages"; import { getRunnerLogger } from "./logging"; diff --git a/src/analyze.ts b/src/analyze.ts index 8f90711682..d585fd6d83 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -9,6 +9,7 @@ import { getTemporaryDirectory } from "./actions-util"; import * as analyses from "./analyses"; import { setupCppAutobuild } from "./autobuild"; import { type CodeQL } from "./codeql"; +import { defaultSuites } from "./config/db-config"; import * as configUtils from "./config-utils"; import { getCsharpTempDependencyDir, @@ -357,15 +358,6 @@ dataExtensions: return diffRangeDir; } -// A set of default query suite names that are understood by the CLI. -export const defaultSuites: Set = new Set([ - "security-experimental", - "security-extended", - "security-and-quality", - "code-quality", - "code-scanning", -]); - /** * If `maybeSuite` is the name of a default query suite, it is resolved into the corresponding * query suite name for the given `language`. Otherwise, `maybeSuite` is returned as is. diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 7b5bdbd8ce..44c671bae2 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -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 = 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)), From 1609a51ab7fe795e6e8f8b616a880584507f0dcb Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Tue, 29 Sep 2026 19:38:55 +0100 Subject: [PATCH 03/18] Avoid per-language bundles when queries may need other languages' library packs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 52 +++++++++++++-- src/codeql.test.ts | 10 +++ src/codeql.ts | 4 ++ src/init-action.ts | 16 ++++- src/init.ts | 2 + src/per-language-bundles.test.ts | 108 +++++++++++++++++++++++++++++++ src/per-language-bundles.ts | 85 ++++++++++++++++++++++++ src/setup-codeql-action.ts | 1 + src/setup-codeql.test.ts | 87 +++++++++++++++++++------ src/setup-codeql.ts | 9 +++ src/upload-lib.ts | 1 + 11 files changed, 351 insertions(+), 24 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 95a5a92ba0..ad390ee7a7 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151603,6 +151603,26 @@ var PER_LANGUAGE_BUNDLE_LANGUAGES = { ["osx64" /* Osx64 */]: /* @__PURE__ */ new Set(["swift" /* swift */]), ["win64" /* Win64 */]: /* @__PURE__ */ new Set() }; +function getOtherLanguagePacksReason(inputs) { + if (inputs.configFile !== void 0) { + return `the configuration file '${inputs.configFile}' may use queries that need library packs for other languages`; + } + if (inputs.configInput !== void 0 && !inputs.isDynamicWorkflow) { + return "the 'config' input may use queries that need library packs for other languages"; + } + const query = findNonBuiltInQuery(inputs.queriesInput); + if (query !== void 0) { + return `the query '${query}' from the 'queries' input may need library packs for other languages`; + } + const extraQuery = findNonBuiltInQuery(inputs.extraQueriesProperty); + if (extraQuery !== void 0) { + return `the query '${extraQuery}' from the '${"github-codeql-extra-queries" /* EXTRA_QUERIES */}' repository property may need library packs for other languages`; + } + return void 0; +} +function findNonBuiltInQuery(queries) { + return queries?.trim().replace(/^\+/, "").split(",").map((query) => query.trim()).find((query) => query !== "" && !defaultSuites.has(query)); +} async function getPerLanguageBundleLanguage({ env, features, @@ -151610,6 +151630,7 @@ async function getPerLanguageBundleLanguage({ }, options) { const { rawLanguages, + otherLanguagePacksReason, cliVersion: cliVersion2, compressionMethod, platform: platform2, @@ -151632,6 +151653,9 @@ async function getPerLanguageBundleLanguage({ if (language === void 0) { return explain(`'${rawLanguages[0]}' is not a known CodeQL language`); } + if (otherLanguagePacksReason !== void 0) { + return explain(otherLanguagePacksReason); + } if (compressionMethod !== "zstd") { return explain(`the bundle would be downloaded as '${compressionMethod}'`); } @@ -152254,7 +152278,7 @@ async function resolveDefaultCliVersion(defaultCliVersion, rawLanguages, useOver } return defaultCliVersion.enabledVersions[0]; } -async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, apiDetails, variant, tarSupportsZstd, features, logger) { +async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, apiDetails, variant, tarSupportsZstd, features, logger) { if (toolsInput && !isReservedToolsValue(toolsInput) && !toolsInput.startsWith("http")) { logger.info(`Using CodeQL CLI from local path ${toolsInput}`); const compressionMethod2 = inferCompressionMethod(toolsInput); @@ -152307,6 +152331,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO bundle = await getLatestNightlyBundle( { env: getEnv(), features, logger }, rawLanguages, + otherLanguagePacksReason, variant ); toolsInput = bundle.url; @@ -152474,6 +152499,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO { env: getEnv(), features, logger }, { rawLanguages, + otherLanguagePacksReason, cliVersion: cliVersion2, compressionMethod, platform: platform2, @@ -152637,7 +152663,7 @@ function getCanonicalToolcacheVersion(cliVersion2, bundleVersion2, logger) { } return cliVersion2; } -async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, features, logger) { +async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger) { if (!await isBinaryAccessible("tar", logger)) { throw new ConfigurationError( "Could not find tar in PATH, so unable to extract CodeQL bundle." @@ -152648,6 +152674,7 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau toolsInput, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, apiDetails, variant, @@ -152746,7 +152773,7 @@ async function useZstdBundle(cliVersion2, tarSupportsZstd) { function getTempExtractionDir(tempDir) { return path13.join(tempDir, v4_default()); } -async function getLatestNightlyBundle(action, rawLanguages, variant) { +async function getLatestNightlyBundle(action, rawLanguages, otherLanguagePacksReason, variant) { const { logger } = action; const zstdAvailability = await isZstdAvailable(logger); const compressionMethod = await useZstdBundle( @@ -152756,6 +152783,7 @@ async function getLatestNightlyBundle(action, rawLanguages, variant) { const platform2 = getBundlePlatform(); const language = await getPerLanguageBundleLanguage(action, { rawLanguages, + otherLanguagePacksReason, cliVersion: void 0, compressionMethod, platform: platform2, @@ -152896,7 +152924,7 @@ function isDiskConfigurationError(e) { e.message.includes("EACCES") ); } -async function setupCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, features, logger, checkVersion) { +async function setupCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger, checkVersion) { try { const { codeqlFolder, @@ -152910,6 +152938,7 @@ async function setupCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliV variant, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger @@ -155541,7 +155570,7 @@ var core15 = __toESM(require_core()); var toolrunner4 = __toESM(require_toolrunner()); var github3 = __toESM(require_github()); var io6 = __toESM(require_io()); -async function initCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, useOverlayAwareDefaultCliVersion, features, logger) { +async function initCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, rawLanguages, otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger) { logger.startGroup("Setup CodeQL tools"); const { codeql, toolsDownloadStatusReport, toolsSource, toolsVersion } = await setupCodeQL( toolsInput, @@ -155550,6 +155579,7 @@ async function initCodeQL(toolsInput, apiDetails, tempDir, variant, defaultCliVe variant, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger, @@ -155911,6 +155941,8 @@ async function combineSarifFilesUsingCLI(sarifFiles, gitHubVersion, features, lo codeQLDefaultVersionInfo, void 0, // rawLanguages: upload-lib does not run analysis + void 0, + // otherLanguagePacksReason: upload-lib does not run analysis false, // useOverlayAwareDefaultCliVersion: upload-lib does not run analysis features, @@ -162350,6 +162382,13 @@ async function run3(actionState) { const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages") ); + const otherLanguagePacksReason = getOtherLanguagePacksReason({ + configFile, + configInput: getOptionalInput("config"), + queriesInput: getOptionalInput("queries"), + extraQueriesProperty: repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */], + isDynamicWorkflow: isDynamicWorkflow(actionState.env) + }); const useOverlayAwareDefaultCliVersion = analysisKinds?.length === 1 && analysisKinds[0] === "code-scanning" /* CodeScanning */; const initCodeQLResult = await initCodeQL( toolsInput?.value, @@ -162358,6 +162397,7 @@ async function run3(actionState) { gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger @@ -163362,6 +163402,8 @@ async function run6(actionState) { gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, + void 0, + // otherLanguagePacksReason: this Action doesn't take a query configuration analysisKinds.length === 1 && analysisKinds[0] === "code-scanning" /* CodeScanning */, features, logger diff --git a/src/codeql.test.ts b/src/codeql.test.ts index 5f2eb31156..3237d57723 100644 --- a/src/codeql.test.ts +++ b/src/codeql.test.ts @@ -99,6 +99,7 @@ async function installIntoToolcache({ ? { enabledVersions: [{ cliVersion, tagName }] } : SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([]), getRunnerLogger(true), @@ -172,6 +173,7 @@ test.serial( util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -207,6 +209,7 @@ test.serial( util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -246,6 +249,7 @@ test.serial( util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -355,6 +359,7 @@ for (const { util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -397,6 +402,7 @@ for (const toolcacheVersion of [ util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -441,6 +447,7 @@ test.serial( ], }, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -487,6 +494,7 @@ test.serial( ], }, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -526,6 +534,7 @@ test.serial( util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), @@ -567,6 +576,7 @@ test.serial( util.GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, getRunnerLogger(true), diff --git a/src/codeql.ts b/src/codeql.ts index a4da8ad68a..00985c9949 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -302,6 +302,8 @@ export function isDiskConfigurationError(e: unknown): boolean { * @param variant * @param defaultCliVersion * @param rawLanguages Raw set of languages. + * @param otherLanguagePacksReason Why the configured queries may need library packs for languages + * other than `rawLanguages`, if they might. See `getOtherLanguagePacksReason`. * @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version. * @param features Information about the features that are enabled. * @param logger @@ -316,6 +318,7 @@ export async function setupCodeQL( variant: util.GitHubVariant, defaultCliVersion: CodeQLDefaultVersionInfo, rawLanguages: string[] | undefined, + otherLanguagePacksReason: string | undefined, useOverlayAwareDefaultCliVersion: boolean, features: FeatureEnablement, logger: Logger, @@ -339,6 +342,7 @@ export async function setupCodeQL( variant, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger, diff --git a/src/init-action.ts b/src/init-action.ts index 41e0b8629d..ac64225381 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -13,6 +13,7 @@ import { getOptionalInput, getRequiredInput, getTemporaryDirectory, + isDynamicWorkflow, persistInputs, } from "./actions-util"; import { AnalysisKind, getAnalysisKinds } from "./analyses"; @@ -40,7 +41,10 @@ import { } from "./diagnostics"; import { ActionsEnvVars, EnvVar } from "./environment"; import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; -import { loadRepositoryProperties } from "./feature-flags/properties"; +import { + loadRepositoryProperties, + RepositoryPropertyName, +} from "./feature-flags/properties"; import { checkInstallPython311, checkPacksForOverlayCompatibility, @@ -58,6 +62,7 @@ import { OverlayBaseDatabaseDownloadStats, } from "./overlay/caching"; import { OverlayDatabaseMode } from "./overlay/overlay-database-mode"; +import { getOtherLanguagePacksReason } from "./per-language-bundles"; import { getRepositoryNwo } from "./repository"; import { ToolsSource } from "./setup-codeql"; import { @@ -302,6 +307,14 @@ async function run( const rawLanguages = configUtils.getRawLanguagesNoAutodetect( getOptionalInput("languages"), ); + const otherLanguagePacksReason = getOtherLanguagePacksReason({ + configFile, + configInput: getOptionalInput("config"), + queriesInput: getOptionalInput("queries"), + extraQueriesProperty: + repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES], + isDynamicWorkflow: isDynamicWorkflow(actionState.env), + }); const useOverlayAwareDefaultCliVersion = analysisKinds?.length === 1 && analysisKinds[0] === AnalysisKind.CodeScanning; @@ -312,6 +325,7 @@ async function run( gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger, diff --git a/src/init.ts b/src/init.ts index c6a258e58c..d757e29976 100644 --- a/src/init.ts +++ b/src/init.ts @@ -40,6 +40,7 @@ export async function initCodeQL( variant: util.GitHubVariant, defaultCliVersion: CodeQLDefaultVersionInfo, rawLanguages: string[] | undefined, + otherLanguagePacksReason: string | undefined, useOverlayAwareDefaultCliVersion: boolean, features: FeatureEnablement, logger: Logger, @@ -58,6 +59,7 @@ export async function initCodeQL( variant, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, features, logger, diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index 0330b51a63..3dc97c70fe 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -4,9 +4,11 @@ import { ActionsEnvVars } from "./environment"; import { Feature } from "./feature-flags"; import { BuiltInLanguage } from "./languages"; import { + getOtherLanguagePacksReason, getPerLanguageBundleLanguage, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION, PerLanguageBundleOptions, + QueryConfigInputs, } from "./per-language-bundles"; import { BundlePlatform } from "./platform"; import { @@ -21,6 +23,7 @@ import { GitHubVariant } from "./util"; /** Options for which we would use a per-language bundle. */ const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = { rawLanguages: ["java"], + otherLanguagePacksReason: undefined, // Any version at least as new as the minimum will do. cliVersion: MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION, compressionMethod: "zstd", @@ -81,6 +84,20 @@ test("getPerLanguageBundleLanguage requires a known language", async (t) => { t.is(await checkEligibility({ rawLanguages: ["cobol"] }), undefined); }); +test("getPerLanguageBundleLanguage explains queries that may need library packs for other languages", async (t) => { + const messages: LoggedMessage[] = []; + const language = await checkEligibility( + { otherLanguagePacksReason: "an example reason" }, + { logger: getRecordingLogger(messages, { logToConsole: false }) }, + ); + + t.is(language, undefined); + t.deepEqual( + messages.map((message) => message.message), + ["Not using a per-language CodeQL bundle since an example reason."], + ); +}); + test("getPerLanguageBundleLanguage requires a zstd bundle", async (t) => { t.is(await checkEligibility({ compressionMethod: "gzip" }), undefined); }); @@ -155,6 +172,7 @@ test("getPerLanguageBundleLanguage skips only the release version check for the for (const overrides of [ { rawLanguages: undefined }, { rawLanguages: ["java", "python"] }, + { otherLanguagePacksReason: "an example reason" }, { compressionMethod: "gzip" as const }, { platform: BundlePlatform.Osx64 }, { variant: GitHubVariant.GHES }, @@ -173,3 +191,93 @@ test("getPerLanguageBundleLanguage skips only the release version check for the undefined, ); }); + +/** Query configuration inputs that configure nothing. */ +const NO_QUERY_CONFIG: QueryConfigInputs = { + configFile: undefined, + configInput: undefined, + queriesInput: undefined, + extraQueriesProperty: undefined, + isDynamicWorkflow: false, +}; + +test("getOtherLanguagePacksReason returns undefined when no queries are configured", (t) => { + t.is(getOtherLanguagePacksReason(NO_QUERY_CONFIG), undefined); +}); + +test("getOtherLanguagePacksReason returns undefined for built-in query suites", (t) => { + for (const queries of [ + "security-extended", + "+security-and-quality", + " security-extended , code-quality ", + ]) { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + queriesInput: queries, + extraQueriesProperty: queries, + }), + undefined, + queries, + ); + } +}); + +test("getOtherLanguagePacksReason returns undefined for the config input in a dynamic workflow", (t) => { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + configInput: "threat-models: [ local ]", + isDynamicWorkflow: true, + }), + undefined, + ); +}); + +test("getOtherLanguagePacksReason explains a configuration file, including in a dynamic workflow", (t) => { + // Default setup can get a configuration file from a repository property. + for (const isDynamicWorkflow of [false, true]) { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + configFile: "./.github/codeql/codeql-config.yml", + isDynamicWorkflow, + }), + "the configuration file './.github/codeql/codeql-config.yml' may use queries that need " + + "library packs for other languages", + ); + } +}); + +test("getOtherLanguagePacksReason explains the config input outside a dynamic workflow", (t) => { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + configInput: "queries: [ { uses: ./queries/show_ifs.ql } ]", + }), + "the 'config' input may use queries that need library packs for other languages", + ); +}); + +test("getOtherLanguagePacksReason explains the first query in the queries input that isn't a built-in query suite", (t) => { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + queriesInput: + "+security-extended, ./queries/show_ifs.ql, octo-org/queries@main", + }), + "the query './queries/show_ifs.ql' from the 'queries' input may need library packs for " + + "other languages", + ); +}); + +test("getOtherLanguagePacksReason explains a query in the extra queries repository property that isn't a built-in query suite", (t) => { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + extraQueriesProperty: "+octo-org/queries/show_ifs.ql@main", + }), + "the query 'octo-org/queries/show_ifs.ql@main' from the 'github-codeql-extra-queries' " + + "repository property may need library packs for other languages", + ); +}); diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index e0db649c7a..b1aced586d 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -2,7 +2,9 @@ import * as semver from "semver"; import { ActionState } from "./action-common"; import { isGitHubHostedRunner } from "./actions-util"; +import { defaultSuites } from "./config/db-config"; import { Feature } from "./feature-flags"; +import { RepositoryPropertyName } from "./feature-flags/properties"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; import { BundlePlatform } from "./platform"; import * as tar from "./tar"; @@ -31,10 +33,88 @@ const PER_LANGUAGE_BUNDLE_LANGUAGES: Readonly< [BundlePlatform.Win64]: new Set(), }; +/** Query configuration that is known before CodeQL is set up. */ +export interface QueryConfigInputs { + /** The configuration file from the `config-file` input or repository property. */ + configFile: string | undefined; + /** The `config` input. */ + configInput: string | undefined; + /** The `queries` input. */ + queriesInput: string | undefined; + /** The `github-codeql-extra-queries` repository property. */ + extraQueriesProperty: string | undefined; + /** Whether the Action is running in a dynamic workflow, such as default setup. */ + isDynamicWorkflow: boolean; +} + +/** + * Explains why the configured queries may need library packs for languages other than the one + * being analyzed, which a per-language bundle doesn't contain. Returns `undefined` if the only + * queries that these inputs add are built-in query suites. The `packs` input doesn't matter, since + * query packs are downloaded together with their dependencies. + * + * The configuration isn't loaded until CodeQL is set up, so any configuration file or `config` + * input is assumed to configure such queries, except for the `config` input in dynamic workflows. + */ +export function getOtherLanguagePacksReason( + inputs: QueryConfigInputs, +): string | undefined { + if (inputs.configFile !== undefined) { + return ( + `the configuration file '${inputs.configFile}' may use queries that need library packs ` + + "for other languages" + ); + } + + // We assume that dynamic workflows, which GitHub manages, don't use the `config` input to add + // queries. For example, default setup only uses it for threat models and model packs. + if (inputs.configInput !== undefined && !inputs.isDynamicWorkflow) { + return "the 'config' input may use queries that need library packs for other languages"; + } + + // We can't tell which language a local query or a query from another repository is for without + // loading it, and CodeQL resolves the library packs for every configured query, including those + // for languages that aren't being analyzed. + const query = findNonBuiltInQuery(inputs.queriesInput); + if (query !== undefined) { + return `the query '${query}' from the 'queries' input may need library packs for other languages`; + } + const extraQuery = findNonBuiltInQuery(inputs.extraQueriesProperty); + if (extraQuery !== undefined) { + return ( + `the query '${extraQuery}' from the '${RepositoryPropertyName.EXTRA_QUERIES}' repository ` + + "property may need library packs for other languages" + ); + } + + return undefined; +} + +/** + * Returns the first query in a comma-separated list of queries, in the format of the `queries` + * input, that isn't a built-in query suite. + */ +function findNonBuiltInQuery(queries: string | undefined): string | undefined { + return ( + queries + ?.trim() + // A leading '+' combines these queries with those configured elsewhere. + .replace(/^\+/, "") + .split(",") + .map((query) => query.trim()) + .find((query) => query !== "" && !defaultSuites.has(query)) + ); +} + /** Inputs that determine whether we may download a per-language bundle. */ export interface PerLanguageBundleOptions { /** Explicit input only: autodetection needs a CLI instance. */ rawLanguages: string[] | undefined; + /** + * Why the configured queries may need library packs for other languages, if they might. See + * `getOtherLanguagePacksReason`. + */ + otherLanguagePacksReason: string | undefined; /** Requested CLI version, if known. Ignored when requesting the latest nightly. */ cliVersion: string | undefined; compressionMethod: tar.CompressionMethod; @@ -56,6 +136,7 @@ export async function getPerLanguageBundleLanguage( ): Promise { const { rawLanguages, + otherLanguagePacksReason, cliVersion, compressionMethod, platform, @@ -85,6 +166,10 @@ export async function getPerLanguageBundleLanguage( return explain(`'${rawLanguages[0]}' is not a known CodeQL language`); } + if (otherLanguagePacksReason !== undefined) { + return explain(otherLanguagePacksReason); + } + if (compressionMethod !== "zstd") { // Per-language bundles are only published as zstd archives. return explain(`the bundle would be downloaded as '${compressionMethod}'`); diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index 91666f19cd..c50f67c415 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -165,6 +165,7 @@ async function run( gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, + undefined, // otherLanguagePacksReason: this Action doesn't take a query configuration analysisKinds.length === 1 && analysisKinds[0] === AnalysisKind.CodeScanning, features, diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index c33ac0700a..3fef11e63e 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -157,6 +157,7 @@ test.serial( url, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -242,6 +243,7 @@ for (const { "linked", SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -275,6 +277,7 @@ test.serial( "latest", SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -320,6 +323,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, logger, @@ -377,6 +381,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, logger, @@ -425,6 +430,7 @@ test.serial( "nightly", SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -452,6 +458,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, ["javascript"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, logger, @@ -495,6 +502,7 @@ test.serial( undefined, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -550,6 +558,7 @@ for (const bundlePath of [ GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([]), getRecordingLogger(messages), @@ -603,6 +612,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([Feature.CleanupToolcacheBundles]), getRunnerLogger(true), @@ -636,6 +646,7 @@ for (const toolsInput of ["nightly", "nightly-latest"]) { toolsInput, SAMPLE_DEFAULT_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -671,12 +682,23 @@ test.serial( await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); - for (const { languages, features } of [ + for (const { languages, otherLanguagePacksReason, features } of [ // The per-language feature is disabled. - { languages: ["java"], features: createFeatures([]) }, + { + languages: ["java"], + otherLanguagePacksReason: undefined, + features: createFeatures([]), + }, // More than one language requires a combined bundle. { languages: ["java", "python"], + otherLanguagePacksReason: undefined, + features: createFeatures([Feature.PerLanguageBundles]), + }, + // The configured queries may need library packs for other languages. + { + languages: ["java"], + otherLanguagePacksReason: "an example reason", features: createFeatures([Feature.PerLanguageBundles]), }, ]) { @@ -684,6 +706,7 @@ test.serial( "nightly", SAMPLE_DEFAULT_CLI_VERSION, languages, + otherLanguagePacksReason, false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -718,6 +741,7 @@ for (const perLanguageBundles of [false, true]) { undefined, // toolsInput: the nightly is selected by ForceNightly SAMPLE_DEFAULT_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -777,6 +801,7 @@ for (const date of ["20200101", "30260213"]) { url, SAMPLE_DEFAULT_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -804,6 +829,7 @@ for (const date of ["20200101", "30260213"]) { GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, features, logger, @@ -834,6 +860,7 @@ test.serial( "toolcache", SAMPLE_DEFAULT_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -875,6 +902,7 @@ test.serial( "toolcache", SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -944,6 +972,7 @@ const toolcacheInputFallbackMacro = makeMacro({ "toolcache", SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -1081,6 +1110,7 @@ test.serial( undefined, overlayMatchEnabledVersions, ["javascript"], + undefined, // otherLanguagePacksReason true, SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -1119,6 +1149,7 @@ test.serial( undefined, overlayMatchEnabledVersions, ["javascript"], + undefined, // otherLanguagePacksReason false, SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -1156,6 +1187,7 @@ test.serial( undefined, PER_LANGUAGE_CLI_VERSION, ["java-kotlin"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion SAMPLE_DOTCOM_API_DETAILS, GitHubVariant.DOTCOM, @@ -1185,7 +1217,7 @@ test.serial( ); test.serial( - "getCodeQLSource downloads the combined bundle when the feature is disabled", + "getCodeQLSource downloads the combined bundle when per-language selection is ineligible", async (t) => { sinon.stub(process, "platform").value("linux"); sinon.stub(process, "arch").value("x64"); @@ -1193,22 +1225,33 @@ test.serial( await withTmpDir(async (tmpDir) => { setupActionsVars(tmpDir, tmpDir); - const source = await setupCodeql.getCodeQLSource( - undefined, - PER_LANGUAGE_CLI_VERSION, - ["java"], - false, // useOverlayAwareDefaultCliVersion - SAMPLE_DOTCOM_API_DETAILS, - GitHubVariant.DOTCOM, - true, // tarSupportsZstd - createFeatures([]), - getRunnerLogger(true), - ); + for (const { otherLanguagePacksReason, features } of [ + // The per-language feature is disabled. + { otherLanguagePacksReason: undefined, features: createFeatures([]) }, + // The configured queries may need library packs for other languages. + { + otherLanguagePacksReason: "an example reason", + features: createFeatures([Feature.PerLanguageBundles]), + }, + ]) { + const source = await setupCodeql.getCodeQLSource( + undefined, + PER_LANGUAGE_CLI_VERSION, + ["java"], + otherLanguagePacksReason, + false, // useOverlayAwareDefaultCliVersion + SAMPLE_DOTCOM_API_DETAILS, + GitHubVariant.DOTCOM, + true, // tarSupportsZstd + features, + getRunnerLogger(true), + ); - t.is(source.sourceType, "download"); - if (source.sourceType === "download") { - t.true(source.bundle.url.endsWith("/codeql-bundle-linux64.tar.zst")); - t.is(source.bundle.kind, "combined"); + t.is(source.sourceType, "download"); + if (source.sourceType === "download") { + t.true(source.bundle.url.endsWith("/codeql-bundle-linux64.tar.zst")); + t.is(source.bundle.kind, "combined"); + } } }); }, @@ -1268,6 +1311,7 @@ for (const fallback of [false, true]) { GitHubVariant.DOTCOM, PER_LANGUAGE_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([Feature.PerLanguageBundles]), getRunnerLogger(true), @@ -1363,6 +1407,7 @@ for (const bundle of ["per-language", "combined", "fallback"] as const) { GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, bundle === "combined" ? ["javascript", "python"] : ["javascript"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, logger, @@ -1443,6 +1488,7 @@ for (const bundle of ["per-language", "combined", "fallback"] as const) { GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, ["javascript"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion features, logger, @@ -1478,6 +1524,7 @@ for (const asset of [ GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([]), getRecordingLogger(messages), @@ -1532,6 +1579,7 @@ for (const error of [ GitHubVariant.DOTCOM, PER_LANGUAGE_CLI_VERSION, ["java"], + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([Feature.PerLanguageBundles]), getRunnerLogger(true), @@ -1567,6 +1615,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([]), getRunnerLogger(true), @@ -2239,6 +2288,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([]), getRunnerLogger(true), @@ -2278,6 +2328,7 @@ test.serial( GitHubVariant.DOTCOM, SAMPLE_DEFAULT_CLI_VERSION, undefined, // rawLanguages + undefined, // otherLanguagePacksReason false, // useOverlayAwareDefaultCliVersion createFeatures([Feature.CleanupToolcacheBundles]), getRunnerLogger(true), diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index d306d40d63..31f09f0c26 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -377,6 +377,8 @@ async function resolveDefaultCliVersion( * @param toolsInput The argument provided for the `tools` input, if any. * @param defaultCliVersion The default CLI version that's linked to the CodeQL Action. * @param rawLanguages Raw set of languages. + * @param otherLanguagePacksReason Why the configured queries may need library packs for languages + * other than `rawLanguages`, if they might. See `getOtherLanguagePacksReason`. * @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version. * @param apiDetails Information about the GitHub API. * @param variant The GitHub variant we are running on. @@ -390,6 +392,7 @@ export async function getCodeQLSource( toolsInput: string | undefined, defaultCliVersion: CodeQLDefaultVersionInfo, rawLanguages: string[] | undefined, + otherLanguagePacksReason: string | undefined, useOverlayAwareDefaultCliVersion: boolean, apiDetails: api.GitHubApiDetails, variant: util.GitHubVariant, @@ -478,6 +481,7 @@ export async function getCodeQLSource( bundle = await getLatestNightlyBundle( { env: getEnv(), features, logger }, rawLanguages, + otherLanguagePacksReason, variant, ); toolsInput = bundle.url; @@ -721,6 +725,7 @@ export async function getCodeQLSource( { env: getEnv(), features, logger }, { rawLanguages, + otherLanguagePacksReason, cliVersion, compressionMethod, platform, @@ -1001,6 +1006,7 @@ export async function setupCodeQLBundle( variant: util.GitHubVariant, defaultCliVersion: CodeQLDefaultVersionInfo, rawLanguages: string[] | undefined, + otherLanguagePacksReason: string | undefined, useOverlayAwareDefaultCliVersion: boolean, features: FeatureEnablement, logger: Logger, @@ -1016,6 +1022,7 @@ export async function setupCodeQLBundle( toolsInput, defaultCliVersion, rawLanguages, + otherLanguagePacksReason, useOverlayAwareDefaultCliVersion, apiDetails, variant, @@ -1159,6 +1166,7 @@ function getTempExtractionDir(tempDir: string) { async function getLatestNightlyBundle( action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>, rawLanguages: string[] | undefined, + otherLanguagePacksReason: string | undefined, variant: util.GitHubVariant, ): Promise { const { logger } = action; @@ -1174,6 +1182,7 @@ async function getLatestNightlyBundle( const platform = getBundlePlatform(); const language = await getPerLanguageBundleLanguage(action, { rawLanguages, + otherLanguagePacksReason, cliVersion: undefined, compressionMethod, platform, diff --git a/src/upload-lib.ts b/src/upload-lib.ts index da5552cf24..c5b6c6efd5 100644 --- a/src/upload-lib.ts +++ b/src/upload-lib.ts @@ -166,6 +166,7 @@ async function combineSarifFilesUsingCLI( gitHubVersion.type, codeQLDefaultVersionInfo, undefined, // rawLanguages: upload-lib does not run analysis + undefined, // otherLanguagePacksReason: upload-lib does not run analysis false, // useOverlayAwareDefaultCliVersion: upload-lib does not run analysis features, logger, From d8b6f2a09b50947d13938f5ef83199b1aa738845 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Wed, 30 Sep 2026 15:24:35 +0100 Subject: [PATCH 04/18] Always use the combined bundle in `setup-codeql` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 13 ++++++++----- src/codeql.ts | 5 +++-- src/per-language-bundles.test.ts | 25 ++++++++++++++----------- src/per-language-bundles.ts | 14 +++++++++----- src/setup-codeql-action.ts | 7 ++++++- src/setup-codeql.ts | 5 +++-- 6 files changed, 43 insertions(+), 26 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ad390ee7a7..8f18954069 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151644,6 +151644,9 @@ async function getPerLanguageBundleLanguage({ if (!await features.getValue("per_language_bundles" /* PerLanguageBundles */)) { return explain(`the ${"per_language_bundles" /* PerLanguageBundles */} feature is disabled`); } + if (otherLanguagePacksReason !== void 0) { + return explain(otherLanguagePacksReason); + } if (rawLanguages?.length !== 1) { return explain( `exactly one language must be requested via the 'languages' input, but ${rawLanguages?.length ?? 0} were` @@ -151653,9 +151656,6 @@ async function getPerLanguageBundleLanguage({ if (language === void 0) { return explain(`'${rawLanguages[0]}' is not a known CodeQL language`); } - if (otherLanguagePacksReason !== void 0) { - return explain(otherLanguagePacksReason); - } if (compressionMethod !== "zstd") { return explain(`the bundle would be downloaded as '${compressionMethod}'`); } @@ -163402,8 +163402,11 @@ async function run6(actionState) { gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, - void 0, - // otherLanguagePacksReason: this Action doesn't take a query configuration + // CodeQL resolves the dependencies of queries that aren't in compiled packs from the bundle, + // so the queries that the workflow runs with this CLI may need library packs for languages + // other than those in the `languages` input. That input therefore only informs the choice of + // CLI version. + "the 'setup-codeql' Action can't tell whether the queries that the workflow runs will need library packs for other languages", analysisKinds.length === 1 && analysisKinds[0] === "code-scanning" /* CodeScanning */, features, logger diff --git a/src/codeql.ts b/src/codeql.ts index 00985c9949..6df604e394 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -302,8 +302,9 @@ export function isDiskConfigurationError(e: unknown): boolean { * @param variant * @param defaultCliVersion * @param rawLanguages Raw set of languages. - * @param otherLanguagePacksReason Why the configured queries may need library packs for languages - * other than `rawLanguages`, if they might. See `getOtherLanguagePacksReason`. + * @param otherLanguagePacksReason Why the CodeQL CLI may need packs for languages other than + * `rawLanguages`, or `undefined` if it won't. If defined, the combined bundle is used. See + * `PerLanguageBundleOptions.otherLanguagePacksReason`. * @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version. * @param features Information about the features that are enabled. * @param logger diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index 3dc97c70fe..b6c945160b 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -84,18 +84,21 @@ test("getPerLanguageBundleLanguage requires a known language", async (t) => { t.is(await checkEligibility({ rawLanguages: ["cobol"] }), undefined); }); -test("getPerLanguageBundleLanguage explains queries that may need library packs for other languages", async (t) => { - const messages: LoggedMessage[] = []; - const language = await checkEligibility( - { otherLanguagePacksReason: "an example reason" }, - { logger: getRecordingLogger(messages, { logToConsole: false }) }, - ); +test("getPerLanguageBundleLanguage explains why the CodeQL CLI may need packs for other languages before checking the languages", async (t) => { + // Without a language, the explanation would otherwise suggest requesting a single language. + for (const rawLanguages of [["java"], undefined]) { + const messages: LoggedMessage[] = []; + const language = await checkEligibility( + { rawLanguages, otherLanguagePacksReason: "an example reason" }, + { logger: getRecordingLogger(messages, { logToConsole: false }) }, + ); - t.is(language, undefined); - t.deepEqual( - messages.map((message) => message.message), - ["Not using a per-language CodeQL bundle since an example reason."], - ); + t.is(language, undefined); + t.deepEqual( + messages.map((message) => message.message), + ["Not using a per-language CodeQL bundle since an example reason."], + ); + } }); test("getPerLanguageBundleLanguage requires a zstd bundle", async (t) => { diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index b1aced586d..dbde59b213 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -111,7 +111,9 @@ export interface PerLanguageBundleOptions { /** Explicit input only: autodetection needs a CLI instance. */ rawLanguages: string[] | undefined; /** - * Why the configured queries may need library packs for other languages, if they might. See + * Why the CodeQL CLI may need packs for other languages, for example because of the configured + * queries, or `undefined` if it won't. If defined, the combined bundle is used, and this reason is + * logged to complete the sentence "Not using a per-language CodeQL bundle since ...". See * `getOtherLanguagePacksReason`. */ otherLanguagePacksReason: string | undefined; @@ -153,6 +155,12 @@ export async function getPerLanguageBundleLanguage( return explain(`the ${Feature.PerLanguageBundles} feature is disabled`); } + // This reason applies whichever languages were requested, so check it first to avoid suggesting + // that requesting a single language would be enough. + if (otherLanguagePacksReason !== undefined) { + return explain(otherLanguagePacksReason); + } + if (rawLanguages?.length !== 1) { return explain( `exactly one language must be requested via the 'languages' input, but ${ @@ -166,10 +174,6 @@ export async function getPerLanguageBundleLanguage( return explain(`'${rawLanguages[0]}' is not a known CodeQL language`); } - if (otherLanguagePacksReason !== undefined) { - return explain(otherLanguagePacksReason); - } - if (compressionMethod !== "zstd") { // Per-language bundles are only published as zstd archives. return explain(`the bundle would be downloaded as '${compressionMethod}'`); diff --git a/src/setup-codeql-action.ts b/src/setup-codeql-action.ts index c50f67c415..23a9c65ebd 100644 --- a/src/setup-codeql-action.ts +++ b/src/setup-codeql-action.ts @@ -165,7 +165,12 @@ async function run( gitHubVersion.type, codeQLDefaultVersionInfo, rawLanguages, - undefined, // otherLanguagePacksReason: this Action doesn't take a query configuration + // CodeQL resolves the dependencies of queries that aren't in compiled packs from the bundle, + // so the queries that the workflow runs with this CLI may need library packs for languages + // other than those in the `languages` input. That input therefore only informs the choice of + // CLI version. + "the 'setup-codeql' Action can't tell whether the queries that the workflow runs will need " + + "library packs for other languages", analysisKinds.length === 1 && analysisKinds[0] === AnalysisKind.CodeScanning, features, diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index 31f09f0c26..f2ce959a8d 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -377,8 +377,9 @@ async function resolveDefaultCliVersion( * @param toolsInput The argument provided for the `tools` input, if any. * @param defaultCliVersion The default CLI version that's linked to the CodeQL Action. * @param rawLanguages Raw set of languages. - * @param otherLanguagePacksReason Why the configured queries may need library packs for languages - * other than `rawLanguages`, if they might. See `getOtherLanguagePacksReason`. + * @param otherLanguagePacksReason Why the CodeQL CLI may need packs for languages other than + * `rawLanguages`, or `undefined` if it won't. If defined, the combined bundle is used. See + * `PerLanguageBundleOptions.otherLanguagePacksReason`. * @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version. * @param apiDetails Information about the GitHub API. * @param variant The GitHub variant we are running on. From 7816c333547100eb95294c36ff83e837080670d4 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Wed, 30 Sep 2026 16:21:00 +0100 Subject: [PATCH 05/18] Fix the docs for the `languages` and `analysis-kinds` inputs of `setup-codeql` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- setup-codeql/action.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/setup-codeql/action.yml b/setup-codeql/action.yml index 8d13eeaff0..14083b4f95 100644 --- a/setup-codeql/action.yml +++ b/setup-codeql/action.yml @@ -21,19 +21,16 @@ inputs: required: false languages: description: >- - A comma-separated list of CodeQL languages that will be analyzed in subsequent - `github/codeql-action/init` and `github/codeql-action/analyze` invocations. If specified, the - Action may use this list to select a CodeQL CLI version that is best suited to analyzing those - languages, for example by preferring a version that has a cached overlay-base database for the - specified languages. This input is not remembered and must also be passed to - `github/codeql-action/init`. + A comma-separated list of CodeQL languages that the installed CodeQL CLI will be used to + analyze. If specified, the Action may use this list to select a CodeQL CLI version that is + best suited to analyzing those languages, for example by preferring a version that has a + cached overlay-base database for the specified languages. required: false analysis-kinds: description: >- - [Internal] A comma-separated list of analysis kinds that subsequent - `github/codeql-action/init` invocations will enable. If specified, the Action may use this - list to select a CodeQL CLI version that is best suited to those analysis kinds. This input is - not remembered and must also be passed to `github/codeql-action/init`. + [Internal] A comma-separated list of analysis kinds that the installed CodeQL CLI will be used + for. If specified, the Action may use this list to select a CodeQL CLI version that is best + suited to those analysis kinds. Available options are the same as for the `analysis-kinds` input on the `init` Action. default: 'code-scanning' From 87a1923fea9a961ea559967840a0f118d0d4dab0 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 10:33:35 +0100 Subject: [PATCH 06/18] Use a new feature flag for per-language bundles Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 6 +++--- src/feature-flags.ts | 2 +- src/per-language-bundles.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 8f18954069..da3df1c220 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -148121,7 +148121,7 @@ var featureConfig = { envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SKIP_RESOURCE_CHECKS", minimumVersion: void 0 }, - ["per_language_bundles" /* PerLanguageBundles */]: { + ["per_language_bundles_v2" /* PerLanguageBundles */]: { defaultValue: false, envVar: "CODEQL_ACTION_PER_LANGUAGE_BUNDLES", minimumVersion: void 0 @@ -151641,8 +151641,8 @@ async function getPerLanguageBundleLanguage({ logger.debug(`Not using a per-language CodeQL bundle since ${reason}.`); return void 0; }; - if (!await features.getValue("per_language_bundles" /* PerLanguageBundles */)) { - return explain(`the ${"per_language_bundles" /* PerLanguageBundles */} feature is disabled`); + if (!await features.getValue("per_language_bundles_v2" /* PerLanguageBundles */)) { + return explain(`the ${"per_language_bundles_v2" /* PerLanguageBundles */} feature is disabled`); } if (otherLanguagePacksReason !== void 0) { return explain(otherLanguagePacksReason); diff --git a/src/feature-flags.ts b/src/feature-flags.ts index afddaea2a4..59b50e2dfd 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -168,7 +168,7 @@ export enum Feature { * Controls whether we may download a bundle containing only the single language being analysed, * rather than the combined bundle that contains every language. */ - PerLanguageBundles = "per_language_bundles", + PerLanguageBundles = "per_language_bundles_v2", QaTelemetryEnabled = "qa_telemetry_enabled", /** Routes (some) API requests through the registry proxy. */ ProxyApiRequests = "proxy_api_requests", diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index b6c945160b..5eb7f8ac2d 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -163,7 +163,7 @@ test("getPerLanguageBundleLanguage explains a disabled feature before checking e t.deepEqual( messages.map((message) => message.message), [ - "Not using a per-language CodeQL bundle since the per_language_bundles feature is disabled.", + "Not using a per-language CodeQL bundle since the per_language_bundles_v2 feature is disabled.", ], ); }); From 2da0d298e0a10c4bb799a1a8f81ae44093b68228 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 11:20:10 +0100 Subject: [PATCH 07/18] Omit the feature flag name from the per-language bundle debug log Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 2 +- src/per-language-bundles.test.ts | 4 +--- src/per-language-bundles.ts | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index da3df1c220..620916e8b1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151642,7 +151642,7 @@ async function getPerLanguageBundleLanguage({ return void 0; }; if (!await features.getValue("per_language_bundles_v2" /* PerLanguageBundles */)) { - return explain(`the ${"per_language_bundles_v2" /* PerLanguageBundles */} feature is disabled`); + return explain("the feature is disabled"); } if (otherLanguagePacksReason !== void 0) { return explain(otherLanguagePacksReason); diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index 5eb7f8ac2d..783ae592bc 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -162,9 +162,7 @@ test("getPerLanguageBundleLanguage explains a disabled feature before checking e t.is(language, undefined); t.deepEqual( messages.map((message) => message.message), - [ - "Not using a per-language CodeQL bundle since the per_language_bundles_v2 feature is disabled.", - ], + ["Not using a per-language CodeQL bundle since the feature is disabled."], ); }); diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index dbde59b213..44cb5ebd03 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -152,7 +152,7 @@ export async function getPerLanguageBundleLanguage( }; if (!(await features.getValue(Feature.PerLanguageBundles))) { - return explain(`the ${Feature.PerLanguageBundles} feature is disabled`); + return explain("the feature is disabled"); } // This reason applies whichever languages were requested, so check it first to avoid suggesting From a98f604084bbcd07e2a0ef7d1c17f1fb7259dac6 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:44:31 +0100 Subject: [PATCH 08/18] Read the `config` and `queries` inputs once in `init` Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 10 ++++++---- src/init-action.ts | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 620916e8b1..8e4b26b543 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -162382,10 +162382,12 @@ async function run3(actionState) { const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages") ); + const configInput = getOptionalInput("config"); + const queriesInput = getOptionalInput("queries"); const otherLanguagePacksReason = getOtherLanguagePacksReason({ configFile, - configInput: getOptionalInput("config"), - queriesInput: getOptionalInput("queries"), + configInput, + queriesInput, extraQueriesProperty: repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */], isDynamicWorkflow: isDynamicWorkflow(actionState.env) }); @@ -162436,13 +162438,13 @@ async function run3(actionState) { config = await initConfig2(actionStateWithFeatures, { analysisKinds, languagesInput: getOptionalInput("languages"), - queriesInput: getOptionalInput("queries"), + queriesInput, packsInput: getOptionalInput("packs"), buildModeInput: getOptionalInput("build-mode"), ramInput: getOptionalInput("ram"), configFile, dbLocation: getOptionalInput("db-location"), - configInput: getOptionalInput("config"), + configInput, dependencyCachingEnabled: getDependencyCachingEnabled(), // Debug mode is enabled if: // - The `init` Action is passed `debug: true`. diff --git a/src/init-action.ts b/src/init-action.ts index ac64225381..3111270243 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -307,10 +307,12 @@ async function run( const rawLanguages = configUtils.getRawLanguagesNoAutodetect( getOptionalInput("languages"), ); + const configInput = getOptionalInput("config"); + const queriesInput = getOptionalInput("queries"); const otherLanguagePacksReason = getOtherLanguagePacksReason({ configFile, - configInput: getOptionalInput("config"), - queriesInput: getOptionalInput("queries"), + configInput, + queriesInput, extraQueriesProperty: repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES], isDynamicWorkflow: isDynamicWorkflow(actionState.env), @@ -376,13 +378,13 @@ async function run( config = await initConfig(actionStateWithFeatures, { analysisKinds, languagesInput: getOptionalInput("languages"), - queriesInput: getOptionalInput("queries"), + queriesInput, packsInput: getOptionalInput("packs"), buildModeInput: getOptionalInput("build-mode"), ramInput: getOptionalInput("ram"), configFile, dbLocation: getOptionalInput("db-location"), - configInput: getOptionalInput("config"), + configInput, dependencyCachingEnabled: getDependencyCachingEnabled(), // Debug mode is enabled if: // - The `init` Action is passed `debug: true`. From a4fbe39872625d4eba4224079971db399bed3b65 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:45:31 +0100 Subject: [PATCH 09/18] Extract parsing of the `queries` input and the extra queries repository property Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 41 ++++++++++++++++----------- src/config/db-config.ts | 63 ++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 8e4b26b543..69a12fa1e1 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149430,18 +149430,32 @@ async function calculateAugmentation(rawPacksInput, rawQueriesInput, repositoryP languages, packsInputCombines ); - const queriesInputCombines = shouldCombine(rawQueriesInput); - const queriesInput = parseQueriesFromInput( - rawQueriesInput, - queriesInputCombines + const queries = parseQueriesInput(rawQueriesInput); + const repoPropertyQueries = parseExtraQueriesProperty( + repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */] ); - const repoExtraQueries = repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */]; - const repoExtraQueriesCombines = shouldCombine(repoExtraQueries); - const repoPropertyQueries = { - combines: repoExtraQueriesCombines, + return { + packsInputCombines, + packsInput: packsInput?.[languages[0]], + queriesInput: queries.input, + queriesInputCombines: queries.combines, + repoPropertyQueries + }; +} +function parseQueriesInput(rawQueriesInput) { + const combines = shouldCombine(rawQueriesInput); + return { + combines, + input: parseQueriesFromInput(rawQueriesInput, combines) + }; +} +function parseExtraQueriesProperty(value) { + const combines = shouldCombine(value); + return { + combines, input: parseQueriesFromInput( - repoExtraQueries, - repoExtraQueriesCombines, + value, + combines, new ConfigurationError( getRepoPropertyError( "github-codeql-extra-queries" /* EXTRA_QUERIES */, @@ -149450,13 +149464,6 @@ async function calculateAugmentation(rawPacksInput, rawQueriesInput, repositoryP ) ) }; - return { - packsInputCombines, - packsInput: packsInput?.[languages[0]], - queriesInput, - queriesInputCombines, - repoPropertyQueries - }; } function parseQueriesFromInput(rawQueriesInput, queriesInputCombines, errorToThrow) { if (!rawQueriesInput) { diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 44c671bae2..4b1ed54790 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -446,20 +446,53 @@ export async function calculateAugmentation( languages, packsInputCombines, ); - const queriesInputCombines = shouldCombine(rawQueriesInput); - const queriesInput = parseQueriesFromInput( - rawQueriesInput, - queriesInputCombines, + const queries = parseQueriesInput(rawQueriesInput); + const repoPropertyQueries = parseExtraQueriesProperty( + repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES], ); - const repoExtraQueries = - repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES]; - const repoExtraQueriesCombines = shouldCombine(repoExtraQueries); - const repoPropertyQueries = { - combines: repoExtraQueriesCombines, + return { + packsInputCombines, + packsInput: packsInput?.[languages[0]], + queriesInput: queries.input, + queriesInputCombines: queries.combines, + repoPropertyQueries, + }; +} + +/** + * Parses the `queries` input, a comma-separated list of queries that's combined with the queries + * from the configuration if it starts with '+'. The `input` of the result is `undefined` if the + * value is unset or empty. Entries aren't validated, so an empty entry becomes `{ uses: "" }`. + * + * @throws A `ConfigurationError` if the input is a '+' with no queries after it. + */ +export function parseQueriesInput( + rawQueriesInput: string | undefined, +): Augmentation { + const combines = shouldCombine(rawQueriesInput); + return { + combines, + input: parseQueriesFromInput(rawQueriesInput, combines), + }; +} + +/** + * Parses the `github-codeql-extra-queries` repository property, which has the same format as the + * `queries` input. The `input` of the result is `undefined` if the value is unset or empty. Entries + * aren't validated, so an empty entry becomes `{ uses: "" }`. + * + * @throws A `ConfigurationError` if the value is a '+' with no queries after it. + */ +export function parseExtraQueriesProperty( + value: string | undefined, +): Augmentation { + const combines = shouldCombine(value); + return { + combines, input: parseQueriesFromInput( - repoExtraQueries, - repoExtraQueriesCombines, + value, + combines, new ConfigurationError( errorMessages.getRepoPropertyError( RepositoryPropertyName.EXTRA_QUERIES, @@ -468,14 +501,6 @@ export async function calculateAugmentation( ), ), }; - - return { - packsInputCombines, - packsInput: packsInput?.[languages[0]], - queriesInput, - queriesInputCombines, - repoPropertyQueries, - }; } function parseQueriesFromInput( From 70897a78fe706cea2b82ecc977b84188e5daa891 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:46:17 +0100 Subject: [PATCH 10/18] Reuse the parsing of query inputs when choosing a bundle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 10 ++++++--- src/per-language-bundles.test.ts | 15 ++++++++++++- src/per-language-bundles.ts | 38 ++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 69a12fa1e1..83c1d99b34 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151617,18 +151617,22 @@ function getOtherLanguagePacksReason(inputs) { if (inputs.configInput !== void 0 && !inputs.isDynamicWorkflow) { return "the 'config' input may use queries that need library packs for other languages"; } - const query = findNonBuiltInQuery(inputs.queriesInput); + const query = findNonBuiltInQuery( + parseQueriesInput(inputs.queriesInput).input + ); if (query !== void 0) { return `the query '${query}' from the 'queries' input may need library packs for other languages`; } - const extraQuery = findNonBuiltInQuery(inputs.extraQueriesProperty); + const extraQuery = findNonBuiltInQuery( + parseExtraQueriesProperty(inputs.extraQueriesProperty).input + ); if (extraQuery !== void 0) { return `the query '${extraQuery}' from the '${"github-codeql-extra-queries" /* EXTRA_QUERIES */}' repository property may need library packs for other languages`; } return void 0; } function findNonBuiltInQuery(queries) { - return queries?.trim().replace(/^\+/, "").split(",").map((query) => query.trim()).find((query) => query !== "" && !defaultSuites.has(query)); + return queries?.find((query) => !defaultSuites.has(query.uses))?.uses; } async function getPerLanguageBundleLanguage({ env, diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index 783ae592bc..63e01d0dc1 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -18,7 +18,7 @@ import { initAllState, LoggedMessage, } from "./testing-utils"; -import { GitHubVariant } from "./util"; +import { ConfigurationError, GitHubVariant } from "./util"; /** Options for which we would use a per-language bundle. */ const ELIGIBLE_OPTIONS: PerLanguageBundleOptions = { @@ -282,3 +282,16 @@ test("getOtherLanguagePacksReason explains a query in the extra queries reposito "repository property may need library packs for other languages", ); }); + +test("getOtherLanguagePacksReason throws a ConfigurationError for a '+' with no queries after it", (t) => { + // Loading the configuration would throw the same errors. + for (const inputs of [ + { queriesInput: "+" }, + { extraQueriesProperty: " + " }, + ]) { + t.throws( + () => getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, ...inputs }), + { instanceOf: ConfigurationError }, + ); + } +}); diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 44cb5ebd03..2b46448b53 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -2,7 +2,12 @@ import * as semver from "semver"; import { ActionState } from "./action-common"; import { isGitHubHostedRunner } from "./actions-util"; -import { defaultSuites } from "./config/db-config"; +import { + defaultSuites, + parseExtraQueriesProperty, + parseQueriesInput, + QuerySpec, +} from "./config/db-config"; import { Feature } from "./feature-flags"; import { RepositoryPropertyName } from "./feature-flags/properties"; import { BuiltInLanguage, parseBuiltInLanguage } from "./languages"; @@ -55,6 +60,10 @@ export interface QueryConfigInputs { * * The configuration isn't loaded until CodeQL is set up, so any configuration file or `config` * input is assumed to configure such queries, except for the `config` input in dynamic workflows. + * + * @throws A `ConfigurationError` if the `queries` input or the `github-codeql-extra-queries` + * repository property is a '+' with no queries after it, unless an input that's checked earlier + * already gives a reason. */ export function getOtherLanguagePacksReason( inputs: QueryConfigInputs, @@ -75,11 +84,15 @@ export function getOtherLanguagePacksReason( // We can't tell which language a local query or a query from another repository is for without // loading it, and CodeQL resolves the library packs for every configured query, including those // for languages that aren't being analyzed. - const query = findNonBuiltInQuery(inputs.queriesInput); + const query = findNonBuiltInQuery( + parseQueriesInput(inputs.queriesInput).input, + ); if (query !== undefined) { return `the query '${query}' from the 'queries' input may need library packs for other languages`; } - const extraQuery = findNonBuiltInQuery(inputs.extraQueriesProperty); + const extraQuery = findNonBuiltInQuery( + parseExtraQueriesProperty(inputs.extraQueriesProperty).input, + ); if (extraQuery !== undefined) { return ( `the query '${extraQuery}' from the '${RepositoryPropertyName.EXTRA_QUERIES}' repository ` + @@ -90,20 +103,11 @@ export function getOtherLanguagePacksReason( return undefined; } -/** - * Returns the first query in a comma-separated list of queries, in the format of the `queries` - * input, that isn't a built-in query suite. - */ -function findNonBuiltInQuery(queries: string | undefined): string | undefined { - return ( - queries - ?.trim() - // A leading '+' combines these queries with those configured elsewhere. - .replace(/^\+/, "") - .split(",") - .map((query) => query.trim()) - .find((query) => query !== "" && !defaultSuites.has(query)) - ); +/** Returns the `uses` value of the first of `queries` that isn't a built-in query suite. */ +function findNonBuiltInQuery( + queries: QuerySpec[] | undefined, +): string | undefined { + return queries?.find((query) => !defaultSuites.has(query.uses))?.uses; } /** Inputs that determine whether we may download a per-language bundle. */ From 725421c27219c5bc6bb70c284c815cba6b8410d4 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:46:33 +0100 Subject: [PATCH 11/18] Explain that the `config` input can configure queries Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/per-language-bundles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 2b46448b53..112a5ec123 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -75,8 +75,9 @@ export function getOtherLanguagePacksReason( ); } - // We assume that dynamic workflows, which GitHub manages, don't use the `config` input to add - // queries. For example, default setup only uses it for threat models and model packs. + // The `config` input can configure queries in the same way as a configuration file. We assume + // that dynamic workflows, which GitHub manages, don't use it to add queries. For example, default + // setup only uses it for threat models and model packs. if (inputs.configInput !== undefined && !inputs.isDynamicWorkflow) { return "the 'config' input may use queries that need library packs for other languages"; } From 1bb99cb5c7d21804d30f2689318c9c24d0abde51 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:46:49 +0100 Subject: [PATCH 12/18] Explain what a reason to use the combined bundle means where it's checked Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/per-language-bundles.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 112a5ec123..d85c3f8923 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -160,8 +160,9 @@ export async function getPerLanguageBundleLanguage( return explain("the feature is disabled"); } - // This reason applies whichever languages were requested, so check it first to avoid suggesting - // that requesting a single language would be enough. + // A defined reason means the CodeQL CLI may need packs for other languages, for example because + // of the configured queries. That applies whichever languages were requested, so check it first + // to avoid suggesting that requesting a single language would be enough. if (otherLanguagePacksReason !== undefined) { return explain(otherLanguagePacksReason); } From a6cd2a544a7ba843f530213d1fefc4a991a36fb1 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 1 Oct 2026 17:59:47 +0100 Subject: [PATCH 13/18] Check what the `config` input sets instead of exempting dynamic workflows Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 21 ++++++++++++++--- src/config/db-config.test.ts | 40 ++++++++++++++++++++++++++++++++ src/config/db-config.ts | 22 ++++++++++++++++++ src/init-action.ts | 2 -- src/per-language-bundles.test.ts | 36 ++++++++++++++-------------- src/per-language-bundles.ts | 18 +++++++------- 6 files changed, 108 insertions(+), 31 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 83c1d99b34..0901767d73 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149268,6 +149268,22 @@ var DEFAULT_SETUP_CONFIG_SCHEMA = { object(DEFAULT_SETUP_SCHEMA) ) }; +function matchesDefaultSetupConfigSchema(contents) { + let config; + try { + config = load(contents); + } catch (error3) { + if (error3 instanceof YAMLException) { + return false; + } + throw error3; + } + if (!isObject(config)) { + return false; + } + const result = checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); + return result.valid && result.unknownKeys.length === 0; +} function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile) { logger.debug( "Combining configuration files from 'config' and 'config-file' inputs" @@ -151614,7 +151630,7 @@ function getOtherLanguagePacksReason(inputs) { if (inputs.configFile !== void 0) { return `the configuration file '${inputs.configFile}' may use queries that need library packs for other languages`; } - if (inputs.configInput !== void 0 && !inputs.isDynamicWorkflow) { + if (inputs.configInput !== void 0 && !matchesDefaultSetupConfigSchema(inputs.configInput)) { return "the 'config' input may use queries that need library packs for other languages"; } const query = findNonBuiltInQuery( @@ -162399,8 +162415,7 @@ async function run3(actionState) { configFile, configInput, queriesInput, - extraQueriesProperty: repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */], - isDynamicWorkflow: isDynamicWorkflow(actionState.env) + extraQueriesProperty: repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */] }); const useOverlayAwareDefaultCliVersion = analysisKinds?.length === 1 && analysisKinds[0] === "code-scanning" /* CodeScanning */; const initCodeQLResult = await initCodeQL( diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index 63d9d2ffec..ecf77194a4 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -625,3 +625,43 @@ test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Se `Invalid keys in Default Setup configuration: ${expectedInvalidKeys}`, ]); }); + +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(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(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(contents), contents); + } +}); + +test("matchesDefaultSetupConfigSchema - returns false for contents that aren't a YAML object", (t) => { + for (const contents of ["threat-models: [", "- threat-models", "local", ""]) { + t.false(dbConfig.matchesDefaultSetupConfigSchema(contents), contents); + } +}); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 4b1ed54790..38cc74ab2e 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -94,6 +94,28 @@ const DEFAULT_SETUP_CONFIG_SCHEMA = { ), } as const satisfies json.Schema; +/** + * Returns whether `contents` is a YAML mapping that only sets the properties that Default Setup + * uses, which are threat models and model packs, to valid values. Returns `false` otherwise, + * including if `contents` isn't valid YAML. + */ +export function matchesDefaultSetupConfigSchema(contents: string): boolean { + let config: unknown; + try { + config = yaml.load(contents); + } catch (error) { + if (error instanceof yaml.YAMLException) { + return false; + } + throw error; + } + if (!json.isObject(config)) { + return false; + } + const result = json.checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); + 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 diff --git a/src/init-action.ts b/src/init-action.ts index 3111270243..e1ac5df5ec 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -13,7 +13,6 @@ import { getOptionalInput, getRequiredInput, getTemporaryDirectory, - isDynamicWorkflow, persistInputs, } from "./actions-util"; import { AnalysisKind, getAnalysisKinds } from "./analyses"; @@ -315,7 +314,6 @@ async function run( queriesInput, extraQueriesProperty: repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES], - isDynamicWorkflow: isDynamicWorkflow(actionState.env), }); const useOverlayAwareDefaultCliVersion = analysisKinds?.length === 1 && diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index 63e01d0dc1..c94b8d6dba 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -199,7 +199,6 @@ const NO_QUERY_CONFIG: QueryConfigInputs = { configInput: undefined, queriesInput: undefined, extraQueriesProperty: undefined, - isDynamicWorkflow: false, }; test("getOtherLanguagePacksReason returns undefined when no queries are configured", (t) => { @@ -224,33 +223,34 @@ test("getOtherLanguagePacksReason returns undefined for built-in query suites", } }); -test("getOtherLanguagePacksReason returns undefined for the config input in a dynamic workflow", (t) => { +test("getOtherLanguagePacksReason returns undefined for a config input that only uses default setup properties", (t) => { t.is( getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, - configInput: "threat-models: [ local ]", - isDynamicWorkflow: true, + // The shape of the `config` input that default setup passes. + configInput: [ + "default-setup:", + " org:", + " model-packs: [ github/immutable-actions-list@0.0.1 ]", + "threat-models: [ ]", + ].join("\n"), }), undefined, ); }); -test("getOtherLanguagePacksReason explains a configuration file, including in a dynamic workflow", (t) => { - // Default setup can get a configuration file from a repository property. - for (const isDynamicWorkflow of [false, true]) { - t.is( - getOtherLanguagePacksReason({ - ...NO_QUERY_CONFIG, - configFile: "./.github/codeql/codeql-config.yml", - isDynamicWorkflow, - }), - "the configuration file './.github/codeql/codeql-config.yml' may use queries that need " + - "library packs for other languages", - ); - } +test("getOtherLanguagePacksReason explains a configuration file", (t) => { + t.is( + getOtherLanguagePacksReason({ + ...NO_QUERY_CONFIG, + configFile: "./.github/codeql/codeql-config.yml", + }), + "the configuration file './.github/codeql/codeql-config.yml' may use queries that need " + + "library packs for other languages", + ); }); -test("getOtherLanguagePacksReason explains the config input outside a dynamic workflow", (t) => { +test("getOtherLanguagePacksReason explains a config input that uses other properties", (t) => { t.is( getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index d85c3f8923..371787901b 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -4,6 +4,7 @@ import { ActionState } from "./action-common"; import { isGitHubHostedRunner } from "./actions-util"; import { defaultSuites, + matchesDefaultSetupConfigSchema, parseExtraQueriesProperty, parseQueriesInput, QuerySpec, @@ -48,8 +49,6 @@ export interface QueryConfigInputs { queriesInput: string | undefined; /** The `github-codeql-extra-queries` repository property. */ extraQueriesProperty: string | undefined; - /** Whether the Action is running in a dynamic workflow, such as default setup. */ - isDynamicWorkflow: boolean; } /** @@ -58,8 +57,9 @@ export interface QueryConfigInputs { * queries that these inputs add are built-in query suites. The `packs` input doesn't matter, since * query packs are downloaded together with their dependencies. * - * The configuration isn't loaded until CodeQL is set up, so any configuration file or `config` - * input is assumed to configure such queries, except for the `config` input in dynamic workflows. + * Any configuration file is assumed to configure such queries, since reading it may need file or API + * access. The `config` input is only assumed to if it sets anything other than valid threat models + * and model packs, which are the properties that default setup uses. * * @throws A `ConfigurationError` if the `queries` input or the `github-codeql-extra-queries` * repository property is a '+' with no queries after it, unless an input that's checked earlier @@ -75,10 +75,12 @@ export function getOtherLanguagePacksReason( ); } - // The `config` input can configure queries in the same way as a configuration file. We assume - // that dynamic workflows, which GitHub manages, don't use it to add queries. For example, default - // setup only uses it for threat models and model packs. - if (inputs.configInput !== undefined && !inputs.isDynamicWorkflow) { + // The `config` input can configure queries in the same way as a configuration file. Default + // setup only uses it for threat models and model packs, neither of which adds queries. + if ( + inputs.configInput !== undefined && + !matchesDefaultSetupConfigSchema(inputs.configInput) + ) { return "the 'config' input may use queries that need library packs for other languages"; } From ce28f3e445ba13d316cd82ad713048e9cbbe3ebb Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 2 Oct 2026 13:49:21 +0100 Subject: [PATCH 14/18] Explain why the file baseline PR check uses the combined bundle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pr-checks/checks/export-file-baseline-information.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pr-checks/checks/export-file-baseline-information.yml b/pr-checks/checks/export-file-baseline-information.yml index 5f409ef8fb..ce22ec5d5f 100644 --- a/pr-checks/checks/export-file-baseline-information.yml +++ b/pr-checks/checks/export-file-baseline-information.yml @@ -11,7 +11,9 @@ installDotNet: true env: CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS: false CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE: true - # Per-language bundles only report file baseline information for their own language. + # To balance speed and coverage, we analyze only a single language (JavaScript), but use the + # combined bundle so we can test that baseline information is reported for each language in the + # multi-language source directory. CODEQL_ACTION_PER_LANGUAGE_BUNDLES: false steps: - uses: ./../action/init From 1c0814d7d2e7b0a986e507771045c7e5f2c1c72f Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 2 Oct 2026 14:22:24 +0100 Subject: [PATCH 15/18] Parse lists of queries with a single function Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 59 +++++++++--------------- src/config/db-config.ts | 89 +++++++++++++------------------------ src/per-language-bundles.ts | 10 +++-- 3 files changed, 58 insertions(+), 100 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0901767d73..0931ef5e91 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149446,9 +149446,10 @@ async function calculateAugmentation(rawPacksInput, rawQueriesInput, repositoryP languages, packsInputCombines ); - const queries = parseQueriesInput(rawQueriesInput); - const repoPropertyQueries = parseExtraQueriesProperty( - repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */] + const queries = parseQueriesFromInput(rawQueriesInput); + const repoPropertyQueries = parseQueriesFromInput( + repositoryProperties["github-codeql-extra-queries" /* EXTRA_QUERIES */], + "github-codeql-extra-queries" /* EXTRA_QUERIES */ ); return { packsInputCombines, @@ -149458,47 +149459,28 @@ async function calculateAugmentation(rawPacksInput, rawQueriesInput, repositoryP repoPropertyQueries }; } -function parseQueriesInput(rawQueriesInput) { - const combines = shouldCombine(rawQueriesInput); - return { - combines, - input: parseQueriesFromInput(rawQueriesInput, combines) - }; -} -function parseExtraQueriesProperty(value) { +function parseQueriesFromInput(value, repositoryProperty) { const combines = shouldCombine(value); - return { - combines, - input: parseQueriesFromInput( - value, - combines, - new ConfigurationError( - getRepoPropertyError( - "github-codeql-extra-queries" /* EXTRA_QUERIES */, - getEmptyCombinesError() - ) - ) - ) - }; -} -function parseQueriesFromInput(rawQueriesInput, queriesInputCombines, errorToThrow) { - if (!rawQueriesInput) { - return void 0; + if (!value) { + return { combines, input: void 0 }; } - const trimmedInput = queriesInputCombines ? rawQueriesInput.trim().slice(1).trim() : rawQueriesInput?.trim() ?? ""; - if (queriesInputCombines && trimmedInput.length === 0) { - if (errorToThrow) { - throw errorToThrow; - } + const trimmedInput = combines ? value.trim().slice(1).trim() : value.trim(); + if (combines && trimmedInput.length === 0) { throw new ConfigurationError( - getConfigFilePropertyError( + repositoryProperty !== void 0 ? getRepoPropertyError( + repositoryProperty, + getEmptyCombinesError() + ) : getConfigFilePropertyError( void 0, "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() })) + }; } function combineQueries(logger, config, augmentationProperties) { const result = []; @@ -151634,13 +151616,16 @@ function getOtherLanguagePacksReason(inputs) { return "the 'config' input may use queries that need library packs for other languages"; } const query = findNonBuiltInQuery( - parseQueriesInput(inputs.queriesInput).input + parseQueriesFromInput(inputs.queriesInput).input ); if (query !== void 0) { return `the query '${query}' from the 'queries' input may need library packs for other languages`; } const extraQuery = findNonBuiltInQuery( - parseExtraQueriesProperty(inputs.extraQueriesProperty).input + parseQueriesFromInput( + inputs.extraQueriesProperty, + "github-codeql-extra-queries" /* EXTRA_QUERIES */ + ).input ); if (extraQuery !== void 0) { return `the query '${extraQuery}' from the '${"github-codeql-extra-queries" /* EXTRA_QUERIES */}' repository property may need library packs for other languages`; diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 38cc74ab2e..cd3ca66bba 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -468,9 +468,10 @@ export async function calculateAugmentation( languages, packsInputCombines, ); - const queries = parseQueriesInput(rawQueriesInput); - const repoPropertyQueries = parseExtraQueriesProperty( + const queries = parseQueriesFromInput(rawQueriesInput); + const repoPropertyQueries = parseQueriesFromInput( repositoryProperties[RepositoryPropertyName.EXTRA_QUERIES], + RepositoryPropertyName.EXTRA_QUERIES, ); return { @@ -483,73 +484,43 @@ export async function calculateAugmentation( } /** - * Parses the `queries` input, a comma-separated list of queries that's combined with the queries - * from the configuration if it starts with '+'. The `input` of the result is `undefined` if the - * value is unset or empty. Entries aren't validated, so an empty entry becomes `{ uses: "" }`. + * Parses a comma-separated list of queries, which may start with '+'. `combines` is whether it + * starts with '+', and `input` holds the queries, or is `undefined` if `value` is unset or empty. + * Entries aren't validated, so an empty entry becomes `{ uses: "" }`. * - * @throws A `ConfigurationError` if the input is a '+' with no queries after it. + * @param value The list of queries. + * @param repositoryProperty The repository property that `value` comes from, if any. Errors name + * this property, or the `queries` input if it's unset. + * @throws A `ConfigurationError` if `value` is a '+' with no queries after it. */ -export function parseQueriesInput( - rawQueriesInput: string | undefined, -): Augmentation { - const combines = shouldCombine(rawQueriesInput); - return { - combines, - input: parseQueriesFromInput(rawQueriesInput, combines), - }; -} - -/** - * Parses the `github-codeql-extra-queries` repository property, which has the same format as the - * `queries` input. The `input` of the result is `undefined` if the value is unset or empty. Entries - * aren't validated, so an empty entry becomes `{ uses: "" }`. - * - * @throws A `ConfigurationError` if the value is a '+' with no queries after it. - */ -export function parseExtraQueriesProperty( +export function parseQueriesFromInput( value: string | undefined, + repositoryProperty?: RepositoryPropertyName, ): Augmentation { const combines = shouldCombine(value); - return { - combines, - input: parseQueriesFromInput( - value, - combines, - new ConfigurationError( - errorMessages.getRepoPropertyError( - RepositoryPropertyName.EXTRA_QUERIES, - errorMessages.getEmptyCombinesError(), - ), - ), - ), - }; -} - -function parseQueriesFromInput( - rawQueriesInput: string | undefined, - queriesInputCombines: boolean, - errorToThrow?: ConfigurationError, -) { - if (!rawQueriesInput) { - return undefined; + if (!value) { + return { combines, input: undefined }; } - const trimmedInput = queriesInputCombines - ? rawQueriesInput.trim().slice(1).trim() - : (rawQueriesInput?.trim() ?? ""); - if (queriesInputCombines && trimmedInput.length === 0) { - if (errorToThrow) { - throw errorToThrow; - } + 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() })), + }; } /** diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 371787901b..308fdcccc5 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -5,8 +5,7 @@ import { isGitHubHostedRunner } from "./actions-util"; import { defaultSuites, matchesDefaultSetupConfigSchema, - parseExtraQueriesProperty, - parseQueriesInput, + parseQueriesFromInput, QuerySpec, } from "./config/db-config"; import { Feature } from "./feature-flags"; @@ -88,13 +87,16 @@ export function getOtherLanguagePacksReason( // loading it, and CodeQL resolves the library packs for every configured query, including those // for languages that aren't being analyzed. const query = findNonBuiltInQuery( - parseQueriesInput(inputs.queriesInput).input, + parseQueriesFromInput(inputs.queriesInput).input, ); if (query !== undefined) { return `the query '${query}' from the 'queries' input may need library packs for other languages`; } const extraQuery = findNonBuiltInQuery( - parseExtraQueriesProperty(inputs.extraQueriesProperty).input, + parseQueriesFromInput( + inputs.extraQueriesProperty, + RepositoryPropertyName.EXTRA_QUERIES, + ).input, ); if (extraQuery !== undefined) { return ( From e869836b5a52553133d0a507b26d16c4c18f19c6 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 2 Oct 2026 14:24:04 +0100 Subject: [PATCH 16/18] Share the check of the properties that Default Setup sets in the `config` input Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 8 +++++--- src/config/db-config.ts | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 0931ef5e91..11e1aef19a 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149268,6 +149268,9 @@ var DEFAULT_SETUP_CONFIG_SCHEMA = { object(DEFAULT_SETUP_SCHEMA) ) }; +function checkDefaultSetupConfig(config) { + return checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); +} function matchesDefaultSetupConfigSchema(contents) { let config; try { @@ -149281,15 +149284,14 @@ function matchesDefaultSetupConfigSchema(contents) { if (!isObject(config)) { return false; } - const result = checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); + const result = checkDefaultSetupConfig(config); return result.valid && result.unknownKeys.length === 0; } function mergeDefaultSetupAndUserConfigs(logger, fromConfigInput, fromConfigFile) { logger.debug( "Combining configuration files from 'config' and 'config-file' inputs" ); - const schemaCheckResult = checkSchema( - DEFAULT_SETUP_CONFIG_SCHEMA, + const schemaCheckResult = checkDefaultSetupConfig( fromConfigInput ); if (schemaCheckResult.invalidKeys.length > 0) { diff --git a/src/config/db-config.ts b/src/config/db-config.ts index cd3ca66bba..977da17c72 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -94,6 +94,17 @@ const DEFAULT_SETUP_CONFIG_SCHEMA = { ), } as const satisfies json.Schema; +/** + * Checks `config` against the properties that Default Setup sets in the `config` input. The result + * lists any other properties in `unknownKeys`, and any properties with invalid values in + * `invalidKeys`. + */ +function checkDefaultSetupConfig( + config: json.UnvalidatedObject, +): json.CheckSchemaResult { + return json.checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); +} + /** * Returns whether `contents` is a YAML mapping that only sets the properties that Default Setup * uses, which are threat models and model packs, to valid values. Returns `false` otherwise, @@ -112,7 +123,7 @@ export function matchesDefaultSetupConfigSchema(contents: string): boolean { if (!json.isObject(config)) { return false; } - const result = json.checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); + const result = checkDefaultSetupConfig(config); return result.valid && result.unknownKeys.length === 0; } @@ -138,8 +149,7 @@ export function mergeDefaultSetupAndUserConfigs( // 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, + const schemaCheckResult = checkDefaultSetupConfig( fromConfigInput as json.UnvalidatedObject, ); From 113b18e688fc5fca5f1f8bc085af94eacc00a5f8 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 2 Oct 2026 14:29:22 +0100 Subject: [PATCH 17/18] Parse the `config` input once Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 43 +++++++-------- src/config-utils.test.ts | 89 ++++++++++++++++++++++++-------- src/config-utils.ts | 45 ++++++++++------ src/config/db-config.test.ts | 42 ++++++++++++--- src/config/db-config.ts | 20 +++---- src/init-action.ts | 5 +- src/per-language-bundles.test.ts | 16 +++--- src/per-language-bundles.ts | 5 +- 8 files changed, 175 insertions(+), 90 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 11e1aef19a..ab220e3926 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -149271,16 +149271,7 @@ var DEFAULT_SETUP_CONFIG_SCHEMA = { function checkDefaultSetupConfig(config) { return checkSchema(DEFAULT_SETUP_CONFIG_SCHEMA, config); } -function matchesDefaultSetupConfigSchema(contents) { - let config; - try { - config = load(contents); - } catch (error3) { - if (error3 instanceof YAMLException) { - return false; - } - throw error3; - } +function matchesDefaultSetupConfigSchema(config) { if (!isObject(config)) { return false; } @@ -150864,20 +150855,22 @@ async function applyIncrementalAnalysisSettings(config, hasDiffRanges, codeql, l }); } } -async function determineUserConfig(action, tempDir, inputs) { - const validateConfig = await action.features.getValue( - "validate_db_config" /* ValidateDbConfig */ +async function parseConfigInput({ logger, features }, configInput) { + if (configInput === void 0) { + return void 0; + } + return parseUserConfig( + logger, + "`config` input", + configInput, + await features.getValue("validate_db_config" /* ValidateDbConfig */) ); - if (inputs.configInput) { +} +async function determineUserConfig(action, tempDir, inputs) { + if (inputs.configInput !== void 0) { const computedConfigPath = userConfigFromActionPath(tempDir); const allowMergeConfigs = () => action.features.getValue("allow_merge_config_files" /* AllowMergeConfigFiles */); if (inputs.configFile && isDefaultSetup(action.env) && await allowMergeConfigs()) { - const fromConfigInput = parseUserConfig( - action.logger, - "`config` input", - inputs.configInput, - validateConfig - ); const fromConfigFile = await loadUserConfig( action, inputs.configFile, @@ -150887,7 +150880,7 @@ async function determineUserConfig(action, tempDir, inputs) { ); const mergedConfig = mergeDefaultSetupAndUserConfigs( action.logger, - fromConfigInput, + inputs.configInput, fromConfigFile ); fs10.writeFileSync(computedConfigPath, dump(mergedConfig)); @@ -150902,11 +150895,12 @@ async function determineUserConfig(action, tempDir, inputs) { `Both a config file and config input were provided. Ignoring config file.` ); } - fs10.writeFileSync(computedConfigPath, inputs.configInput); + fs10.writeFileSync(computedConfigPath, dump(inputs.configInput)); inputs.configFile = computedConfigPath; action.logger.debug( `Using config from action input: ${inputs.configFile}` ); + return inputs.configInput; } } if (!inputs.configFile) { @@ -162396,7 +162390,10 @@ async function run3(actionState) { const rawLanguages = getRawLanguagesNoAutodetect( getOptionalInput("languages") ); - const configInput = getOptionalInput("config"); + const configInput = await parseConfigInput( + actionStateWithFeatures, + getOptionalInput("config") + ); const queriesInput = getOptionalInput("queries"); const otherLanguagePacksReason = getOtherLanguagePacksReason({ configFile, diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 29d72f3af4..3ea71ebc42 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -473,6 +473,9 @@ const simpleConfigFileContents = ` queries: - uses: ./foo_file`; +/** The configuration in `simpleConfigFileContents`, as parsed from the `config` input. */ +const simpleConfigInput = yaml.load(simpleConfigFileContents) as UserConfig; + /** A less minimal configuration file. */ const otherConfigFileContents = ` name: my config @@ -591,7 +594,7 @@ test.serial( createTestInitConfigInputs({ languagesInput, configFile: configFilePath, - configInput, + configInput: yaml.load(configInput) as UserConfig, tempDir, codeql, workspacePath: tempDir, @@ -2343,6 +2346,40 @@ test("applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only r ]); }); +test("parseConfigInput - returns undefined when the input isn't set", async (t) => { + await callee(configUtils.parseConfigInput) + .withArgs(undefined) + .passes(t.is, undefined); +}); + +test("parseConfigInput - parses the input as YAML", async (t) => { + await callee(configUtils.parseConfigInput) + .withArgs(simpleConfigFileContents) + .passes(t.deepEqual, { + name: "my config", + queries: [{ uses: "./foo_file" }], + }); +}); + +test("parseConfigInput - throws a ConfigurationError naming the input if it isn't valid YAML", async (t) => { + await callee(configUtils.parseConfigInput) + .withArgs("queries: [") + .throws(t, { + instanceOf: ConfigurationError, + message: /^Cannot parse "`config` input"/, + }); +}); + +test("parseConfigInput - throws a ConfigurationError naming the input if validation fails", async (t) => { + await callee(configUtils.parseConfigInput) + .withFeatures([Feature.ValidateDbConfig]) + .withArgs("queries: 1") + .throws(t, { + instanceOf: ConfigurationError, + message: /^The configuration file "`config` input" is invalid/, + }); +}); + test("determineUserConfig - empty config when neither input is specified", async (t) => { await withTmpDir(async (tmpDir) => { const target = callee(configUtils.determineUserConfig) @@ -2413,7 +2450,7 @@ test("determineUserConfig - loads config input", async (t) => { const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); const inputs = createTestInitConfigInputs({ - configInput: simpleConfigFileContents, + configInput: simpleConfigInput, configFile: undefined, workspacePath: tmpDir, }); @@ -2423,17 +2460,15 @@ test("determineUserConfig - loads config input", async (t) => { 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. + .logs(t, `Using config from action input: ${expectedConfigPath}`) + // The message about no configuration input and the warning about both inputs should not have + // been logged. The generated config file isn't loaded, since the `config` input has already + // been parsed. .notLogs( t, "No configuration file was provided", "Both a config file and config input were provided. Ignoring config file.", + `Using configuration file: ${expectedConfigPath}`, ) // The loaded configuration should match `simpleConfigFileContents`. .passes(t.deepEqual, { @@ -2452,7 +2487,7 @@ test("determineUserConfig - ignores config file input when both specified", asyn const expectedConfigPath = configUtils.userConfigFromActionPath(tmpDir); const inputs = createTestInitConfigInputs({ - configInput: simpleConfigFileContents, + configInput: simpleConfigInput, configFile: configFilePath, workspacePath: tmpDir, }); @@ -2466,10 +2501,14 @@ test("determineUserConfig - ignores config file input when both specified", asyn .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 generated config file isn't loaded, since the `config` input has already been parsed. + .notLogs( + t, + "No configuration file was provided", + `Using configuration file: ${expectedConfigPath}`, + ) // The loaded configuration should match `simpleConfigFileContents`. .passes(t.deepEqual, { name: "my config", @@ -2481,12 +2520,12 @@ test("determineUserConfig - ignores config file input when both specified", asyn }); }); -/** A `config` input that we might get from Default Setup. */ -const defaultSetupConfigInput = ` +/** The configuration from a `config` input that we might get from Default Setup. */ +const defaultSetupConfigInput = yaml.load(` threat-models: [local, remote] default-setup: org: - model-packs: [foo, bar]`; + model-packs: [foo, bar]`) as UserConfig; test("determineUserConfig - merges configs if FF is enabled in Default Setup", async (t) => { await withTmpDir(async (tmpDir) => { @@ -2555,7 +2594,7 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is .withArgs( tmpDir, createTestInitConfigInputs({ - configInput: simpleConfigFileContents, + configInput: simpleConfigInput, configFile: configFilePath, workspacePath: tmpDir, }), @@ -2565,10 +2604,14 @@ test("determineUserConfig - ignores config file input in Default Setup if FF is .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 generated config file isn't loaded, since the `config` input has already been parsed. + .notLogs( + t, + "No configuration file was provided", + `Using configuration file: ${expectedConfigPath}`, + ) .passes(t.deepEqual, { name: "my config", queries: [{ uses: "./foo_file" }], @@ -2587,7 +2630,7 @@ test("determineUserConfig - ignores config file input outside Default Setup if F .withArgs( tmpDir, createTestInitConfigInputs({ - configInput: simpleConfigFileContents, + configInput: simpleConfigInput, configFile: configFilePath, workspacePath: tmpDir, }), @@ -2597,10 +2640,14 @@ test("determineUserConfig - ignores config file input outside Default Setup if F .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 generated config file isn't loaded, since the `config` input has already been parsed. + .notLogs( + t, + "No configuration file was provided", + `Using configuration file: ${expectedConfigPath}`, + ) .passes(t.deepEqual, { name: "my config", queries: [{ uses: "./foo_file" }], diff --git a/src/config-utils.ts b/src/config-utils.ts index 4dd14b2909..ba15138dcc 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -337,7 +337,8 @@ export interface InitConfigInputs { packsInput: string | undefined; configFile: string | undefined; dbLocation: string | undefined; - configInput: string | undefined; + /** The configuration from the `config` input. */ + configInput: UserConfig | undefined; buildModeInput: string | undefined; ramInput: string | undefined; dependencyCachingEnabled: string | undefined; @@ -1043,6 +1044,29 @@ export async function applyIncrementalAnalysisSettings( } } +/** + * Parses the `config` input, which contains a configuration in YAML. + * + * @returns The configuration, or `undefined` if the input isn't set. Unless configuration validation + * is enabled, the configuration might not be a mapping. + * @throws A `ConfigurationError` if the input isn't valid YAML or, when configuration validation is + * enabled, isn't a valid configuration. + */ +export async function parseConfigInput( + { logger, features }: ActionState<["Logger", "FeatureFlags"]>, + configInput: string | undefined, +): Promise { + if (configInput === undefined) { + return undefined; + } + return parseUserConfig( + logger, + "`config` input", + configInput, + await features.getValue(Feature.ValidateDbConfig), + ); +} + /** * Determines where to load the `UserConfig` for the CLI from and loads it. * @@ -1057,17 +1081,13 @@ export async function determineUserConfig( tempDir: string, inputs: InitConfigInputs, ): Promise { - const validateConfig = await action.features.getValue( - Feature.ValidateDbConfig, - ); - // 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.configInput !== undefined) { const computedConfigPath = userConfigFromActionPath(tempDir); // Get a function which enables us to determine whether the FF that allows us to @@ -1084,12 +1104,6 @@ export async function determineUserConfig( ) { // 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, @@ -1102,7 +1116,7 @@ export async function determineUserConfig( // the CLI or other CodeQL Action steps. const mergedConfig = mergeDefaultSetupAndUserConfigs( action.logger, - fromConfigInput, + inputs.configInput, fromConfigFile, ); fs.writeFileSync(computedConfigPath, yaml.dump(mergedConfig)); @@ -1122,12 +1136,13 @@ export async function determineUserConfig( ); } - // Write the `config` input straight to disk. - fs.writeFileSync(computedConfigPath, inputs.configInput); + // Write the `config` input to disk. + fs.writeFileSync(computedConfigPath, yaml.dump(inputs.configInput)); inputs.configFile = computedConfigPath; action.logger.debug( `Using config from action input: ${inputs.configFile}`, ); + return inputs.configInput; } } diff --git a/src/config/db-config.test.ts b/src/config/db-config.test.ts index ecf77194a4..76fa918015 100644 --- a/src/config/db-config.test.ts +++ b/src/config/db-config.test.ts @@ -626,6 +626,16 @@ test("mergeDefaultSetupAndUserConfigs - warns about invalid keys from Default Se ]); }); +/** 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 [ [ @@ -637,7 +647,12 @@ test("matchesDefaultSetupConfigSchema - returns true for configurations that onl "threat-models: [ local ]", "{}", ]) { - t.true(dbConfig.matchesDefaultSetupConfigSchema(contents), contents); + t.true( + dbConfig.matchesDefaultSetupConfigSchema( + parseUnvalidatedConfig(contents), + ), + contents, + ); } }); @@ -647,7 +662,12 @@ test("matchesDefaultSetupConfigSchema - returns false for configurations that us "paths-ignore: [ tests ]", "default-setup: { org: { model-packs: [], queries: [] } }", ]) { - t.false(dbConfig.matchesDefaultSetupConfigSchema(contents), contents); + t.false( + dbConfig.matchesDefaultSetupConfigSchema( + parseUnvalidatedConfig(contents), + ), + contents, + ); } }); @@ -656,12 +676,22 @@ test("matchesDefaultSetupConfigSchema - returns false for invalid Default Setup "threat-models: local", "default-setup: { org: { model-packs: [ 1 ] } }", ]) { - t.false(dbConfig.matchesDefaultSetupConfigSchema(contents), contents); + t.false( + dbConfig.matchesDefaultSetupConfigSchema( + parseUnvalidatedConfig(contents), + ), + contents, + ); } }); -test("matchesDefaultSetupConfigSchema - returns false for contents that aren't a YAML object", (t) => { - for (const contents of ["threat-models: [", "- threat-models", "local", ""]) { - t.false(dbConfig.matchesDefaultSetupConfigSchema(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, + ); } }); diff --git a/src/config/db-config.ts b/src/config/db-config.ts index 977da17c72..e7924bf02e 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -106,24 +106,16 @@ function checkDefaultSetupConfig( } /** - * Returns whether `contents` is a YAML mapping that only sets the properties that Default Setup - * uses, which are threat models and model packs, to valid values. Returns `false` otherwise, - * including if `contents` isn't valid YAML. + * Returns whether `config` is a mapping that only sets the properties that Default Setup sets in + * the `config` input, to valid values. */ -export function matchesDefaultSetupConfigSchema(contents: string): boolean { - let config: unknown; - try { - config = yaml.load(contents); - } catch (error) { - if (error instanceof yaml.YAMLException) { - return false; - } - throw error; - } +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 = checkDefaultSetupConfig(config); + const result = checkDefaultSetupConfig(config as json.UnvalidatedObject); + // `valid` doesn't account for unknown properties. return result.valid && result.unknownKeys.length === 0; } diff --git a/src/init-action.ts b/src/init-action.ts index e1ac5df5ec..2b3c95e0ba 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -306,7 +306,10 @@ async function run( const rawLanguages = configUtils.getRawLanguagesNoAutodetect( getOptionalInput("languages"), ); - const configInput = getOptionalInput("config"); + const configInput = await configUtils.parseConfigInput( + actionStateWithFeatures, + getOptionalInput("config"), + ); const queriesInput = getOptionalInput("queries"); const otherLanguagePacksReason = getOtherLanguagePacksReason({ configFile, diff --git a/src/per-language-bundles.test.ts b/src/per-language-bundles.test.ts index c94b8d6dba..09500d2f77 100644 --- a/src/per-language-bundles.test.ts +++ b/src/per-language-bundles.test.ts @@ -227,13 +227,13 @@ test("getOtherLanguagePacksReason returns undefined for a config input that only t.is( getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, - // The shape of the `config` input that default setup passes. - configInput: [ - "default-setup:", - " org:", - " model-packs: [ github/immutable-actions-list@0.0.1 ]", - "threat-models: [ ]", - ].join("\n"), + // The configuration from the `config` input that default setup passes. + configInput: { + "default-setup": { + org: { "model-packs": ["github/immutable-actions-list@0.0.1"] }, + }, + "threat-models": [], + }, }), undefined, ); @@ -254,7 +254,7 @@ test("getOtherLanguagePacksReason explains a config input that uses other proper t.is( getOtherLanguagePacksReason({ ...NO_QUERY_CONFIG, - configInput: "queries: [ { uses: ./queries/show_ifs.ql } ]", + configInput: { queries: [{ uses: "./queries/show_ifs.ql" }] }, }), "the 'config' input may use queries that need library packs for other languages", ); diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 308fdcccc5..3a481c2444 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -7,6 +7,7 @@ import { matchesDefaultSetupConfigSchema, parseQueriesFromInput, QuerySpec, + UserConfig, } from "./config/db-config"; import { Feature } from "./feature-flags"; import { RepositoryPropertyName } from "./feature-flags/properties"; @@ -42,8 +43,8 @@ const PER_LANGUAGE_BUNDLE_LANGUAGES: Readonly< export interface QueryConfigInputs { /** The configuration file from the `config-file` input or repository property. */ configFile: string | undefined; - /** The `config` input. */ - configInput: string | undefined; + /** The configuration from the `config` input. */ + configInput: UserConfig | undefined; /** The `queries` input. */ queriesInput: string | undefined; /** The `github-codeql-extra-queries` repository property. */ From f6a7f00613135d9752f3c30b62ed48479c534503 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 2 Oct 2026 14:30:02 +0100 Subject: [PATCH 18/18] Point to the Default Setup config schema from the per-language bundle check Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/config/db-config.ts | 6 +++++- src/per-language-bundles.ts | 9 +++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/config/db-config.ts b/src/config/db-config.ts index e7924bf02e..eb2cf15eb7 100644 --- a/src/config/db-config.ts +++ b/src/config/db-config.ts @@ -86,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 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( diff --git a/src/per-language-bundles.ts b/src/per-language-bundles.ts index 3a481c2444..9a46d2df66 100644 --- a/src/per-language-bundles.ts +++ b/src/per-language-bundles.ts @@ -58,8 +58,8 @@ export interface QueryConfigInputs { * query packs are downloaded together with their dependencies. * * Any configuration file is assumed to configure such queries, since reading it may need file or API - * access. The `config` input is only assumed to if it sets anything other than valid threat models - * and model packs, which are the properties that default setup uses. + * access. So is the `config` input, unless it only sets the properties that default setup sets (see + * `matchesDefaultSetupConfigSchema`). * * @throws A `ConfigurationError` if the `queries` input or the `github-codeql-extra-queries` * repository property is a '+' with no queries after it, unless an input that's checked earlier @@ -75,8 +75,9 @@ export function getOtherLanguagePacksReason( ); } - // The `config` input can configure queries in the same way as a configuration file. Default - // setup only uses it for threat models and model packs, neither of which adds queries. + // The `config` input can configure queries in the same way as a configuration file. The + // properties that default setup sets, listed in `DEFAULT_SETUP_CONFIG_SCHEMA` in + // `config/db-config.ts`, don't add queries. if ( inputs.configInput !== undefined && !matchesDefaultSetupConfigSchema(inputs.configInput)