Merge pull request #2967 from github/cklin/overlay-feature-flags

Overlay: additional feature flags
This commit is contained in:
Chuan-kai Lin
2025-07-17 05:54:21 -07:00
committed by GitHub
22 changed files with 1321 additions and 435 deletions

View File

@@ -13,7 +13,7 @@ import {
} from "./actions-util";
import * as api from "./api-client";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import { type Config } from "./config-utils";
import { generateCodeScanningConfig, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { EnvVar } from "./environment";
import {
@@ -35,7 +35,7 @@ import { ToolsDownloadStatusReport } from "./tools-download";
import { ToolsFeature, isSupportedToolsFeature } from "./tools-features";
import { shouldEnableIndirectTracing } from "./tracer-config";
import * as util from "./util";
import { BuildMode, cloneObject, getErrorMessage } from "./util";
import { BuildMode, getErrorMessage } from "./util";
type Options = Array<string | number | boolean>;
@@ -574,7 +574,7 @@ export async function getCodeQLForCmd(
extraArgs.push(`--trace-process-name=${processName}`);
}
const codeScanningConfigFile = await generateCodeScanningConfig(
const codeScanningConfigFile = await writeCodeScanningConfigFile(
config,
logger,
);
@@ -1217,66 +1217,15 @@ async function runCli(
* @param config The configuration to use.
* @returns the path to the generated user configuration file.
*/
async function generateCodeScanningConfig(
async function writeCodeScanningConfigFile(
config: Config,
logger: Logger,
): Promise<string> {
const codeScanningConfigFile = getGeneratedCodeScanningConfigPath(config);
// make a copy so we can modify it
const augmentedConfig = cloneObject(config.originalUserInput);
// Inject the queries from the input
if (config.augmentationProperties.queriesInput) {
if (config.augmentationProperties.queriesInputCombines) {
augmentedConfig.queries = (augmentedConfig.queries || []).concat(
config.augmentationProperties.queriesInput,
);
} else {
augmentedConfig.queries = config.augmentationProperties.queriesInput;
}
}
if (augmentedConfig.queries?.length === 0) {
delete augmentedConfig.queries;
}
// Inject the packs from the input
if (config.augmentationProperties.packsInput) {
if (config.augmentationProperties.packsInputCombines) {
// At this point, we already know that this is a single-language analysis
if (Array.isArray(augmentedConfig.packs)) {
augmentedConfig.packs = (augmentedConfig.packs || []).concat(
config.augmentationProperties.packsInput,
);
} else if (!augmentedConfig.packs) {
augmentedConfig.packs = config.augmentationProperties.packsInput;
} else {
// At this point, we know there is only one language.
// If there were more than one language, an error would already have been thrown.
const language = Object.keys(augmentedConfig.packs)[0];
augmentedConfig.packs[language] = augmentedConfig.packs[
language
].concat(config.augmentationProperties.packsInput);
}
} else {
augmentedConfig.packs = config.augmentationProperties.packsInput;
}
}
if (Array.isArray(augmentedConfig.packs) && !augmentedConfig.packs.length) {
delete augmentedConfig.packs;
}
augmentedConfig["query-filters"] = [
// Ordering matters. If the first filter is an inclusion, it implicitly
// excludes all queries that are not included. If it is an exclusion,
// it implicitly includes all queries that are not excluded. So user
// filters (if any) should always be first to preserve intent.
...(augmentedConfig["query-filters"] || []),
...(config.augmentationProperties.extraQueryExclusions || []),
];
if (augmentedConfig["query-filters"]?.length === 0) {
delete augmentedConfig["query-filters"];
}
const augmentedConfig = generateCodeScanningConfig(
config.originalUserInput,
config.augmentationProperties,
);
logger.info(
`Writing augmented user configuration file to ${codeScanningConfigFile}`,

View File

@@ -818,16 +818,10 @@ const calculateAugmentationMacro = test.macro({
) => {
const actualAugmentationProperties =
await configUtils.calculateAugmentation(
getCachedCodeQL(),
{ owner: "github", repo: "repo" },
createFeatures([]),
rawPacksInput,
rawQueriesInput,
rawQualityQueriesInput,
languages,
"", // sourceRoot
undefined, // buildMode
mockLogger,
);
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
},
@@ -950,16 +944,10 @@ const calculateAugmentationErrorMacro = test.macro({
await t.throwsAsync(
() =>
configUtils.calculateAugmentation(
getCachedCodeQL(),
{ owner: "github", repo: "repo" },
createFeatures([]),
rawPacksInput,
rawQueriesInput,
rawQualityQueriesInput,
languages,
"", // sourceRoot
undefined, // buildMode
mockLogger,
),
{ message: expectedError },
);
@@ -1206,7 +1194,7 @@ for (const { displayName, language, feature } of [
interface OverlayDatabaseModeTestSetup {
overlayDatabaseEnvVar: string | undefined;
isFeatureEnabled: boolean;
features: Feature[];
isPullRequest: boolean;
isDefaultBranch: boolean;
repositoryOwner: string;
@@ -1214,11 +1202,12 @@ interface OverlayDatabaseModeTestSetup {
languages: Language[];
codeqlVersion: string;
gitRoot: string | undefined;
codeScanningConfig: configUtils.UserConfig;
}
const defaultOverlayDatabaseModeTestSetup: OverlayDatabaseModeTestSetup = {
overlayDatabaseEnvVar: undefined,
isFeatureEnabled: false,
features: [],
isPullRequest: false,
isDefaultBranch: false,
repositoryOwner: "github",
@@ -1226,6 +1215,7 @@ const defaultOverlayDatabaseModeTestSetup: OverlayDatabaseModeTestSetup = {
languages: [Language.javascript],
codeqlVersion: "2.21.0",
gitRoot: "/some/git/root",
codeScanningConfig: {},
};
const getOverlayDatabaseModeMacro = test.macro({
@@ -1259,9 +1249,7 @@ const getOverlayDatabaseModeMacro = test.macro({
}
// Mock feature flags
const features = createFeatures(
setup.isFeatureEnabled ? [Feature.OverlayAnalysis] : [],
);
const features = createFeatures(setup.features);
// Mock isAnalyzingPullRequest function
sinon
@@ -1294,6 +1282,7 @@ const getOverlayDatabaseModeMacro = test.macro({
setup.languages,
tempDir, // sourceRoot
setup.buildMode,
setup.codeScanningConfig,
logger,
);
@@ -1359,7 +1348,8 @@ test(
getOverlayDatabaseModeMacro,
"Ignore feature flag when analyzing non-default branch",
{
isFeatureEnabled: true,
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
@@ -1371,7 +1361,8 @@ test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when feature enabled",
{
isFeatureEnabled: true,
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isDefaultBranch: true,
},
{
@@ -1382,8 +1373,152 @@ test(
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when feature disabled",
"Overlay-base database on default branch when feature enabled with custom analysis",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when code-scanning feature enabled",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with disable-default-queries",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"disable-default-queries": true,
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with packs",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with queries",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
queries: [{ uses: "some-query.ql" }],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when code-scanning feature enabled with query-filters",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"query-filters": [{ include: { "security-severity": "high" } }],
} as configUtils.UserConfig,
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when only language-specific feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysisJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when only code-scanning feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysisCodeScanningJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay-base database on default branch when language-specific feature disabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis],
isDefaultBranch: true,
},
{
@@ -1396,7 +1531,8 @@ test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when feature enabled",
{
isFeatureEnabled: true,
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
},
{
@@ -1407,8 +1543,152 @@ test(
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when feature disabled",
"Overlay analysis on PR when feature enabled with custom analysis",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when code-scanning feature enabled",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with disable-default-queries",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"disable-default-queries": true,
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with packs",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
packs: ["some-custom-pack@1.0.0"],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with queries",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
queries: [{ uses: "some-query.ql" }],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when code-scanning feature enabled with query-filters",
{
languages: [Language.javascript],
features: [
Feature.OverlayAnalysis,
Feature.OverlayAnalysisCodeScanningJavascript,
],
codeScanningConfig: {
"query-filters": [{ include: { "security-severity": "high" } }],
} as configUtils.UserConfig,
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when only language-specific feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysisJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when only code-scanning feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysisCodeScanningJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay analysis on PR when language-specific feature disabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis],
isPullRequest: true,
},
{
@@ -1447,7 +1727,8 @@ test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by feature flag for dsp-testing",
{
isFeatureEnabled: true,
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "dsp-testing",
},
@@ -1461,7 +1742,8 @@ test(
getOverlayDatabaseModeMacro,
"No overlay PR analysis by feature flag for other-org",
{
isFeatureEnabled: true,
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "other-org",
},
@@ -1524,3 +1806,20 @@ test(
useOverlayDatabaseCaching: false,
},
);
// Exercise language-specific overlay analysis features code paths
for (const language in Language) {
test(
getOverlayDatabaseModeMacro,
`Check default overlay analysis feature for ${language}`,
{
languages: [language as Language],
features: [Feature.OverlayAnalysis],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
}

View File

@@ -8,7 +8,7 @@ import * as semver from "semver";
import { isAnalyzingPullRequest } from "./actions-util";
import * as api from "./api-client";
import { CachingKind, getCachingKind } from "./caching-utils";
import { CodeQL } from "./codeql";
import { type CodeQL } from "./codeql";
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { Feature, FeatureEnablement } from "./feature-flags";
import { getGitRoot, isAnalyzingDefaultBranch } from "./git-utils";
@@ -26,6 +26,7 @@ import {
ConfigurationError,
BuildMode,
codeQlVersionAtLeast,
cloneObject,
} from "./util";
// Property names from the user-supplied config file.
@@ -195,7 +196,7 @@ export interface AugmentationProperties {
/**
* Extra query exclusions to append to the config.
*/
extraQueryExclusions?: ExcludeQueryFilter[];
extraQueryExclusions: ExcludeQueryFilter[];
/**
* The overlay database mode to use.
@@ -459,17 +460,8 @@ export interface InitConfigInputs {
logger: Logger;
}
type GetDefaultConfigInputs = Omit<
InitConfigInputs,
"configFile" | "configInput"
>;
type LoadConfigInputs = Omit<InitConfigInputs, "configInput"> & {
configFile: string;
};
/**
* Get the default config for when the user has not supplied one.
* Get the default config, populated without user configuration file.
*/
export async function getDefaultConfig({
languagesInput,
@@ -486,11 +478,10 @@ export async function getDefaultConfig({
repository,
tempDir,
codeql,
sourceRoot,
githubVersion,
features,
logger,
}: GetDefaultConfigInputs): Promise<Config> {
}: InitConfigInputs): Promise<Config> {
const languages = await getLanguages(
codeql,
languagesInput,
@@ -506,16 +497,10 @@ export async function getDefaultConfig({
);
const augmentationProperties = await calculateAugmentation(
codeql,
repository,
features,
packsInput,
queriesInput,
qualityQueriesInput,
languages,
sourceRoot,
buildMode,
logger,
);
const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime(
@@ -562,34 +547,12 @@ async function downloadCacheWithTime(
return { trapCaches, trapCacheDownloadTime };
}
/**
* Load the config from the given file.
*/
async function loadConfig({
languagesInput,
queriesInput,
qualityQueriesInput,
packsInput,
buildModeInput,
configFile,
dbLocation,
trapCachingEnabled,
dependencyCachingEnabled,
debugMode,
debugArtifactName,
debugDatabaseName,
repository,
tempDir,
codeql,
workspacePath,
sourceRoot,
githubVersion,
apiDetails,
features,
logger,
}: LoadConfigInputs): Promise<Config> {
let parsedYAML: UserConfig;
async function loadUserConfig(
configFile: string,
workspacePath: string,
apiDetails: api.GitHubApiCombinedDetails,
tempDir: string,
): Promise<UserConfig> {
if (isLocal(configFile)) {
if (configFile !== userConfigFromActionPath(tempDir)) {
// If the config file is not generated by the Action, it should be relative to the workspace.
@@ -601,61 +564,10 @@ async function loadConfig({
);
}
}
parsedYAML = getLocalConfig(configFile);
return getLocalConfig(configFile);
} else {
parsedYAML = await getRemoteConfig(configFile, apiDetails);
return await getRemoteConfig(configFile, apiDetails);
}
const languages = await getLanguages(
codeql,
languagesInput,
repository,
logger,
);
const buildMode = await parseBuildModeInput(
buildModeInput,
languages,
features,
logger,
);
const augmentationProperties = await calculateAugmentation(
codeql,
repository,
features,
packsInput,
queriesInput,
qualityQueriesInput,
languages,
sourceRoot,
buildMode,
logger,
);
const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime(
trapCachingEnabled,
codeql,
languages,
logger,
);
return {
languages,
buildMode,
originalUserInput: parsedYAML,
tempDir,
codeQLCmd: codeql.getPath(),
gitHubVersion: githubVersion,
dbLocation: dbLocationOrDefault(dbLocation, tempDir),
debugMode,
debugArtifactName,
debugDatabaseName,
augmentationProperties,
trapCaches,
trapCacheDownloadTime,
dependencyCachingEnabled: getCachingKind(dependencyCachingEnabled),
};
}
/**
@@ -665,17 +577,11 @@ async function loadConfig({
* and the CLI does not know about these inputs so we need to inject them into
* the config file sent to the CLI.
*
* @param codeql The CodeQL object.
* @param repository The repository to analyze.
* @param features The feature enablement object.
* @param rawPacksInput The packs input from the action configuration.
* @param rawQueriesInput The queries input from the action configuration.
* @param languages The languages that the config file is for. If the packs input
* is non-empty, then there must be exactly one language. Otherwise, an
* error is thrown.
* @param sourceRoot The source root of the repository.
* @param buildMode The build mode to use.
* @param logger The logger to use for logging.
*
* @returns The properties that need to be augmented in the config file.
*
@@ -684,16 +590,10 @@ async function loadConfig({
*/
// exported for testing.
export async function calculateAugmentation(
codeql: CodeQL,
repository: RepositoryNwo,
features: FeatureEnablement,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
rawQualityQueriesInput: string | undefined,
languages: Language[],
sourceRoot: string,
buildMode: BuildMode | undefined,
logger: Logger,
): Promise<AugmentationProperties> {
const packsInputCombines = shouldCombine(rawPacksInput);
const packsInput = parsePacksFromInput(
@@ -706,42 +606,21 @@ export async function calculateAugmentation(
rawQueriesInput,
queriesInputCombines,
);
const { overlayDatabaseMode, useOverlayDatabaseCaching } =
await getOverlayDatabaseMode(
codeql,
repository,
features,
languages,
sourceRoot,
buildMode,
logger,
);
logger.info(
`Using overlay database mode: ${overlayDatabaseMode} ` +
`${useOverlayDatabaseCaching ? "with" : "without"} caching.`,
);
const qualityQueriesInput = parseQueriesFromInput(
rawQualityQueriesInput,
false,
);
const extraQueryExclusions: ExcludeQueryFilter[] = [];
if (await shouldPerformDiffInformedAnalysis(codeql, features, logger)) {
extraQueryExclusions.push({
exclude: { tags: "exclude-from-incremental" },
});
}
return {
packsInputCombines,
packsInput: packsInput?.[languages[0]],
queriesInput,
queriesInputCombines,
qualityQueriesInput,
extraQueryExclusions,
overlayDatabaseMode,
useOverlayDatabaseCaching,
extraQueryExclusions: [],
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
};
}
@@ -768,6 +647,78 @@ function parseQueriesFromInput(
return trimmedInput.split(",").map((query) => ({ uses: query.trim() }));
}
const OVERLAY_ANALYSIS_FEATURES: Record<Language, Feature> = {
actions: Feature.OverlayAnalysisActions,
cpp: Feature.OverlayAnalysisCpp,
csharp: Feature.OverlayAnalysisCsharp,
go: Feature.OverlayAnalysisGo,
java: Feature.OverlayAnalysisJava,
javascript: Feature.OverlayAnalysisJavascript,
python: Feature.OverlayAnalysisPython,
ruby: Feature.OverlayAnalysisRuby,
rust: Feature.OverlayAnalysisRust,
swift: Feature.OverlayAnalysisSwift,
};
const OVERLAY_ANALYSIS_CODE_SCANNING_FEATURES: Record<Language, Feature> = {
actions: Feature.OverlayAnalysisCodeScanningActions,
cpp: Feature.OverlayAnalysisCodeScanningCpp,
csharp: Feature.OverlayAnalysisCodeScanningCsharp,
go: Feature.OverlayAnalysisCodeScanningGo,
java: Feature.OverlayAnalysisCodeScanningJava,
javascript: Feature.OverlayAnalysisCodeScanningJavascript,
python: Feature.OverlayAnalysisCodeScanningPython,
ruby: Feature.OverlayAnalysisCodeScanningRuby,
rust: Feature.OverlayAnalysisCodeScanningRust,
swift: Feature.OverlayAnalysisCodeScanningSwift,
};
async function isOverlayAnalysisFeatureEnabled(
repository: RepositoryNwo,
features: FeatureEnablement,
codeql: CodeQL,
languages: Language[],
codeScanningConfig: UserConfig,
): Promise<boolean> {
// TODO: Remove the repository owner check once support for overlay analysis
// stabilizes, and no more backward-incompatible changes are expected.
if (!["github", "dsp-testing"].includes(repository.owner)) {
return false;
}
if (!(await features.getValue(Feature.OverlayAnalysis, codeql))) {
return false;
}
let enableForCodeScanningOnly = false;
for (const language of languages) {
const feature = OVERLAY_ANALYSIS_FEATURES[language];
if (feature && (await features.getValue(feature, codeql))) {
continue;
}
const codeScanningFeature =
OVERLAY_ANALYSIS_CODE_SCANNING_FEATURES[language];
if (
codeScanningFeature &&
(await features.getValue(codeScanningFeature, codeql))
) {
enableForCodeScanningOnly = true;
continue;
}
return false;
}
if (enableForCodeScanningOnly) {
// A code-scanning configuration runs only the (default) code-scanning suite
// if the default queries are not disabled, and no packs, queries, or
// query-filters are specified.
return (
codeScanningConfig["disable-default-queries"] !== true &&
codeScanningConfig.packs === undefined &&
codeScanningConfig.queries === undefined &&
codeScanningConfig["query-filters"] === undefined
);
}
return true;
}
/**
* Calculate and validate the overlay database mode and caching to use.
*
@@ -796,6 +747,7 @@ export async function getOverlayDatabaseMode(
languages: Language[],
sourceRoot: string,
buildMode: BuildMode | undefined,
codeScanningConfig: UserConfig,
logger: Logger,
): Promise<{
overlayDatabaseMode: OverlayDatabaseMode;
@@ -818,10 +770,13 @@ export async function getOverlayDatabaseMode(
"from the CODEQL_OVERLAY_DATABASE_MODE environment variable.",
);
} else if (
// TODO: Remove the repository owner check once support for overlay analysis
// stabilizes, and no more backward-incompatible changes are expected.
["github", "dsp-testing"].includes(repository.owner) &&
(await features.getValue(Feature.OverlayAnalysis, codeql))
await isOverlayAnalysisFeatureEnabled(
repository,
features,
codeql,
languages,
codeScanningConfig,
)
) {
if (isAnalyzingPullRequest()) {
overlayDatabaseMode = OverlayDatabaseMode.Overlay;
@@ -1053,8 +1008,6 @@ function userConfigFromActionPath(tempDir: string): string {
* a default config. The parsed config is then stored to a known location.
*/
export async function initConfig(inputs: InitConfigInputs): Promise<Config> {
let config: Config;
const { logger, tempDir } = inputs;
// if configInput is set, it takes precedence over configFile
@@ -1069,13 +1022,56 @@ export async function initConfig(inputs: InitConfigInputs): Promise<Config> {
logger.debug(`Using config from action input: ${inputs.configFile}`);
}
// If no config file was provided create an empty one
let userConfig: UserConfig = {};
if (!inputs.configFile) {
logger.debug("No configuration file was provided");
config = await getDefaultConfig(inputs);
} else {
// Convince the type checker that inputs.configFile is defined.
config = await loadConfig({ ...inputs, configFile: inputs.configFile });
logger.debug(`Using configuration file: ${inputs.configFile}`);
userConfig = await loadUserConfig(
inputs.configFile,
inputs.workspacePath,
inputs.apiDetails,
tempDir,
);
}
const config = await getDefaultConfig(inputs);
const augmentationProperties = config.augmentationProperties;
config.originalUserInput = userConfig;
// The choice of overlay database mode depends on the selection of languages
// and queries, which in turn depends on the user config and the augmentation
// properties. So we need to calculate the overlay database mode after the
// rest of the config has been populated.
const { overlayDatabaseMode, useOverlayDatabaseCaching } =
await getOverlayDatabaseMode(
inputs.codeql,
inputs.repository,
inputs.features,
config.languages,
inputs.sourceRoot,
config.buildMode,
generateCodeScanningConfig(userConfig, augmentationProperties),
logger,
);
logger.info(
`Using overlay database mode: ${overlayDatabaseMode} ` +
`${useOverlayDatabaseCaching ? "with" : "without"} caching.`,
);
augmentationProperties.overlayDatabaseMode = overlayDatabaseMode;
augmentationProperties.useOverlayDatabaseCaching = useOverlayDatabaseCaching;
if (
overlayDatabaseMode === OverlayDatabaseMode.Overlay ||
(await shouldPerformDiffInformedAnalysis(
inputs.codeql,
inputs.features,
logger,
))
) {
augmentationProperties.extraQueryExclusions.push({
exclude: { tags: "exclude-from-incremental" },
});
}
// Save the config so we can easily access it again in the future
@@ -1355,3 +1351,64 @@ export async function parseBuildModeInput(
}
return input as BuildMode;
}
export function generateCodeScanningConfig(
originalUserInput: UserConfig,
augmentationProperties: AugmentationProperties,
): UserConfig {
// make a copy so we can modify it
const augmentedConfig = cloneObject(originalUserInput);
// Inject the queries from the input
if (augmentationProperties.queriesInput) {
if (augmentationProperties.queriesInputCombines) {
augmentedConfig.queries = (augmentedConfig.queries || []).concat(
augmentationProperties.queriesInput,
);
} else {
augmentedConfig.queries = augmentationProperties.queriesInput;
}
}
if (augmentedConfig.queries?.length === 0) {
delete augmentedConfig.queries;
}
// Inject the packs from the input
if (augmentationProperties.packsInput) {
if (augmentationProperties.packsInputCombines) {
// At this point, we already know that this is a single-language analysis
if (Array.isArray(augmentedConfig.packs)) {
augmentedConfig.packs = (augmentedConfig.packs || []).concat(
augmentationProperties.packsInput,
);
} else if (!augmentedConfig.packs) {
augmentedConfig.packs = augmentationProperties.packsInput;
} else {
// At this point, we know there is only one language.
// If there were more than one language, an error would already have been thrown.
const language = Object.keys(augmentedConfig.packs)[0];
augmentedConfig.packs[language] = augmentedConfig.packs[
language
].concat(augmentationProperties.packsInput);
}
} else {
augmentedConfig.packs = augmentationProperties.packsInput;
}
}
if (Array.isArray(augmentedConfig.packs) && !augmentedConfig.packs.length) {
delete augmentedConfig.packs;
}
augmentedConfig["query-filters"] = [
// Ordering matters. If the first filter is an inclusion, it implicitly
// excludes all queries that are not included. If it is an exclusion,
// it implicitly includes all queries that are not excluded. So user
// filters (if any) should always be first to preserve intent.
...(augmentedConfig["query-filters"] || []),
...augmentationProperties.extraQueryExclusions,
];
if (augmentedConfig["query-filters"]?.length === 0) {
delete augmentedConfig["query-filters"];
}
return augmentedConfig;
}

View File

@@ -15,11 +15,13 @@ import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
getRecordingLogger,
initializeFeatures,
LoggedMessage,
mockCodeQLVersion,
mockFeatureFlagApiEndpoint,
setupActionsVars,
setupTests,
stubFeatureFlagApiEndpoint,
} from "./testing-utils";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
@@ -131,6 +133,29 @@ test("Features use default value if they're not returned in API response", async
});
});
test("Include no more than 25 features in each API request", async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
stubFeatureFlagApiEndpoint((request) => {
const requestedFeatures = (request.features as string).split(",");
return {
status: requestedFeatures.length <= 25 ? 200 : 400,
messageIfError: "Can request a maximum of 25 features.",
data: {},
};
});
// We only need to call getValue once, and it does not matter which feature
// we ask for. Under the hood, the features library will request all features
// from the API.
const feature = Object.values(Feature)[0];
await t.notThrowsAsync(async () =>
features.getValue(feature, includeCodeQlIfRequired(feature)),
);
});
});
test("Feature flags exception is propagated if the API request errors", async (t) => {
await withTmpDir(async (tmpDir) => {
const features = setUpFeatureFlagTests(tmpDir);
@@ -552,13 +577,6 @@ function assertAllFeaturesUndefinedInApi(
}
}
export function initializeFeatures(initialValue: boolean) {
return Object.keys(featureConfig).reduce((features, key) => {
features[key] = initialValue;
return features;
}, {});
}
function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),

View File

@@ -54,6 +54,26 @@ export enum Feature {
ExportDiagnosticsEnabled = "export_diagnostics_enabled",
ExtractToToolcache = "extract_to_toolcache",
OverlayAnalysis = "overlay_analysis",
OverlayAnalysisActions = "overlay_analysis_actions",
OverlayAnalysisCodeScanningActions = "overlay_analysis_code_scanning_actions",
OverlayAnalysisCodeScanningCpp = "overlay_analysis_code_scanning_cpp",
OverlayAnalysisCodeScanningCsharp = "overlay_analysis_code_scanning_csharp",
OverlayAnalysisCodeScanningGo = "overlay_analysis_code_scanning_go",
OverlayAnalysisCodeScanningJava = "overlay_analysis_code_scanning_java",
OverlayAnalysisCodeScanningJavascript = "overlay_analysis_code_scanning_javascript",
OverlayAnalysisCodeScanningPython = "overlay_analysis_code_scanning_python",
OverlayAnalysisCodeScanningRuby = "overlay_analysis_code_scanning_ruby",
OverlayAnalysisCodeScanningRust = "overlay_analysis_code_scanning_rust",
OverlayAnalysisCodeScanningSwift = "overlay_analysis_code_scanning_swift",
OverlayAnalysisCpp = "overlay_analysis_cpp",
OverlayAnalysisCsharp = "overlay_analysis_csharp",
OverlayAnalysisGo = "overlay_analysis_go",
OverlayAnalysisJava = "overlay_analysis_java",
OverlayAnalysisJavascript = "overlay_analysis_javascript",
OverlayAnalysisPython = "overlay_analysis_python",
OverlayAnalysisRuby = "overlay_analysis_ruby",
OverlayAnalysisRust = "overlay_analysis_rust",
OverlayAnalysisSwift = "overlay_analysis_swift",
PythonDefaultIsToNotExtractStdlib = "python_default_is_to_not_extract_stdlib",
QaTelemetryEnabled = "qa_telemetry_enabled",
RustAnalysis = "rust_analysis",
@@ -155,6 +175,106 @@ export const featureConfig: Record<
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS",
minimumVersion: CODEQL_OVERLAY_MINIMUM_VERSION,
},
[Feature.OverlayAnalysisActions]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_ACTIONS",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningActions]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_ACTIONS",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningCpp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_CPP",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningCsharp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_CSHARP",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningGo]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_GO",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningJava]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_JAVA",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningJavascript]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_JAVASCRIPT",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningPython]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_PYTHON",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningRuby]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_RUBY",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningRust]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_RUST",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCodeScanningSwift]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CODE_SCANNING_SWIFT",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCpp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CPP",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisCsharp]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_CSHARP",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisGo]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_GO",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisJava]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_JAVA",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisJavascript]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_JAVASCRIPT",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisPython]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_PYTHON",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisRuby]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RUBY",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisRust]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_RUST",
minimumVersion: undefined,
},
[Feature.OverlayAnalysisSwift]: {
defaultValue: false,
envVar: "CODEQL_ACTION_OVERLAY_ANALYSIS_SWIFT",
minimumVersion: undefined,
},
[Feature.PythonDefaultIsToNotExtractStdlib]: {
defaultValue: false,
envVar: "CODEQL_ACTION_DISABLE_PYTHON_STANDARD_LIBRARY_EXTRACTION",
@@ -502,18 +622,29 @@ class GitHubFeatureFlags {
try {
const featuresToRequest = Object.entries(featureConfig)
.filter(([, config]) => !config.legacyApi)
.map(([f]) => f)
.join(",");
.map(([f]) => f);
const FEATURES_PER_REQUEST = 25;
const featureChunks: string[][] = [];
while (featuresToRequest.length > 0) {
featureChunks.push(featuresToRequest.splice(0, FEATURES_PER_REQUEST));
}
let remoteFlags: GitHubFeatureFlagsApiResponse = {};
for (const chunk of featureChunks) {
const response = await getApiClient().request(
"GET /repos/:owner/:repo/code-scanning/codeql-action/features",
{
owner: this.repositoryNwo.owner,
repo: this.repositoryNwo.repo,
features: chunk.join(","),
},
);
const chunkFlags = response.data as GitHubFeatureFlagsApiResponse;
remoteFlags = { ...remoteFlags, ...chunkFlags };
}
const response = await getApiClient().request(
"GET /repos/:owner/:repo/code-scanning/codeql-action/features",
{
owner: this.repositoryNwo.owner,
repo: this.repositoryNwo.repo,
features: featuresToRequest,
},
);
const remoteFlags = response.data as GitHubFeatureFlagsApiResponse;
this.logger.debug(
"Loaded the following default values for the feature flags from the Code Scanning API:",
);

View File

@@ -5,7 +5,6 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import { Feature, FeatureEnablement } from "./feature-flags";
import { initializeFeatures } from "./feature-flags.test";
import { getRunnerLogger } from "./logging";
import * as setupCodeql from "./setup-codeql";
import {
@@ -14,6 +13,7 @@ import {
SAMPLE_DEFAULT_CLI_VERSION,
SAMPLE_DOTCOM_API_DETAILS,
getRecordingLogger,
initializeFeatures,
mockBundleDownloadApi,
setupActionsVars,
setupTests,

View File

@@ -14,6 +14,7 @@ import * as defaults from "./defaults.json";
import {
CodeQLDefaultVersionInfo,
Feature,
featureConfig,
FeatureEnablement,
} from "./feature-flags";
import { Logger } from "./logging";
@@ -179,6 +180,21 @@ export function getRecordingLogger(messages: LoggedMessage[]): Logger {
export function mockFeatureFlagApiEndpoint(
responseStatusCode: number,
response: { [flagName: string]: boolean },
) {
stubFeatureFlagApiEndpoint(() => ({
status: responseStatusCode,
messageIfError: "some error message",
data: response,
}));
}
/** Stub the HTTP request to the feature flags enablement API endpoint. */
export function stubFeatureFlagApiEndpoint(
responseFunction: (params: any) => {
status: number;
messageIfError?: string;
data: { [flagName: string]: boolean };
},
) {
// Passing an auth token is required, so we just use a dummy value
const client = github.getOctokit("123");
@@ -188,16 +204,23 @@ export function mockFeatureFlagApiEndpoint(
const optInSpy = requestSpy.withArgs(
"GET /repos/:owner/:repo/code-scanning/codeql-action/features",
);
if (responseStatusCode < 300) {
optInSpy.resolves({
status: responseStatusCode,
data: response,
headers: {},
url: "GET /repos/:owner/:repo/code-scanning/codeql-action/features",
});
} else {
optInSpy.throws(new HTTPError("some error message", responseStatusCode));
}
optInSpy.callsFake((_route, params) => {
const response = responseFunction(params);
if (response.status < 300) {
return Promise.resolve({
status: response.status,
data: response.data,
headers: {},
url: "GET /repos/:owner/:repo/code-scanning/codeql-action/features",
});
} else {
throw new HTTPError(
response.messageIfError || "default stub error message",
response.status,
);
}
});
sinon.stub(apiClient, "getApiClient").value(() => client);
}
@@ -263,6 +286,13 @@ export function createFeatures(enabledFeatures: Feature[]): FeatureEnablement {
};
}
export function initializeFeatures(initialValue: boolean) {
return Object.keys(featureConfig).reduce((features, key) => {
features[key] = initialValue;
return features;
}, {});
}
/**
* Mocks the API for downloading the bundle tagged `tagName`.
*
@@ -335,6 +365,7 @@ export function createTestConfig(overrides: Partial<Config>): Config {
augmentationProperties: {
packsInputCombines: false,
queriesInputCombines: false,
extraQueryExclusions: [],
},
trapCaches: {},
trapCacheDownloadTime: 0,

View File

@@ -5,8 +5,8 @@ import * as actionsCache from "@actions/cache";
import * as actionsUtil from "./actions-util";
import * as apiClient from "./api-client";
import { CodeQL } from "./codeql";
import type { Config } from "./config-utils";
import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
import { Feature, FeatureEnablement } from "./feature-flags";
import * as gitUtils from "./git-utils";