diff --git a/lib/entry-points.js b/lib/entry-points.js index 46c44a8183..6aaa81918f 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146993,6 +146993,11 @@ var featureConfig = { envVar: "CODEQL_ACTION_PROXY_API_REQUESTS", minimumVersion: void 0 }, + ["remote_address_analysis_meta_var" /* RemoteAddressAnalysisMetaVar */]: { + defaultValue: false, + envVar: "CODEQL_ACTION_REMOTE_ADDRESS_ANALYSIS_META_VAR", + minimumVersion: void 0 + }, ["skip_file_coverage_on_prs" /* SkipFileCoverageOnPrs */]: { defaultValue: false, envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS", @@ -148524,7 +148529,23 @@ async function getConfigFileInput({ } return void 0; } -async function getRemoteConfig(actionState, configFile, apiDetails) { +function replaceMetaVars(configFileAddress, analysisKind) { + return configFileAddress.replaceAll("$kind", analysisKind); +} +async function getRemoteConfig(actionState, analysisKinds, configFile, apiDetails) { + const supportMetaVar = await actionState.features.getValue( + "remote_address_analysis_meta_var" /* RemoteAddressAnalysisMetaVar */ + ); + if (supportMetaVar && analysisKinds.length === 1) { + configFile = replaceMetaVars(configFile, analysisKinds[0]); + actionState.logger.debug( + `Remote file address after replacing meta variables: ${configFile}` + ); + } else if (supportMetaVar) { + actionState.logger.warning( + `Ignoring '${"remote_address_analysis_meta_var" /* RemoteAddressAnalysisMetaVar */}' feature, because multiple analysis kinds are enabled.` + ); + } const address = await parseRemoteFileAddress(actionState, configFile); const shouldProxyRequest = await actionState.features.getValue( "proxy_api_requests" /* ProxyApiRequests */ @@ -149386,7 +149407,7 @@ async function downloadCacheWithTime(codeQL, languages, logger) { const trapCacheDownloadTime = import_perf_hooks.performance.now() - start; return { trapCaches, trapCacheDownloadTime }; } -async function loadUserConfig(actionState, configFile, workspacePath, apiDetails, tempDir) { +async function loadUserConfig(actionState, analysisKinds, configFile, workspacePath, apiDetails, tempDir) { if (isLocal(configFile)) { if (configFile !== userConfigFromActionPath(tempDir)) { configFile = path10.resolve(workspacePath, configFile); @@ -149404,7 +149425,12 @@ async function loadUserConfig(actionState, configFile, workspacePath, apiDetails if (isExplicitRemotePath(configFile)) { configFile = configFile.substring(REMOTE_PATH_PREFIX.length); } - return await getRemoteConfig(actionState, configFile, apiDetails); + return await getRemoteConfig( + actionState, + analysisKinds, + configFile, + apiDetails + ); } } var OVERLAY_ANALYSIS_FEATURES = { @@ -149706,6 +149732,7 @@ async function determineUserConfig(action, tempDir, inputs) { ); const fromConfigFile = await loadUserConfig( action, + inputs.analysisKinds, inputs.configFile, inputs.workspacePath, inputs.apiDetails, @@ -149742,6 +149769,7 @@ async function determineUserConfig(action, tempDir, inputs) { action.logger.debug(`Using configuration file: ${inputs.configFile}`); return await loadUserConfig( action, + inputs.analysisKinds, inputs.configFile, inputs.workspacePath, inputs.apiDetails, diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index 84c709e72a..69d242cbbf 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2544,6 +2544,7 @@ test("loadUserConfig - loads local configuration files", async (t) => { ) => configUtils.loadUserConfig( actionState, + [AnalysisKind.CodeScanning], filePath, workspaceDir, SAMPLE_DOTCOM_API_DETAILS, @@ -2587,12 +2588,19 @@ test.serial("loadUserConfig - loads remote configuration files", async (t) => { const remoteAddress = "owner/repo/file@ref"; await callee(configUtils.loadUserConfig) - .withArgs(remoteAddress, tmpDir, SAMPLE_DOTCOM_API_DETAILS, tmpDir) + .withArgs( + [AnalysisKind.CodeScanning], + remoteAddress, + tmpDir, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + ) .passes(t.deepEqual, {}); t.true( getRemoteConfig.calledOnceWithExactly( sinon.match.any, + [AnalysisKind.CodeScanning], remoteAddress, SAMPLE_DOTCOM_API_DETAILS, ), @@ -2626,9 +2634,9 @@ test.serial( // match our expectations. We break it down like this to get // more useful test output. const args = getRemoteConfig.getCalls()[0].args; - t.is(args.length, 3); - t.deepEqual(args[1], address); - t.deepEqual(args[2], SAMPLE_DOTCOM_API_DETAILS); + t.is(args.length, 4); + t.deepEqual(args[2], address); + t.deepEqual(args[3], SAMPLE_DOTCOM_API_DETAILS); }; // Utility function to assert that `targetWithArgs` has not identified @@ -2665,6 +2673,7 @@ test.serial( // Prepare the test call to `loadUserConfig`. const targetWithArgs = target.withArgs( + [AnalysisKind.CodeScanning], address, tmpDir, SAMPLE_DOTCOM_API_DETAILS, diff --git a/src/config-utils.ts b/src/config-utils.ts index b5a880ba7b..bacf1f3cc8 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -484,6 +484,7 @@ async function downloadCacheWithTime( */ export async function loadUserConfig( actionState: ActionState<["Logger", "Env", "FeatureFlags"]>, + analysisKinds: AnalysisKind[], configFile: string, workspacePath: string, apiDetails: api.GitHubApiCombinedDetails, @@ -511,7 +512,12 @@ export async function loadUserConfig( if (isExplicitRemotePath(configFile)) { configFile = configFile.substring(REMOTE_PATH_PREFIX.length); } - return await getRemoteConfig(actionState, configFile, apiDetails); + return await getRemoteConfig( + actionState, + analysisKinds, + configFile, + apiDetails, + ); } } @@ -1071,6 +1077,7 @@ export async function determineUserConfig( ); const fromConfigFile = await loadUserConfig( action, + inputs.analysisKinds, inputs.configFile, inputs.workspacePath, inputs.apiDetails, @@ -1118,6 +1125,7 @@ export async function determineUserConfig( action.logger.debug(`Using configuration file: ${inputs.configFile}`); return await loadUserConfig( action, + inputs.analysisKinds, inputs.configFile, inputs.workspacePath, inputs.apiDetails, diff --git a/src/config/file.test.ts b/src/config/file.test.ts index 0833ad3d06..2cfcb9e182 100644 --- a/src/config/file.test.ts +++ b/src/config/file.test.ts @@ -13,6 +13,7 @@ import { setupTests, } from "../testing-utils"; +import type { UserConfig } from "./db-config"; import { getConfigFileInput, getRemoteConfig } from "./file"; setupTests(test); @@ -137,7 +138,11 @@ test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => { const target = callee(getRemoteConfig) .withDefaultActionsEnv() - .withArgs("file.yml", SAMPLE_DOTCOM_API_DETAILS); + .withArgs( + [AnalysisKind.CodeScanning], + "file.yml", + SAMPLE_DOTCOM_API_DETAILS, + ); // Should use it when the FF is enabled and the environment variables are set. await target @@ -164,3 +169,68 @@ test.serial("getRemoteConfig uses proxy when it is supposed to", async (t) => { .notLogs(t, "Using private registry proxy at 'http://localhost:1234'") .throws(t, { message: errorMessage }); }); + +test.serial("getRemoteConfig replaces meta variables", async (t) => { + const client = github.getOctokit("123"); + const response = { + data: { + content: Buffer.from("disable-default-queries: false").toString("base64"), + }, + }; + sinon.stub(client.rest.repos, "getContent").callsFake((params) => { + if (params?.path.endsWith("$kind.yml")) { + throw new Error(`Unexpected request path: ${params.path}`); + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return response as any; + }); + + sinon + .stub(api, "getApiClientWithExternalAuth") + .callsFake((_details, _proxy) => { + return client; + }); + + const target = callee(getRemoteConfig) + .withDefaultActionsEnv() + .withArgs( + [AnalysisKind.CodeScanning], + "owner/repo:file-$kind.yml", + SAMPLE_DOTCOM_API_DETAILS, + ); + + // Should replace the meta variable if the FF is enabled. + await target + .withFeatures([Feature.RemoteAddressAnalysisMetaVar]) + .logs( + t, + "Remote file address after replacing meta variables: owner/repo:file-code-scanning.yml", + ) + .passes(t.deepEqual, { + "disable-default-queries": false, + } satisfies UserConfig); + + // But not if the FF is off. + await target.throws(t, { + instanceOf: Error, + message: "Unexpected request path: file-$kind.yml", + }); + + // Or if there are multiple analysis kinds. + await callee(getRemoteConfig) + .withDefaultActionsEnv() + .withArgs( + [AnalysisKind.CodeScanning, AnalysisKind.CodeQuality], + "owner/repo:file-$kind.yml", + SAMPLE_DOTCOM_API_DETAILS, + ) + .withFeatures([Feature.RemoteAddressAnalysisMetaVar]) + .logs( + t, + `Ignoring '${Feature.RemoteAddressAnalysisMetaVar}' feature, because multiple analysis kinds are enabled.`, + ) + .throws(t, { + instanceOf: Error, + message: "Unexpected request path: file-$kind.yml", + }); +}); diff --git a/src/config/file.ts b/src/config/file.ts index be0e415a38..e981596220 100644 --- a/src/config/file.ts +++ b/src/config/file.ts @@ -80,6 +80,14 @@ export async function getConfigFileInput( return undefined; } +/** Replaces supported meta variables in `configFileAddress`. */ +export function replaceMetaVars( + configFileAddress: string, + analysisKind: AnalysisKind, +): string { + return configFileAddress.replaceAll("$kind", analysisKind); +} + /** * Attempts to fetch a `UserConfig` from a remote `address`. * @@ -91,9 +99,25 @@ export async function getConfigFileInput( */ export async function getRemoteConfig( actionState: ActionState<["Logger", "Env", "FeatureFlags"]>, + analysisKinds: AnalysisKind[], configFile: string, apiDetails: api.GitHubApiCombinedDetails, ): Promise { + const supportMetaVar = await actionState.features.getValue( + Feature.RemoteAddressAnalysisMetaVar, + ); + + if (supportMetaVar && analysisKinds.length === 1) { + configFile = replaceMetaVars(configFile, analysisKinds[0]); + actionState.logger.debug( + `Remote file address after replacing meta variables: ${configFile}`, + ); + } else if (supportMetaVar) { + actionState.logger.warning( + `Ignoring '${Feature.RemoteAddressAnalysisMetaVar}' feature, because multiple analysis kinds are enabled.`, + ); + } + const address = await parseRemoteFileAddress(actionState, configFile); const shouldProxyRequest = await actionState.features.getValue( diff --git a/src/feature-flags.ts b/src/feature-flags.ts index b3107af962..c858ddceca 100644 --- a/src/feature-flags.ts +++ b/src/feature-flags.ts @@ -137,6 +137,8 @@ export enum Feature { QaTelemetryEnabled = "qa_telemetry_enabled", /** Routes (some) API requests through the registry proxy. */ ProxyApiRequests = "proxy_api_requests", + /** Adds support for an analysis kind meta variable in remote addresses. */ + RemoteAddressAnalysisMetaVar = "remote_address_analysis_meta_var", /** Note that this currently only disables baseline file coverage information. */ SkipFileCoverageOnPrs = "skip_file_coverage_on_prs", StartProxyUseFeaturesRelease = "start_proxy_use_features_release", @@ -385,6 +387,11 @@ export const featureConfig = { envVar: "CODEQL_ACTION_PROXY_API_REQUESTS", minimumVersion: undefined, }, + [Feature.RemoteAddressAnalysisMetaVar]: { + defaultValue: false, + envVar: "CODEQL_ACTION_REMOTE_ADDRESS_ANALYSIS_META_VAR", + minimumVersion: undefined, + }, [Feature.SkipFileCoverageOnPrs]: { defaultValue: false, envVar: "CODEQL_ACTION_SKIP_FILE_COVERAGE_ON_PRS",