Merge branch 'main' into update-bundle/codeql-bundle-v2.22.2

This commit is contained in:
Chuan-kai Lin
2025-07-29 07:36:07 -07:00
committed by GitHub
2326 changed files with 82820 additions and 22710 deletions

View File

@@ -47,6 +47,7 @@ test("analyze action with RAM & threads from environment variables", async (t) =
const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
requiredInputStub.withArgs("token").returns("fake-token");
requiredInputStub.withArgs("upload-database").returns("false");
requiredInputStub.withArgs("output").returns("out");
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
optionalInputStub.withArgs("cleanup-level").returns("none");
optionalInputStub.withArgs("expect-error").returns("false");

View File

@@ -45,6 +45,7 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
const requiredInputStub = sinon.stub(actionsUtil, "getRequiredInput");
requiredInputStub.withArgs("token").returns("fake-token");
requiredInputStub.withArgs("upload-database").returns("false");
requiredInputStub.withArgs("output").returns("out");
const optionalInputStub = sinon.stub(actionsUtil, "getOptionalInput");
optionalInputStub.withArgs("cleanup-level").returns("none");
optionalInputStub.withArgs("expect-error").returns("false");

View File

@@ -114,7 +114,9 @@ test("status report fields", async (t) => {
createFeatures([Feature.QaTelemetryEnabled]),
);
t.deepEqual(Object.keys(statusReport).sort(), [
"analysis_builds_overlay_base_database",
"analysis_is_diff_informed",
"analysis_is_overlay",
`analyze_builtin_queries_${language}_duration_ms`,
"event_reports",
`interpret_results_${language}_duration_ms`,

View File

@@ -132,6 +132,18 @@ export interface QueriesStatusReport {
*/
analysis_is_diff_informed?: boolean;
/**
* Whether the analysis runs in overlay mode (i.e., uses an overlay-base database).
* This is true if the AugmentationProperties.overlayDatabaseMode === Overlay.
*/
analysis_is_overlay?: boolean;
/**
* Whether the analysis builds an overlay-base database.
* This is true if the AugmentationProperties.overlayDatabaseMode === OverlayBase.
*/
analysis_builds_overlay_base_database?: boolean;
/** Name of language that errored during analysis (or undefined if no language failed). */
analyze_failure_language?: string;
/** Reports on discrete events associated with this status report. */
@@ -617,12 +629,20 @@ export async function runQueries(
queryFlags.push("--extension-packs=codeql-action/pr-diff-range");
incrementalMode.push("diff-informed");
}
statusReport.analysis_is_overlay =
config.augmentationProperties.overlayDatabaseMode ===
OverlayDatabaseMode.Overlay;
statusReport.analysis_builds_overlay_base_database =
config.augmentationProperties.overlayDatabaseMode ===
OverlayDatabaseMode.OverlayBase;
if (
config.augmentationProperties.overlayDatabaseMode ===
OverlayDatabaseMode.Overlay
) {
incrementalMode.push("overlay");
}
const sarifRunPropertyFlag =
incrementalMode.length > 0
? `--sarif-run-property=incrementalMode=${incrementalMode.join(",")}`

View File

@@ -1 +1 @@
{"maximumVersion": "3.18", "minimumVersion": "3.14"}
{"maximumVersion": "3.18", "minimumVersion": "3.13"}

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

@@ -0,0 +1,185 @@
import test, { ExecutionContext } from "ava";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import type { PullRequestBranches } from "./actions-util";
import * as apiClient from "./api-client";
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
import { Feature, Features } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import {
setupTests,
mockCodeQLVersion,
mockFeatureFlagApiEndpoint,
setupActionsVars,
} from "./testing-utils";
import { GitHubVariant, withTmpDir } from "./util";
import type { GitHubVersion } from "./util";
setupTests(test);
interface DiffInformedAnalysisTestCase {
featureEnabled: boolean;
gitHubVersion: GitHubVersion;
pullRequestBranches: PullRequestBranches;
codeQLVersion: string;
diffInformedQueriesEnvVar?: boolean;
}
const defaultTestCase: DiffInformedAnalysisTestCase = {
featureEnabled: true,
gitHubVersion: {
type: GitHubVariant.DOTCOM,
},
pullRequestBranches: {
base: "main",
head: "feature-branch",
},
codeQLVersion: "2.21.0",
};
const testShouldPerformDiffInformedAnalysis = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
partialTestCase: Partial<DiffInformedAnalysisTestCase>,
expectedResult: boolean,
) => {
return await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const testCase = { ...defaultTestCase, ...partialTestCase };
const logger = getRunnerLogger(true);
const codeql = mockCodeQLVersion(testCase.codeQLVersion);
if (testCase.diffInformedQueriesEnvVar !== undefined) {
process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES =
testCase.diffInformedQueriesEnvVar.toString();
} else {
delete process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES;
}
const features = new Features(
testCase.gitHubVersion,
parseRepositoryNwo("github/example"),
tmpDir,
logger,
);
mockFeatureFlagApiEndpoint(200, {
[Feature.DiffInformedQueries]: testCase.featureEnabled,
});
const getGitHubVersionStub = sinon
.stub(apiClient, "getGitHubVersion")
.resolves(testCase.gitHubVersion);
const getPullRequestBranchesStub = sinon
.stub(actionsUtil, "getPullRequestBranches")
.returns(testCase.pullRequestBranches);
const result = await shouldPerformDiffInformedAnalysis(
codeql,
features,
logger,
);
t.is(result, expectedResult);
delete process.env.CODEQL_ACTION_DIFF_INFORMED_QUERIES;
getGitHubVersionStub.restore();
getPullRequestBranchesStub.restore();
});
},
title: (_, title) => `shouldPerformDiffInformedAnalysis: ${title}`,
});
test(
testShouldPerformDiffInformedAnalysis,
"returns true in the default test case",
{},
true,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false when feature flag is disabled from the API",
{
featureEnabled: false,
},
false,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false when CODEQL_ACTION_DIFF_INFORMED_QUERIES is set to false",
{
featureEnabled: true,
diffInformedQueriesEnvVar: false,
},
false,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns true when CODEQL_ACTION_DIFF_INFORMED_QUERIES is set to true",
{
featureEnabled: false,
diffInformedQueriesEnvVar: true,
},
true,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false for CodeQL version 2.20.0",
{
codeQLVersion: "2.20.0",
},
false,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false for invalid GHES version",
{
gitHubVersion: {
type: GitHubVariant.GHES,
version: "invalid-version",
},
},
false,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false for GHES version 3.18.5",
{
gitHubVersion: {
type: GitHubVariant.GHES,
version: "3.18.5",
},
},
false,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns true for GHES version 3.19.0",
{
gitHubVersion: {
type: GitHubVariant.GHES,
version: "3.19.0",
},
},
true,
);
test(
testShouldPerformDiffInformedAnalysis,
"returns false when not a pull request",
{
pullRequestBranches: undefined,
},
false,
);

View File

@@ -3,9 +3,11 @@ import * as path from "path";
import * as actionsUtil from "./actions-util";
import type { PullRequestBranches } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import type { CodeQL } from "./codeql";
import { Feature, FeatureEnablement } from "./feature-flags";
import { Logger } from "./logging";
import { GitHubVariant, satisfiesGHESVersion } from "./util";
/**
* Check if the action should perform diff-informed analysis.
@@ -37,6 +39,14 @@ export async function getDiffInformedAnalysisBranches(
return undefined;
}
const gitHubVersion = await getGitHubVersion();
if (
gitHubVersion.type === GitHubVariant.GHES &&
satisfiesGHESVersion(gitHubVersion.version, "<3.19", true)
) {
return undefined;
}
const branches = actionsUtil.getPullRequestBranches();
if (!branches) {
logger.info(

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",
@@ -113,7 +133,7 @@ export const featureConfig: Record<
minimumVersion: "2.15.0",
},
[Feature.DiffInformedQueries]: {
defaultValue: false,
defaultValue: true,
envVar: "CODEQL_ACTION_DIFF_INFORMED_QUERIES",
minimumVersion: "2.21.0",
},
@@ -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

@@ -44,6 +44,7 @@ import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
OverlayBaseDatabaseDownloadStats,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { getRepositoryNwo } from "./repository";
@@ -107,6 +108,10 @@ interface InitWithConfigStatusReport extends InitStatusReport {
trap_cache_download_size_bytes: number;
/** Time taken to download TRAP caches, in milliseconds. */
trap_cache_download_duration_ms: number;
/** Size of the overlay-base database that we downloaded, in bytes. */
overlay_base_database_download_size_bytes?: number;
/** Time taken to download the overlay-base database, in milliseconds. */
overlay_base_database_download_duration_ms?: number;
/** Stringified JSON array of registry configuration objects, from the 'registries' config field
or workflow input. **/
registries: string;
@@ -134,6 +139,7 @@ async function sendCompletedStatusReport(
toolsFeatureFlagsValid: boolean | undefined,
toolsSource: ToolsSource,
toolsVersion: string,
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
logger: Logger,
error?: Error,
) {
@@ -237,6 +243,10 @@ async function sendCompletedStatusReport(
await getTotalCacheSize(Object.values(config.trapCaches), logger),
),
trap_cache_download_duration_ms: Math.round(config.trapCacheDownloadTime),
overlay_base_database_download_size_bytes:
overlayBaseDatabaseStats?.databaseSizeBytes,
overlay_base_database_download_duration_ms:
overlayBaseDatabaseStats?.databaseDownloadDurationMs,
query_filters: JSON.stringify(
config.originalUserInput["query-filters"] ?? [],
),
@@ -400,6 +410,7 @@ async function run() {
return;
}
let overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined;
try {
if (
config.augmentationProperties.overlayDatabaseMode ===
@@ -417,9 +428,12 @@ async function run() {
// necessary preparations. So, in that mode, we would assume that
// everything is in order and let the analysis fail if that turns out not
// to be the case.
const overlayDatabaseDownloaded =
await downloadOverlayBaseDatabaseFromCache(codeql, config, logger);
if (!overlayDatabaseDownloaded) {
overlayBaseDatabaseStats = await downloadOverlayBaseDatabaseFromCache(
codeql,
config,
logger,
);
if (!overlayBaseDatabaseStats) {
config.augmentationProperties.overlayDatabaseMode =
OverlayDatabaseMode.None;
logger.info(
@@ -729,6 +743,7 @@ async function run() {
toolsFeatureFlagsValid,
toolsSource,
toolsVersion,
overlayBaseDatabaseStats,
logger,
error,
);
@@ -744,6 +759,7 @@ async function run() {
toolsFeatureFlagsValid,
toolsSource,
toolsVersion,
overlayBaseDatabaseStats,
logger,
);
}

View File

@@ -1,6 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as actionsCache from "@actions/cache";
import test from "ava";
import * as sinon from "sinon";
@@ -8,10 +9,17 @@ import * as actionsUtil from "./actions-util";
import * as gitUtils from "./git-utils";
import { getRunnerLogger } from "./logging";
import {
downloadOverlayBaseDatabaseFromCache,
OverlayDatabaseMode,
writeBaseDatabaseOidsFile,
writeOverlayChangesFile,
} from "./overlay-database-utils";
import { createTestConfig, setupTests } from "./testing-utils";
import {
createTestConfig,
mockCodeQLVersion,
setupTests,
} from "./testing-utils";
import * as utils from "./util";
import { withTmpDir } from "./util";
setupTests(test);
@@ -75,3 +83,177 @@ test("writeOverlayChangesFile generates correct changes file", async (t) => {
);
});
});
interface DownloadOverlayBaseDatabaseTestCase {
overlayDatabaseMode: OverlayDatabaseMode;
useOverlayDatabaseCaching: boolean;
isInTestMode: boolean;
restoreCacheResult: string | undefined | Error;
hasBaseDatabaseOidsFile: boolean;
tryGetFolderBytesSucceeds: boolean;
codeQLVersion: string;
}
const defaultDownloadTestCase: DownloadOverlayBaseDatabaseTestCase = {
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
isInTestMode: false,
restoreCacheResult: "cache-key",
hasBaseDatabaseOidsFile: true,
tryGetFolderBytesSucceeds: true,
codeQLVersion: "2.20.5",
};
const testDownloadOverlayBaseDatabaseFromCache = test.macro({
exec: async (
t,
_title: string,
partialTestCase: Partial<DownloadOverlayBaseDatabaseTestCase>,
expectDownloadSuccess: boolean,
) => {
await withTmpDir(async (tmpDir) => {
const dbLocation = path.join(tmpDir, "db");
await fs.promises.mkdir(dbLocation, { recursive: true });
const logger = getRunnerLogger(true);
const config = createTestConfig({ dbLocation });
const testCase = { ...defaultDownloadTestCase, ...partialTestCase };
config.augmentationProperties.overlayDatabaseMode =
testCase.overlayDatabaseMode;
config.augmentationProperties.useOverlayDatabaseCaching =
testCase.useOverlayDatabaseCaching;
if (testCase.hasBaseDatabaseOidsFile) {
const baseDatabaseOidsFile = path.join(
dbLocation,
"base-database-oids.json",
);
await fs.promises.writeFile(baseDatabaseOidsFile, JSON.stringify({}));
}
const stubs: sinon.SinonStub[] = [];
const isInTestModeStub = sinon
.stub(utils, "isInTestMode")
.returns(testCase.isInTestMode);
stubs.push(isInTestModeStub);
if (testCase.restoreCacheResult instanceof Error) {
const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.rejects(testCase.restoreCacheResult);
stubs.push(restoreCacheStub);
} else {
const restoreCacheStub = sinon
.stub(actionsCache, "restoreCache")
.resolves(testCase.restoreCacheResult);
stubs.push(restoreCacheStub);
}
const tryGetFolderBytesStub = sinon
.stub(utils, "tryGetFolderBytes")
.resolves(testCase.tryGetFolderBytesSucceeds ? 1024 * 1024 : undefined);
stubs.push(tryGetFolderBytesStub);
try {
const result = await downloadOverlayBaseDatabaseFromCache(
mockCodeQLVersion(testCase.codeQLVersion),
config,
logger,
);
if (expectDownloadSuccess) {
t.truthy(result);
} else {
t.is(result, undefined);
}
} finally {
for (const stub of stubs) {
stub.restore();
}
}
});
},
title: (_, title) => `downloadOverlayBaseDatabaseFromCache: ${title}`,
});
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns stats when successful",
{},
true,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when mode is OverlayDatabaseMode.OverlayBase",
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when mode is OverlayDatabaseMode.None",
{
overlayDatabaseMode: OverlayDatabaseMode.None,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when caching is disabled",
{
useOverlayDatabaseCaching: false,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined in test mode",
{
isInTestMode: true,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when cache miss",
{
restoreCacheResult: undefined,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when download fails",
{
restoreCacheResult: new Error("Download failed"),
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when downloaded database is invalid",
{
hasBaseDatabaseOidsFile: false,
},
false,
);
test(
testDownloadOverlayBaseDatabaseFromCache,
"returns undefined when filesystem error occurs",
{
tryGetFolderBytesSucceeds: false,
},
false,
);

View File

@@ -8,7 +8,7 @@ import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { getCommitOid, getFileOidsUnderPath } from "./git-utils";
import { Logger } from "./logging";
import { isInTestMode, withTimeout } from "./util";
import { isInTestMode, tryGetFolderBytes, withTimeout } from "./util";
export enum OverlayDatabaseMode {
Overlay = "overlay",
@@ -235,6 +235,11 @@ export async function uploadOverlayBaseDatabaseToCache(
return true;
}
export interface OverlayBaseDatabaseDownloadStats {
databaseSizeBytes: number;
databaseDownloadDurationMs: number;
}
/**
* Downloads the overlay-base database from the GitHub Actions cache. If conditions
* for downloading are not met, the function does nothing and returns false.
@@ -242,34 +247,35 @@ export async function uploadOverlayBaseDatabaseToCache(
* @param codeql The CodeQL instance
* @param config The configuration object
* @param logger The logger instance
* @returns A promise that resolves to true if the download was performed and
* successfully completed, or false otherwise
* @returns A promise that resolves to download statistics if an overlay-base
* database was successfully downloaded, or undefined if the download was
* either not performed or failed.
*/
export async function downloadOverlayBaseDatabaseFromCache(
codeql: CodeQL,
config: Config,
logger: Logger,
): Promise<boolean> {
): Promise<OverlayBaseDatabaseDownloadStats | undefined> {
const overlayDatabaseMode = config.augmentationProperties.overlayDatabaseMode;
if (overlayDatabaseMode !== OverlayDatabaseMode.Overlay) {
logger.debug(
`Overlay database mode is ${overlayDatabaseMode}. ` +
"Skip downloading overlay-base database from cache.",
);
return false;
return undefined;
}
if (!config.augmentationProperties.useOverlayDatabaseCaching) {
logger.debug(
"Overlay database caching is disabled. " +
"Skip downloading overlay-base database from cache.",
);
return false;
return undefined;
}
if (isInTestMode()) {
logger.debug(
"In test mode. Skip downloading overlay-base database from cache.",
);
return false;
return undefined;
}
const dbLocation = config.dbLocation;
@@ -280,7 +286,9 @@ export async function downloadOverlayBaseDatabaseFromCache(
`Looking in Actions cache for overlay-base database with restore key ${restoreKey}`,
);
let databaseDownloadDurationMs = 0;
try {
const databaseDownloadStart = performance.now();
const foundKey = await withTimeout(
MAX_CACHE_OPERATION_MS,
actionsCache.restoreCache([dbLocation], restoreKey),
@@ -288,10 +296,13 @@ export async function downloadOverlayBaseDatabaseFromCache(
logger.info("Timed out downloading overlay-base database from cache");
},
);
databaseDownloadDurationMs = Math.round(
performance.now() - databaseDownloadStart,
);
if (foundKey === undefined) {
logger.info("No overlay-base database found in Actions cache");
return false;
return undefined;
}
logger.info(
@@ -302,7 +313,7 @@ export async function downloadOverlayBaseDatabaseFromCache(
"Failed to download overlay-base database from cache: " +
`${error instanceof Error ? error.message : String(error)}`,
);
return false;
return undefined;
}
const databaseIsValid = checkOverlayBaseDatabase(
@@ -312,11 +323,26 @@ export async function downloadOverlayBaseDatabaseFromCache(
);
if (!databaseIsValid) {
logger.warning("Downloaded overlay-base database failed validation");
return false;
return undefined;
}
const databaseSizeBytes = await tryGetFolderBytes(dbLocation, logger);
if (databaseSizeBytes === undefined) {
logger.info(
"Filesystem error while accessing downloaded overlay-base database",
);
// The problem that warrants reporting download failure is not that we are
// unable to determine the size of the database. Rather, it is that we
// encountered a filesystem error while accessing the database, which
// indicates that an overlay analysis will likely fail.
return undefined;
}
logger.info(`Successfully downloaded overlay-base database to ${dbLocation}`);
return true;
return {
databaseSizeBytes: Math.round(databaseSizeBytes),
databaseDownloadDurationMs,
};
}
async function generateCacheKey(

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";

View File

@@ -399,6 +399,18 @@ test("shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.14", async (t
);
});
test("shouldShowCombineSarifFilesDeprecationWarning when on GHES 3.16 pre", async (t) => {
t.true(
await uploadLib.shouldShowCombineSarifFilesDeprecationWarning(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
{
type: GitHubVariant.GHES,
version: "3.16.0.pre1",
},
),
);
});
test("shouldShowCombineSarifFilesDeprecationWarning with only 1 run", async (t) => {
t.false(
await uploadLib.shouldShowCombineSarifFilesDeprecationWarning(
@@ -454,6 +466,10 @@ test("throwIfCombineSarifFilesDisabled when on dotcom with feature flag", async
type: GitHubVariant.DOTCOM,
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
@@ -518,6 +534,10 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.18 pre", async (t) => {
version: "3.18.0.pre1",
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
@@ -531,6 +551,10 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.18 alpha", async (t) => {
version: "3.18.0-alpha.1",
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
@@ -544,6 +568,23 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => {
version: "3.18.0",
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
test("throwIfCombineSarifFilesDisabled with an invalid GHES version", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([]),
{
type: GitHubVariant.GHES,
version: "foobar",
},
),
);
});

View File

@@ -6,10 +6,8 @@ import * as core from "@actions/core";
import { OctokitResponse } from "@octokit/types";
import fileUrl from "file-url";
import * as jsonschema from "jsonschema";
import * as semver from "semver";
import * as actionsUtil from "./actions-util";
import { getOptionalInput, getRequiredInput } from "./actions-util";
import * as api from "./api-client";
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
@@ -30,6 +28,7 @@ import {
getRequiredEnvParam,
GitHubVariant,
GitHubVersion,
satisfiesGHESVersion,
SarifFile,
SarifRun,
} from "./util";
@@ -132,7 +131,7 @@ export async function shouldShowCombineSarifFilesDeprecationWarning(
// Do not show this warning on GHES versions before 3.14.0
if (
githubVersion.type === GitHubVariant.GHES &&
semver.lt(githubVersion.version, "3.14.0")
satisfiesGHESVersion(githubVersion.version, "<3.14", true)
) {
return false;
}
@@ -160,9 +159,8 @@ export async function throwIfCombineSarifFilesDisabled(
return;
}
// TODO: Update this changelog URL to the correct one when it's published.
const deprecationMoreInformationMessage =
"For more information, see https://github.blog/changelog/2024-05-06-code-scanning-will-stop-combining-runs-from-a-single-upload";
"For more information, see https://github.blog/changelog/2025-07-21-code-scanning-will-stop-combining-multiple-sarif-runs-uploaded-in-the-same-sarif-file/";
throw new ConfigurationError(
`The CodeQL Action does not support uploading multiple SARIF runs with the same category. Please update your workflow to upload a single run per category. ${deprecationMoreInformationMessage}`,
@@ -177,7 +175,7 @@ async function shouldDisableCombineSarifFiles(
) {
if (githubVersion.type === GitHubVariant.GHES) {
// Never block on GHES versions before 3.18.
if (semver.lt(githubVersion.version, "3.18.0-0")) {
if (satisfiesGHESVersion(githubVersion.version, "<3.18", true)) {
return false;
}
} else {
@@ -208,9 +206,6 @@ async function combineSarifFilesUsingCLI(
logger: Logger,
): Promise<SarifFile> {
logger.info("Combining SARIF files using the CodeQL CLI");
if (sarifFiles.length === 1) {
return JSON.parse(fs.readFileSync(sarifFiles[0], "utf8")) as SarifFile;
}
const sarifObjects = sarifFiles.map((sarifFile): SarifFile => {
return JSON.parse(fs.readFileSync(sarifFile, "utf8")) as SarifFile;
@@ -265,8 +260,10 @@ async function combineSarifFilesUsingCLI(
);
const apiDetails = {
auth: getRequiredInput("token"),
externalRepoAuth: getOptionalInput("external-repository-token"),
auth: actionsUtil.getRequiredInput("token"),
externalRepoAuth: actionsUtil.getOptionalInput(
"external-repository-token",
),
url: getRequiredEnvParam("GITHUB_SERVER_URL"),
apiURL: getRequiredEnvParam("GITHUB_API_URL"),
};
@@ -728,6 +725,9 @@ export async function uploadSpecifiedFiles(
const sarifPath = sarifPaths[0];
sarif = readSarifFile(sarifPath);
validateSarifFileSchema(sarif, sarifPath, logger);
// Validate that there are no runs for the same category
await throwIfCombineSarifFilesDisabled([sarif], features, gitHubVersion);
}
sarif = filterAlertsByDiffRange(logger, sarif);
@@ -896,6 +896,7 @@ export function shouldConsiderConfigurationError(
const expectedConfigErrors = [
"CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled",
"rejecting delivery as the repository has too many logical alerts",
"A delivery cannot contain multiple runs with the same category",
];
return (

View File

@@ -300,9 +300,10 @@ const shortTime = 10;
test("withTimeout on long task", async (t) => {
let longTaskTimedOut = false;
const longTask = new Promise((resolve) => {
setTimeout(() => {
const timer = setTimeout(() => {
resolve(42);
}, longTime);
t.teardown(() => clearTimeout(timer));
});
const result = await util.withTimeout(shortTime, longTask, () => {
longTaskTimedOut = true;

View File

@@ -1132,6 +1132,31 @@ export function checkActionVersion(
}
}
/**
* This will check whether the given GitHub version satisfies the given range,
* taking into account that a range like >=3.18 will also match the GHES 3.18
* pre-release/RC versions.
*
* When the given `githubVersion` is not a GHES version, or if the version
* is invalid, this will return `defaultIfInvalid`.
*/
export function satisfiesGHESVersion(
ghesVersion: string,
range: string,
defaultIfInvalid: boolean,
): boolean {
const semverVersion = semver.coerce(ghesVersion);
if (semverVersion === null) {
return defaultIfInvalid;
}
// We always drop the pre-release part of the version, since anything that
// applies to GHES 3.18.0 should also apply to GHES 3.18.0.pre1.
semverVersion.prerelease = [];
return semver.satisfies(semverVersion, range);
}
/**
* Supported build modes.
*