Merge branch 'main' into redsun82/rust

This commit is contained in:
Paolo Tranquilli
2025-08-04 17:24:20 +02:00
2355 changed files with 85511 additions and 23499 deletions
+131
View File
@@ -1,5 +1,7 @@
import * as github from "@actions/github";
import test from "ava";
import { getPullRequestBranches, isAnalyzingPullRequest } from "./actions-util";
import { computeAutomationID } from "./api-client";
import { EnvVar } from "./environment";
import { setupTests } from "./testing-utils";
@@ -7,6 +9,39 @@ import { initializeEnvironment } from "./util";
setupTests(test);
function withMockedContext<T>(mockPayload: any, testFn: () => T): T {
const originalPayload = github.context.payload;
github.context.payload = mockPayload;
try {
return testFn();
} finally {
github.context.payload = originalPayload;
}
}
function withMockedEnv<T>(
envVars: Record<string, string | undefined>,
testFn: () => T,
): T {
const originalEnv = { ...process.env };
// Apply environment changes
for (const [key, value] of Object.entries(envVars)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
try {
return testFn();
} finally {
// Restore original environment
process.env = originalEnv;
}
}
test("computeAutomationID()", async (t) => {
let actualAutomationID = computeAutomationID(
".github/workflows/codeql-analysis.yml:analyze",
@@ -58,6 +93,102 @@ test("computeAutomationID()", async (t) => {
);
});
test("getPullRequestBranches() with pull request context", (t) => {
withMockedContext(
{
pull_request: {
number: 123,
base: { ref: "main" },
head: { label: "user:feature-branch" },
},
},
() => {
t.deepEqual(getPullRequestBranches(), {
base: "main",
head: "user:feature-branch",
});
t.is(isAnalyzingPullRequest(), true);
},
);
});
test("getPullRequestBranches() returns undefined with push context", (t) => {
withMockedContext(
{
push: {
ref: "refs/heads/main",
},
},
() => {
t.is(getPullRequestBranches(), undefined);
t.is(isAnalyzingPullRequest(), false);
},
);
});
test("getPullRequestBranches() with Default Setup environment variables", (t) => {
withMockedContext({}, () => {
withMockedEnv(
{
CODE_SCANNING_REF: "refs/heads/feature-branch",
CODE_SCANNING_BASE_BRANCH: "main",
},
() => {
t.deepEqual(getPullRequestBranches(), {
base: "main",
head: "refs/heads/feature-branch",
});
t.is(isAnalyzingPullRequest(), true);
},
);
});
});
test("getPullRequestBranches() returns undefined when only CODE_SCANNING_REF is set", (t) => {
withMockedContext({}, () => {
withMockedEnv(
{
CODE_SCANNING_REF: "refs/heads/feature-branch",
CODE_SCANNING_BASE_BRANCH: undefined,
},
() => {
t.is(getPullRequestBranches(), undefined);
t.is(isAnalyzingPullRequest(), false);
},
);
});
});
test("getPullRequestBranches() returns undefined when only CODE_SCANNING_BASE_BRANCH is set", (t) => {
withMockedContext({}, () => {
withMockedEnv(
{
CODE_SCANNING_REF: undefined,
CODE_SCANNING_BASE_BRANCH: "main",
},
() => {
t.is(getPullRequestBranches(), undefined);
t.is(isAnalyzingPullRequest(), false);
},
);
});
});
test("getPullRequestBranches() returns undefined when no PR context", (t) => {
withMockedContext({}, () => {
withMockedEnv(
{
CODE_SCANNING_REF: undefined,
CODE_SCANNING_BASE_BRANCH: undefined,
},
() => {
t.is(getPullRequestBranches(), undefined);
t.is(isAnalyzingPullRequest(), false);
},
);
});
});
test("initializeEnvironment", (t) => {
initializeEnvironment("1.2.3");
t.deepEqual(process.env[EnvVar.VERSION], "1.2.3");
+46
View File
@@ -3,6 +3,7 @@ import * as path from "path";
import * as core from "@actions/core";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as github from "@actions/github";
import * as io from "@actions/io";
import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package";
@@ -363,3 +364,48 @@ export const restoreInputs = function () {
}
}
};
export interface PullRequestBranches {
base: string;
head: string;
}
/**
* Returns the base and head branches of the pull request being analyzed.
*
* @returns the base and head branches of the pull request, or undefined if
* we are not analyzing a pull request.
*/
export function getPullRequestBranches(): PullRequestBranches | undefined {
const pullRequest = github.context.payload.pull_request;
if (pullRequest) {
return {
base: pullRequest.base.ref,
// We use the head label instead of the head ref here, because the head
// ref lacks owner information and by itself does not uniquely identify
// the head branch (which may be in a forked repository).
head: pullRequest.head.label,
};
}
// PR analysis under Default Setup does not have the pull_request context,
// but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH.
const codeScanningRef = process.env.CODE_SCANNING_REF;
const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH;
if (codeScanningRef && codeScanningBaseBranch) {
return {
base: codeScanningBaseBranch,
// PR analysis under Default Setup analyzes the PR head commit instead of
// the merge commit, so we can use the provided ref directly.
head: codeScanningRef,
};
}
return undefined;
}
/**
* Returns whether we are analyzing a pull request.
*/
export function isAnalyzingPullRequest(): boolean {
return getPullRequestBranches() !== undefined;
}
+2
View File
@@ -39,6 +39,7 @@ test("analyze action with RAM & threads from environment variables", async (t) =
};
sinon.stub(configUtils, "getConfig").resolves({
gitHubVersion,
augmentationProperties: {},
languages: [],
packs: [],
trapCaches: {},
@@ -46,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");
+2
View File
@@ -37,6 +37,7 @@ test("analyze action with RAM & threads from action inputs", async (t) => {
};
sinon.stub(configUtils, "getConfig").resolves({
gitHubVersion,
augmentationProperties: {},
languages: [],
packs: [],
trapCaches: {},
@@ -44,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");
+15 -1
View File
@@ -27,6 +27,10 @@ import { EnvVar } from "./environment";
import { Features } from "./feature-flags";
import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import {
OverlayDatabaseMode,
uploadOverlayBaseDatabaseToCache,
} from "./overlay-database-utils";
import { getRepositoryNwo } from "./repository";
import * as statusReport from "./status-report";
import {
@@ -291,8 +295,15 @@ async function run() {
logger,
);
// An overlay-base database should always use the 'overlay' cleanup level
// to preserve the cached intermediate results.
//
// Note that we may be overriding the 'cleanup-level' input parameter.
const cleanupLevel =
actionsUtil.getOptionalInput("cleanup-level") || "brutal";
config.augmentationProperties.overlayDatabaseMode ===
OverlayDatabaseMode.OverlayBase
? "overlay"
: actionsUtil.getOptionalInput("cleanup-level") || "brutal";
if (actionsUtil.getRequiredInput("skip-queries") !== "true") {
runStats = await runQueries(
@@ -349,6 +360,9 @@ async function run() {
// Possibly upload the database bundles for remote queries
await uploadDatabases(repositoryNwo, config, apiDetails, logger);
// Possibly upload the overlay-base database to actions cache
await uploadOverlayBaseDatabaseToCache(codeql, config, logger);
// Possibly upload the TRAP caches for later re-use
const trapCacheUploadStartTime = performance.now();
didUploadTrapCaches = await uploadTrapCaches(codeql, config, logger);
+2
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`,
+40 -10
View File
@@ -6,7 +6,11 @@ import * as io from "@actions/io";
import del from "del";
import * as yaml from "js-yaml";
import * as actionsUtil from "./actions-util";
import {
getRequiredInput,
getTemporaryDirectory,
PullRequestBranches,
} from "./actions-util";
import { getApiClient } from "./api-client";
import { setupCppAutobuild } from "./autobuild";
import { CodeQL, getCodeQL } from "./codeql";
@@ -15,13 +19,13 @@ import { getJavaTempDependencyDir } from "./dependency-caching";
import { addDiagnostic, makeDiagnostic } from "./diagnostics";
import {
DiffThunkRange,
PullRequestBranches,
writeDiffRangesJsonFile,
} from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { FeatureEnablement, Feature } from "./feature-flags";
import { isScannedLanguage, Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import { OverlayDatabaseMode } from "./overlay-database-utils";
import { getRepositoryNwoFromEnv } from "./repository";
import { DatabaseCreationTimings, EventReport } from "./status-report";
import { endTracingForCluster } from "./tracer-config";
@@ -128,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. */
@@ -392,7 +408,7 @@ function getDiffRanges(
// uses forward slashes as the path separator, so on Windows we need to
// replace any backslashes with forward slashes.
const filename = path
.join(actionsUtil.getRequiredInput("checkout_path"), fileDiff.filename)
.join(getRequiredInput("checkout_path"), fileDiff.filename)
.replaceAll(path.sep, "/");
if (fileDiff.patch === undefined) {
@@ -498,10 +514,7 @@ function writeDiffRangeDataExtensionPack(
ranges = [{ path: "", startLine: 0, endLine: 0 }];
}
const diffRangeDir = path.join(
actionsUtil.getTemporaryDirectory(),
"pr-diff-range",
);
const diffRangeDir = path.join(getTemporaryDirectory(), "pr-diff-range");
// We expect the Actions temporary directory to already exist, so are mainly
// using `recursive: true` to avoid errors if the directory already exists,
@@ -604,6 +617,7 @@ export async function runQueries(
): Promise<QueriesStatusReport> {
const statusReport: QueriesStatusReport = {};
const queryFlags = [memoryFlag, threadsFlag];
const incrementalMode: string[] = [];
if (cleanupLevel !== "overlay") {
queryFlags.push("--expect-discarded-cache");
@@ -613,10 +627,26 @@ export async function runQueries(
if (diffRangePackDir) {
queryFlags.push(`--additional-packs=${diffRangePackDir}`);
queryFlags.push("--extension-packs=codeql-action/pr-diff-range");
incrementalMode.push("diff-informed");
}
const sarifRunPropertyFlag = diffRangePackDir
? "--sarif-run-property=incrementalMode=diff-informed"
: undefined;
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(",")}`
: undefined;
const codeql = await getCodeQL(config.codeQLCmd);
-5
View File
@@ -24,7 +24,6 @@ import { DocUrl } from "./doc-url";
import { FeatureEnablement } from "./feature-flags";
import { Language } from "./languages";
import { getRunnerLogger } from "./logging";
import { OverlayDatabaseMode } from "./overlay-database-utils";
import { ToolsSource } from "./setup-codeql";
import {
setupTests,
@@ -515,7 +514,6 @@ const injectedConfigMacro = test.macro({
"",
undefined,
undefined,
OverlayDatabaseMode.None,
getRunnerLogger(true),
);
@@ -726,7 +724,6 @@ test("passes a code scanning config AND qlconfig to the CLI", async (t: Executio
"",
undefined,
"/path/to/qlconfig.yml",
OverlayDatabaseMode.None,
getRunnerLogger(true),
);
@@ -756,7 +753,6 @@ test("does not pass a qlconfig to the CLI when it is undefined", async (t: Execu
"",
undefined,
undefined, // undefined qlconfigFile
OverlayDatabaseMode.None,
getRunnerLogger(true),
);
@@ -1010,7 +1006,6 @@ test("Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OP
"sourceRoot",
undefined,
undefined,
OverlayDatabaseMode.None,
getRunnerLogger(false),
);
+13 -64
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>;
@@ -87,7 +87,6 @@ export interface CodeQL {
sourceRoot: string,
processName: string | undefined,
qlconfigFile: string | undefined,
overlayDatabaseMode: OverlayDatabaseMode,
logger: Logger,
): Promise<void>;
/**
@@ -291,17 +290,17 @@ const CODEQL_MINIMUM_VERSION = "2.16.6";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
*/
const CODEQL_NEXT_MINIMUM_VERSION = "2.16.6";
const CODEQL_NEXT_MINIMUM_VERSION = "2.17.6";
/**
* This is the version of GHES that was most recently deprecated.
*/
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.12";
const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.13";
/**
* This is the deprecation date for the version of GHES that was most recently deprecated.
*/
const GHES_MOST_RECENT_DEPRECATION_DATE = "2025-04-03";
const GHES_MOST_RECENT_DEPRECATION_DATE = "2025-06-19";
/** The CLI verbosity level to use for extraction in debug mode. */
const EXTRACTION_DEBUG_MODE_VERBOSITY = "progress++";
@@ -564,7 +563,6 @@ export async function getCodeQLForCmd(
sourceRoot: string,
processName: string | undefined,
qlconfigFile: string | undefined,
overlayDatabaseMode: OverlayDatabaseMode,
logger: Logger,
) {
const extraArgs = config.languages.map(
@@ -576,7 +574,7 @@ export async function getCodeQLForCmd(
extraArgs.push(`--trace-process-name=${processName}`);
}
const codeScanningConfigFile = await generateCodeScanningConfig(
const codeScanningConfigFile = await writeCodeScanningConfigFile(
config,
logger,
);
@@ -602,6 +600,8 @@ export async function getCodeQLForCmd(
? "--force-overwrite"
: "--overwrite";
const overlayDatabaseMode =
config.augmentationProperties.overlayDatabaseMode;
if (overlayDatabaseMode === OverlayDatabaseMode.Overlay) {
const overlayChangesFile = await writeOverlayChangesFile(
config,
@@ -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}`,
+637 -6
View File
@@ -6,6 +6,7 @@ import test, { ExecutionContext } from "ava";
import * as yaml from "js-yaml";
import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as api from "./api-client";
import { CachingKind } from "./caching-utils";
import {
@@ -16,8 +17,10 @@ import {
} from "./codeql";
import * as configUtils from "./config-utils";
import { Feature } from "./feature-flags";
import * as gitUtils from "./git-utils";
import { Language } from "./languages";
import { getRunnerLogger } from "./logging";
import { OverlayDatabaseMode } from "./overlay-database-utils";
import { parseRepositoryNwo } from "./repository";
import {
setupTests,
@@ -25,6 +28,7 @@ import {
createFeatures,
getRecordingLogger,
LoggedMessage,
mockCodeQLVersion,
} from "./testing-utils";
import {
GitHubVariant,
@@ -62,6 +66,7 @@ function createTestInitConfigInputs(
tempDir: "",
codeql: {} as CodeQL,
workspacePath: "",
sourceRoot: "",
githubVersion,
apiDetails: {
auth: "token",
@@ -813,13 +818,10 @@ const calculateAugmentationMacro = test.macro({
) => {
const actualAugmentationProperties =
await configUtils.calculateAugmentation(
getCachedCodeQL(),
createFeatures([]),
rawPacksInput,
rawQueriesInput,
rawQualityQueriesInput,
languages,
mockLogger,
);
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
},
@@ -942,13 +944,10 @@ const calculateAugmentationErrorMacro = test.macro({
await t.throwsAsync(
() =>
configUtils.calculateAugmentation(
getCachedCodeQL(),
createFeatures([]),
rawPacksInput,
rawQueriesInput,
rawQualityQueriesInput,
languages,
mockLogger,
),
{ message: expectedError },
);
@@ -1192,3 +1191,635 @@ for (const { displayName, language, feature } of [
]);
});
}
interface OverlayDatabaseModeTestSetup {
overlayDatabaseEnvVar: string | undefined;
features: Feature[];
isPullRequest: boolean;
isDefaultBranch: boolean;
repositoryOwner: string;
buildMode: BuildMode | undefined;
languages: Language[];
codeqlVersion: string;
gitRoot: string | undefined;
codeScanningConfig: configUtils.UserConfig;
}
const defaultOverlayDatabaseModeTestSetup: OverlayDatabaseModeTestSetup = {
overlayDatabaseEnvVar: undefined,
features: [],
isPullRequest: false,
isDefaultBranch: false,
repositoryOwner: "github",
buildMode: BuildMode.None,
languages: [Language.javascript],
codeqlVersion: "2.21.0",
gitRoot: "/some/git/root",
codeScanningConfig: {},
};
const getOverlayDatabaseModeMacro = test.macro({
exec: async (
t: ExecutionContext,
_title: string,
setupOverrides: Partial<OverlayDatabaseModeTestSetup>,
expected: {
overlayDatabaseMode: OverlayDatabaseMode;
useOverlayDatabaseCaching: boolean;
},
) => {
return await withTmpDir(async (tempDir) => {
const messages: LoggedMessage[] = [];
const logger = getRecordingLogger(messages);
// Save the original environment
const originalEnv = { ...process.env };
try {
const setup = {
...defaultOverlayDatabaseModeTestSetup,
...setupOverrides,
};
// Set up environment variable if specified
delete process.env.CODEQL_OVERLAY_DATABASE_MODE;
if (setup.overlayDatabaseEnvVar !== undefined) {
process.env.CODEQL_OVERLAY_DATABASE_MODE =
setup.overlayDatabaseEnvVar;
}
// Mock feature flags
const features = createFeatures(setup.features);
// Mock isAnalyzingPullRequest function
sinon
.stub(actionsUtil, "isAnalyzingPullRequest")
.returns(setup.isPullRequest);
// Mock repository owner
const repository = {
owner: setup.repositoryOwner,
repo: "test-repo",
};
// Set up CodeQL mock
const codeql = mockCodeQLVersion(setup.codeqlVersion);
// Mock git root detection
if (setup.gitRoot !== undefined) {
sinon.stub(gitUtils, "getGitRoot").resolves(setup.gitRoot);
}
// Mock default branch detection
sinon
.stub(gitUtils, "isAnalyzingDefaultBranch")
.resolves(setup.isDefaultBranch);
const result = await configUtils.getOverlayDatabaseMode(
codeql,
repository,
features,
setup.languages,
tempDir, // sourceRoot
setup.buildMode,
setup.codeScanningConfig,
logger,
);
t.deepEqual(result, expected);
} finally {
// Restore the original environment
process.env = originalEnv;
}
});
},
title: (_, title) => `getOverlayDatabaseMode: ${title}`,
});
test(
getOverlayDatabaseModeMacro,
"Environment variable override - Overlay",
{
overlayDatabaseEnvVar: "overlay",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Environment variable override - OverlayBase",
{
overlayDatabaseEnvVar: "overlay-base",
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Environment variable override - None",
{
overlayDatabaseEnvVar: "none",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Ignore invalid environment variable",
{
overlayDatabaseEnvVar: "invalid-mode",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Ignore feature flag when analyzing non-default branch",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay-base database on default branch when feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isDefaultBranch: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.OverlayBase,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"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,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay analysis on PR when feature enabled",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"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,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by env for dsp-testing",
{
overlayDatabaseEnvVar: "overlay",
repositoryOwner: "dsp-testing",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by env for other-org",
{
overlayDatabaseEnvVar: "overlay",
repositoryOwner: "other-org",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Overlay PR analysis by feature flag for dsp-testing",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "dsp-testing",
},
{
overlayDatabaseMode: OverlayDatabaseMode.Overlay,
useOverlayDatabaseCaching: true,
},
);
test(
getOverlayDatabaseModeMacro,
"No overlay PR analysis by feature flag for other-org",
{
languages: [Language.javascript],
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
isPullRequest: true,
repositoryOwner: "other-org",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to autobuild with traced language",
{
overlayDatabaseEnvVar: "overlay",
buildMode: BuildMode.Autobuild,
languages: [Language.java],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to no build mode with traced language",
{
overlayDatabaseEnvVar: "overlay",
buildMode: undefined,
languages: [Language.java],
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to old CodeQL version",
{
overlayDatabaseEnvVar: "overlay",
codeqlVersion: "2.14.0",
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
},
);
test(
getOverlayDatabaseModeMacro,
"Fallback due to missing git root",
{
overlayDatabaseEnvVar: "overlay",
gitRoot: undefined,
},
{
overlayDatabaseMode: OverlayDatabaseMode.None,
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,
},
);
}
+340 -114
View File
@@ -5,13 +5,19 @@ import { performance } from "perf_hooks";
import * as yaml from "js-yaml";
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 { Language, parseLanguage } from "./languages";
import { getGitRoot, isAnalyzingDefaultBranch } from "./git-utils";
import { isTracedLanguage, Language, parseLanguage } from "./languages";
import { Logger } from "./logging";
import {
CODEQL_OVERLAY_MINIMUM_VERSION,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { RepositoryNwo } from "./repository";
import { downloadTrapCaches } from "./trap-caching";
import {
@@ -19,6 +25,8 @@ import {
prettyPrintPack,
ConfigurationError,
BuildMode,
codeQlVersionAtLeast,
cloneObject,
} from "./util";
// Property names from the user-supplied config file.
@@ -188,7 +196,24 @@ export interface AugmentationProperties {
/**
* Extra query exclusions to append to the config.
*/
extraQueryExclusions?: ExcludeQueryFilter[];
extraQueryExclusions: ExcludeQueryFilter[];
/**
* The overlay database mode to use.
*/
overlayDatabaseMode: OverlayDatabaseMode;
/**
* Whether to use caching for overlay databases. If it is true, the action
* will upload the created overlay-base database to the actions cache, and
* download an overlay-base database from the actions cache before it creates
* a new overlay database. If it is false, the action assumes that the
* workflow will be responsible for managing database storage and retrieval.
*
* This property has no effect unless `overlayDatabaseMode` is `Overlay` or
* `OverlayBase`.
*/
useOverlayDatabaseCaching: boolean;
}
/**
@@ -202,6 +227,8 @@ export const defaultAugmentationProperties: AugmentationProperties = {
queriesInput: undefined,
qualityQueriesInput: undefined,
extraQueryExclusions: [],
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
};
export type Packs = Partial<Record<Language, string[]>>;
@@ -426,23 +453,15 @@ export interface InitConfigInputs {
tempDir: string;
codeql: CodeQL;
workspacePath: string;
sourceRoot: string;
githubVersion: GitHubVersion;
apiDetails: api.GitHubApiCombinedDetails;
features: FeatureEnablement;
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,
@@ -462,7 +481,7 @@ export async function getDefaultConfig({
githubVersion,
features,
logger,
}: GetDefaultConfigInputs): Promise<Config> {
}: InitConfigInputs): Promise<Config> {
const languages = await getLanguages(
codeql,
languagesInput,
@@ -478,13 +497,10 @@ export async function getDefaultConfig({
);
const augmentationProperties = await calculateAugmentation(
codeql,
features,
packsInput,
queriesInput,
qualityQueriesInput,
languages,
logger,
);
const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime(
@@ -531,33 +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,
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.
@@ -569,58 +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,
features,
packsInput,
queriesInput,
qualityQueriesInput,
languages,
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),
};
}
/**
@@ -630,14 +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 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 logger The logger to use for logging.
*
* @returns The properties that need to be augmented in the config file.
*
@@ -646,13 +590,10 @@ async function loadConfig({
*/
// exported for testing.
export async function calculateAugmentation(
codeql: CodeQL,
features: FeatureEnablement,
rawPacksInput: string | undefined,
rawQueriesInput: string | undefined,
rawQualityQueriesInput: string | undefined,
languages: Language[],
logger: Logger,
): Promise<AugmentationProperties> {
const packsInputCombines = shouldCombine(rawPacksInput);
const packsInput = parsePacksFromInput(
@@ -671,20 +612,15 @@ export async function calculateAugmentation(
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,
extraQueryExclusions: [],
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
};
}
@@ -711,6 +647,194 @@ 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.
*
* - If the environment variable `CODEQL_OVERLAY_DATABASE_MODE` is set, use it.
* In this case, the workflow is responsible for managing database storage and
* retrieval, and the action will not perform overlay database caching. Think
* of it as a "manual control" mode where the calling workflow is responsible
* for making sure that everything is set up correctly.
* - Otherwise, if `Feature.OverlayAnalysis` is enabled, calculate the mode
* based on what we are analyzing. Think of it as a "automatic control" mode
* where the action will do the right thing by itself.
* - If we are analyzing a pull request, use `Overlay` with caching.
* - If we are analyzing the default branch, use `OverlayBase` with caching.
* - Otherwise, use `None`.
*
* For `Overlay` and `OverlayBase`, the function performs further checks and
* reverts to `None` if any check should fail.
*
* @returns An object containing the overlay database mode and whether the
* action should perform overlay-base database caching.
*/
export async function getOverlayDatabaseMode(
codeql: CodeQL,
repository: RepositoryNwo,
features: FeatureEnablement,
languages: Language[],
sourceRoot: string,
buildMode: BuildMode | undefined,
codeScanningConfig: UserConfig,
logger: Logger,
): Promise<{
overlayDatabaseMode: OverlayDatabaseMode;
useOverlayDatabaseCaching: boolean;
}> {
let overlayDatabaseMode = OverlayDatabaseMode.None;
let useOverlayDatabaseCaching = false;
const modeEnv = process.env.CODEQL_OVERLAY_DATABASE_MODE;
// Any unrecognized CODEQL_OVERLAY_DATABASE_MODE value will be ignored and
// treated as if the environment variable was not set.
if (
modeEnv === OverlayDatabaseMode.Overlay ||
modeEnv === OverlayDatabaseMode.OverlayBase ||
modeEnv === OverlayDatabaseMode.None
) {
overlayDatabaseMode = modeEnv;
logger.info(
`Setting overlay database mode to ${overlayDatabaseMode} ` +
"from the CODEQL_OVERLAY_DATABASE_MODE environment variable.",
);
} else if (
await isOverlayAnalysisFeatureEnabled(
repository,
features,
codeql,
languages,
codeScanningConfig,
)
) {
if (isAnalyzingPullRequest()) {
overlayDatabaseMode = OverlayDatabaseMode.Overlay;
useOverlayDatabaseCaching = true;
logger.info(
`Setting overlay database mode to ${overlayDatabaseMode} ` +
"with caching because we are analyzing a pull request.",
);
} else if (await isAnalyzingDefaultBranch()) {
overlayDatabaseMode = OverlayDatabaseMode.OverlayBase;
useOverlayDatabaseCaching = true;
logger.info(
`Setting overlay database mode to ${overlayDatabaseMode} ` +
"with caching because we are analyzing the default branch.",
);
}
}
const nonOverlayAnalysis = {
overlayDatabaseMode: OverlayDatabaseMode.None,
useOverlayDatabaseCaching: false,
};
if (overlayDatabaseMode === OverlayDatabaseMode.None) {
return nonOverlayAnalysis;
}
if (buildMode !== BuildMode.None && languages.some(isTracedLanguage)) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`build-mode is set to "${buildMode}" instead of "none". ` +
"Falling back to creating a normal full database instead.",
);
return nonOverlayAnalysis;
}
if (!(await codeQlVersionAtLeast(codeql, CODEQL_OVERLAY_MINIMUM_VERSION))) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the CodeQL CLI is older than ${CODEQL_OVERLAY_MINIMUM_VERSION}. ` +
"Falling back to creating a normal full database instead.",
);
return nonOverlayAnalysis;
}
if ((await getGitRoot(sourceRoot)) === undefined) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the source root "${sourceRoot}" is not inside a git repository. ` +
"Falling back to creating a normal full database instead.",
);
return nonOverlayAnalysis;
}
return {
overlayDatabaseMode,
useOverlayDatabaseCaching,
};
}
/**
* Pack names must be in the form of `scope/name`, with only alpha-numeric characters,
* and `-` allowed as long as not the first or last char.
@@ -884,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
@@ -900,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
@@ -1186,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;
}
+4 -4
View File
@@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.22.1",
"cliVersion": "2.22.1",
"priorBundleVersion": "codeql-bundle-v2.22.0",
"priorCliVersion": "2.22.0"
"bundleVersion": "codeql-bundle-v2.22.2",
"cliVersion": "2.22.2",
"priorBundleVersion": "codeql-bundle-v2.22.1",
"priorCliVersion": "2.22.1"
}
+185
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,
);
+12 -35
View File
@@ -1,44 +1,13 @@
import * as fs from "fs";
import * as path from "path";
import * as github from "@actions/github";
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";
export interface PullRequestBranches {
base: string;
head: string;
}
function getPullRequestBranches(): PullRequestBranches | undefined {
const pullRequest = github.context.payload.pull_request;
if (pullRequest) {
return {
base: pullRequest.base.ref,
// We use the head label instead of the head ref here, because the head
// ref lacks owner information and by itself does not uniquely identify
// the head branch (which may be in a forked repository).
head: pullRequest.head.label,
};
}
// PR analysis under Default Setup does not have the pull_request context,
// but it should set CODE_SCANNING_REF and CODE_SCANNING_BASE_BRANCH.
const codeScanningRef = process.env.CODE_SCANNING_REF;
const codeScanningBaseBranch = process.env.CODE_SCANNING_BASE_BRANCH;
if (codeScanningRef && codeScanningBaseBranch) {
return {
base: codeScanningBaseBranch,
// PR analysis under Default Setup analyzes the PR head commit instead of
// the merge commit, so we can use the provided ref directly.
head: codeScanningRef,
};
}
return undefined;
}
import { GitHubVariant, satisfiesGHESVersion } from "./util";
/**
* Check if the action should perform diff-informed analysis.
@@ -70,7 +39,15 @@ export async function getDiffInformedAnalysisBranches(
return undefined;
}
const branches = getPullRequestBranches();
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(
"Not performing diff-informed analysis " +
+25 -7
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),
+150 -18
View File
@@ -7,6 +7,7 @@ import { getApiClient } from "./api-client";
import type { CodeQL } from "./codeql";
import * as defaults from "./defaults.json";
import { Logger } from "./logging";
import { CODEQL_OVERLAY_MINIMUM_VERSION } from "./overlay-database-utils";
import { RepositoryNwo } from "./repository";
import { ToolsFeature } from "./tools-features";
import * as util from "./util";
@@ -46,12 +47,32 @@ export enum Feature {
CppBuildModeNone = "cpp_build_mode_none",
CppDependencyInstallation = "cpp_dependency_installation_enabled",
DiffInformedQueries = "diff_informed_queries",
DisableCombineSarifFiles = "disable_combine_sarif_files",
DisableCsharpBuildless = "disable_csharp_buildless",
DisableJavaBuildlessEnabled = "disable_java_buildless_enabled",
DisableKotlinAnalysisEnabled = "disable_kotlin_analysis_enabled",
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",
ZstdBundleStreamingExtraction = "zstd_bundle_streaming_extraction",
@@ -110,15 +131,10 @@ export const featureConfig: Record<
minimumVersion: "2.15.0",
},
[Feature.DiffInformedQueries]: {
defaultValue: false,
defaultValue: true,
envVar: "CODEQL_ACTION_DIFF_INFORMED_QUERIES",
minimumVersion: "2.21.0",
},
[Feature.DisableCombineSarifFiles]: {
defaultValue: false,
envVar: "CODEQL_ACTION_DISABLE_COMBINE_SARIF_FILES",
minimumVersion: undefined,
},
[Feature.DisableCsharpBuildless]: {
defaultValue: false,
envVar: "CODEQL_ACTION_DISABLE_CSHARP_BUILDLESS",
@@ -147,6 +163,111 @@ export const featureConfig: Record<
envVar: "CODEQL_ACTION_EXTRACT_TOOLCACHE",
minimumVersion: undefined,
},
[Feature.OverlayAnalysis]: {
defaultValue: false,
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",
@@ -489,18 +610,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:",
);
+60 -16
View File
@@ -35,14 +35,17 @@ import { Feature, Features } from "./feature-flags";
import {
checkInstallPython311,
cleanupDatabaseClusterDirectory,
getOverlayDatabaseMode,
initCodeQL,
initConfig,
runInit,
} from "./init";
import { Language } from "./languages";
import { getActionsLogger, Logger } from "./logging";
import { OverlayDatabaseMode } from "./overlay-database-utils";
import {
downloadOverlayBaseDatabaseFromCache,
OverlayBaseDatabaseDownloadStats,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { getRepositoryNwo } from "./repository";
import { ToolsSource } from "./setup-codeql";
import {
@@ -104,6 +107,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;
@@ -131,6 +138,7 @@ async function sendCompletedStatusReport(
toolsFeatureFlagsValid: boolean | undefined,
toolsSource: ToolsSource,
toolsVersion: string,
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
logger: Logger,
error?: Error,
) {
@@ -234,6 +242,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"] ?? [],
),
@@ -296,6 +308,14 @@ async function run() {
const configFile = getOptionalInput("config-file");
// path.resolve() respects the intended semantics of source-root. If
// source-root is relative, it is relative to the GITHUB_WORKSPACE. If
// source-root is absolute, it is used as given.
const sourceRoot = path.resolve(
getRequiredEnvParam("GITHUB_WORKSPACE"),
getOptionalInput("source-root") || "",
);
try {
const statusReportBase = await createStatusReportBase(
ActionName.Init,
@@ -362,6 +382,7 @@ async function run() {
tempDir: getTemporaryDirectory(),
codeql,
workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"),
sourceRoot,
githubVersion: gitHubVersion,
apiDetails,
features,
@@ -388,21 +409,43 @@ async function run() {
return;
}
let overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined;
try {
const sourceRoot = path.resolve(
getRequiredEnvParam("GITHUB_WORKSPACE"),
getOptionalInput("source-root") || "",
);
if (
config.augmentationProperties.overlayDatabaseMode ===
OverlayDatabaseMode.Overlay &&
config.augmentationProperties.useOverlayDatabaseCaching
) {
// OverlayDatabaseMode.Overlay comes in two flavors: with database
// caching, or without. The flavor with database caching is intended to be
// an "automatic control" mode, which is supposed to be fail-safe. If we
// cannot download an overlay-base database, we revert to
// OverlayDatabaseMode.None so that the workflow can continue to run.
//
// The flavor without database caching is intended to be a "manual
// control" mode, where the workflow is supposed to make all the
// 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.
overlayBaseDatabaseStats = await downloadOverlayBaseDatabaseFromCache(
codeql,
config,
logger,
);
if (!overlayBaseDatabaseStats) {
config.augmentationProperties.overlayDatabaseMode =
OverlayDatabaseMode.None;
logger.info(
"No overlay-base database found in cache, " +
`reverting overlay database mode to ${OverlayDatabaseMode.None}.`,
);
}
}
const overlayDatabaseMode = await getOverlayDatabaseMode(
(await codeql.getVersion()).version,
config,
sourceRoot,
logger,
);
logger.info(`Using overlay database mode: ${overlayDatabaseMode}`);
if (overlayDatabaseMode !== OverlayDatabaseMode.Overlay) {
if (
config.augmentationProperties.overlayDatabaseMode !==
OverlayDatabaseMode.Overlay
) {
cleanupDatabaseClusterDirectory(config, logger);
}
@@ -667,7 +710,6 @@ async function run() {
"Runner.Worker.exe",
getOptionalInput("registries"),
apiDetails,
overlayDatabaseMode,
logger,
);
if (tracerConfig !== undefined) {
@@ -693,6 +735,7 @@ async function run() {
toolsFeatureFlagsValid,
toolsSource,
toolsVersion,
overlayBaseDatabaseStats,
logger,
error,
);
@@ -708,6 +751,7 @@ async function run() {
toolsFeatureFlagsValid,
toolsSource,
toolsVersion,
overlayBaseDatabaseStats,
logger,
);
}
-49
View File
@@ -3,20 +3,14 @@ import * as path from "path";
import * as toolrunner from "@actions/exec/lib/toolrunner";
import * as io from "@actions/io";
import * as semver from "semver";
import { getOptionalInput, isSelfHostedRunner } from "./actions-util";
import { GitHubApiCombinedDetails, GitHubApiDetails } from "./api-client";
import { CodeQL, setupCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { CodeQLDefaultVersionInfo, FeatureEnablement } from "./feature-flags";
import { getGitRoot } from "./git-utils";
import { Language } from "./languages";
import { Logger, withGroupAsync } from "./logging";
import {
CODEQL_OVERLAY_MINIMUM_VERSION,
OverlayDatabaseMode,
} from "./overlay-database-utils";
import { ToolsSource } from "./setup-codeql";
import { ZstdAvailability } from "./tar";
import { ToolsDownloadStatusReport } from "./tools-download";
@@ -74,47 +68,6 @@ export async function initConfig(
});
}
export async function getOverlayDatabaseMode(
codeqlVersion: string,
config: configUtils.Config,
sourceRoot: string,
logger: Logger,
): Promise<OverlayDatabaseMode> {
const overlayDatabaseMode = process.env.CODEQL_OVERLAY_DATABASE_MODE;
if (
overlayDatabaseMode === OverlayDatabaseMode.Overlay ||
overlayDatabaseMode === OverlayDatabaseMode.OverlayBase
) {
if (config.buildMode !== util.BuildMode.None) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`build-mode is set to "${config.buildMode}" instead of "none". ` +
"Falling back to creating a normal full database instead.",
);
return OverlayDatabaseMode.None;
}
if (semver.lt(codeqlVersion, CODEQL_OVERLAY_MINIMUM_VERSION)) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the CodeQL CLI is older than ${CODEQL_OVERLAY_MINIMUM_VERSION}. ` +
"Falling back to creating a normal full database instead.",
);
return OverlayDatabaseMode.None;
}
if ((await getGitRoot(sourceRoot)) === undefined) {
logger.warning(
`Cannot build an ${overlayDatabaseMode} database because ` +
`the source root "${sourceRoot}" is not inside a git repository. ` +
"Falling back to creating a normal full database instead.",
);
return OverlayDatabaseMode.None;
}
return overlayDatabaseMode as OverlayDatabaseMode;
}
return OverlayDatabaseMode.None;
}
export async function runInit(
codeql: CodeQL,
config: configUtils.Config,
@@ -122,7 +75,6 @@ export async function runInit(
processName: string | undefined,
registriesInput: string | undefined,
apiDetails: GitHubApiCombinedDetails,
overlayDatabaseMode: OverlayDatabaseMode,
logger: Logger,
): Promise<TracerConfig | undefined> {
fs.mkdirSync(config.dbLocation, { recursive: true });
@@ -146,7 +98,6 @@ export async function runInit(
sourceRoot,
processName,
qlconfigFile,
overlayDatabaseMode,
logger,
),
);
+183 -1
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,
);
+247 -2
View File
@@ -1,10 +1,14 @@
import * as fs from "fs";
import * as path from "path";
import { getTemporaryDirectory } from "./actions-util";
import * as actionsCache from "@actions/cache";
import { getRequiredInput, getTemporaryDirectory } from "./actions-util";
import { type CodeQL } from "./codeql";
import { type Config } from "./config-utils";
import { getFileOidsUnderPath } from "./git-utils";
import { getCommitOid, getFileOidsUnderPath } from "./git-utils";
import { Logger } from "./logging";
import { isInTestMode, tryGetFolderBytes, withTimeout } from "./util";
export enum OverlayDatabaseMode {
Overlay = "overlay",
@@ -122,3 +126,244 @@ function computeChangedFiles(
}
return changes;
}
// Constants for database caching
const CACHE_VERSION = 1;
const CACHE_PREFIX = "codeql-overlay-base-database";
const MAX_CACHE_OPERATION_MS = 120_000; // Two minutes
/**
* Checks that the overlay-base database is valid by checking for the
* existence of the base database OIDs file.
*
* @param config The configuration object
* @param logger The logger instance
* @param warningPrefix Prefix for the check failure warning message
* @returns True if the verification succeeded, false otherwise
*/
export function checkOverlayBaseDatabase(
config: Config,
logger: Logger,
warningPrefix: string,
): boolean {
// An overlay-base database should contain the base database OIDs file.
const baseDatabaseOidsFilePath = getBaseDatabaseOidsFilePath(config);
if (!fs.existsSync(baseDatabaseOidsFilePath)) {
logger.warning(
`${warningPrefix}: ${baseDatabaseOidsFilePath} does not exist`,
);
return false;
}
return true;
}
/**
* Uploads the overlay-base database to the GitHub Actions cache. If conditions
* for uploading are not met, the function does nothing and returns false.
*
* This function uses the `checkout_path` input to determine the repository path
* and works only when called from `analyze` or `upload-sarif`.
*
* @param codeql The CodeQL instance
* @param config The configuration object
* @param logger The logger instance
* @returns A promise that resolves to true if the upload was performed and
* successfully completed, or false otherwise
*/
export async function uploadOverlayBaseDatabaseToCache(
codeql: CodeQL,
config: Config,
logger: Logger,
): Promise<boolean> {
const overlayDatabaseMode = config.augmentationProperties.overlayDatabaseMode;
if (overlayDatabaseMode !== OverlayDatabaseMode.OverlayBase) {
logger.debug(
`Overlay database mode is ${overlayDatabaseMode}. ` +
"Skip uploading overlay-base database to cache.",
);
return false;
}
if (!config.augmentationProperties.useOverlayDatabaseCaching) {
logger.debug(
"Overlay database caching is disabled. " +
"Skip uploading overlay-base database to cache.",
);
return false;
}
if (isInTestMode()) {
logger.debug(
"In test mode. Skip uploading overlay-base database to cache.",
);
return false;
}
const databaseIsValid = checkOverlayBaseDatabase(
config,
logger,
"Abort uploading overlay-base database to cache",
);
if (!databaseIsValid) {
return false;
}
const dbLocation = config.dbLocation;
const codeQlVersion = (await codeql.getVersion()).version;
const checkoutPath = getRequiredInput("checkout_path");
const cacheKey = await generateCacheKey(config, codeQlVersion, checkoutPath);
logger.info(
`Uploading overlay-base database to Actions cache with key ${cacheKey}`,
);
try {
const cacheId = await withTimeout(
MAX_CACHE_OPERATION_MS,
actionsCache.saveCache([dbLocation], cacheKey),
() => {},
);
if (cacheId === undefined) {
logger.warning("Timed out while uploading overlay-base database");
return false;
}
} catch (error) {
logger.warning(
"Failed to upload overlay-base database to cache: " +
`${error instanceof Error ? error.message : String(error)}`,
);
return false;
}
logger.info(`Successfully uploaded overlay-base database from ${dbLocation}`);
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.
*
* @param codeql The CodeQL instance
* @param config The configuration object
* @param logger The logger instance
* @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<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 undefined;
}
if (!config.augmentationProperties.useOverlayDatabaseCaching) {
logger.debug(
"Overlay database caching is disabled. " +
"Skip downloading overlay-base database from cache.",
);
return undefined;
}
if (isInTestMode()) {
logger.debug(
"In test mode. Skip downloading overlay-base database from cache.",
);
return undefined;
}
const dbLocation = config.dbLocation;
const codeQlVersion = (await codeql.getVersion()).version;
const restoreKey = getCacheRestoreKey(config, codeQlVersion);
logger.info(
`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),
() => {
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 undefined;
}
logger.info(
`Downloaded overlay-base database in cache with key ${foundKey}`,
);
} catch (error) {
logger.warning(
"Failed to download overlay-base database from cache: " +
`${error instanceof Error ? error.message : String(error)}`,
);
return undefined;
}
const databaseIsValid = checkOverlayBaseDatabase(
config,
logger,
"Downloaded overlay-base database is invalid",
);
if (!databaseIsValid) {
logger.warning("Downloaded overlay-base database failed validation");
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 {
databaseSizeBytes: Math.round(databaseSizeBytes),
databaseDownloadDurationMs,
};
}
async function generateCacheKey(
config: Config,
codeQlVersion: string,
checkoutPath: string,
): Promise<string> {
const sha = await getCommitOid(checkoutPath);
return `${getCacheRestoreKey(config, codeQlVersion)}${sha}`;
}
function getCacheRestoreKey(config: Config, codeQlVersion: string): string {
// The restore key (prefix) specifies which cached overlay-base databases are
// compatible with the current analysis: the cached database must have the
// same cache version and the same CodeQL bundle version.
//
// Actions cache supports using multiple restore keys to indicate preference.
// Technically we prefer a cached overlay-base database with the same SHA as
// we are analyzing. However, since overlay-base databases are built from the
// default branch and used in PR analysis, it is exceedingly unlikely that
// the commit SHA will ever be the same, so we can just leave it out.
const languages = [...config.languages].sort().join("_");
return `${CACHE_PREFIX}-${CACHE_VERSION}-${languages}-${codeQlVersion}-`;
}
+1 -1
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,
+41 -10
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,
+2 -2
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";
+78 -22
View File
@@ -3,9 +3,8 @@ import * as path from "path";
import test from "ava";
import { Feature } from "./feature-flags";
import { getRunnerLogger, Logger } from "./logging";
import { createFeatures, setupTests } from "./testing-utils";
import { setupTests } from "./testing-utils";
import * as uploadLib from "./upload-lib";
import { GitHubVariant, initializeEnvironment, withTmpDir } from "./util";
@@ -399,6 +398,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(
@@ -445,27 +456,18 @@ test("shouldShowCombineSarifFilesDeprecationWarning when environment variable is
);
});
test("throwIfCombineSarifFilesDisabled when on dotcom with feature flag", async (t) => {
test("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => {
await t.throwsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.DOTCOM,
},
),
);
});
test("throwIfCombineSarifFilesDisabled when on dotcom without feature flag", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([]),
{
type: GitHubVariant.DOTCOM,
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
@@ -473,7 +475,6 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.13", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.GHES,
version: "3.13.2",
@@ -486,7 +487,6 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.GHES,
version: "3.14.0",
@@ -495,16 +495,75 @@ test("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => {
);
});
test("throwIfCombineSarifFilesDisabled when on GHES 3.17", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
{
type: GitHubVariant.GHES,
version: "3.17.0",
},
),
);
});
test("throwIfCombineSarifFilesDisabled when on GHES 3.18 pre", async (t) => {
await t.throwsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
{
type: GitHubVariant.GHES,
version: "3.18.0.pre1",
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
test("throwIfCombineSarifFilesDisabled when on GHES 3.18 alpha", async (t) => {
await t.throwsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
{
type: GitHubVariant.GHES,
version: "3.18.0-alpha.1",
},
),
{
message:
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
},
);
});
test("throwIfCombineSarifFilesDisabled when on GHES 3.18", async (t) => {
await t.throwsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.GHES,
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")],
{
type: GitHubVariant.GHES,
version: "foobar",
},
),
);
});
@@ -512,7 +571,6 @@ test("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.DOTCOM,
},
@@ -524,7 +582,6 @@ test("throwIfCombineSarifFilesDisabled with distinct categories", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "def"), createMockSarif("def", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.DOTCOM,
},
@@ -536,7 +593,6 @@ test("throwIfCombineSarifFilesDisabled with distinct tools", async (t) => {
await t.notThrowsAsync(
uploadLib.throwIfCombineSarifFilesDisabled(
[createMockSarif("abc", "abc"), createMockSarif("abc", "def")],
createFeatures([Feature.DisableCombineSarifFiles]),
{
type: GitHubVariant.DOTCOM,
},
+23 -37
View File
@@ -6,17 +6,15 @@ 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";
import { getConfig } from "./config-utils";
import { readDiffRangesJsonFile } from "./diff-informed-analysis-utils";
import { EnvVar } from "./environment";
import { Feature, FeatureEnablement } from "./feature-flags";
import { FeatureEnablement } from "./feature-flags";
import * as fingerprints from "./fingerprints";
import * as gitUtils from "./git-utils";
import { initCodeQL } from "./init";
@@ -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;
}
@@ -147,22 +146,14 @@ export async function shouldShowCombineSarifFilesDeprecationWarning(
export async function throwIfCombineSarifFilesDisabled(
sarifObjects: util.SarifFile[],
features: FeatureEnablement,
githubVersion: GitHubVersion,
) {
if (
!(await shouldDisableCombineSarifFiles(
sarifObjects,
features,
githubVersion,
))
) {
if (!(await shouldDisableCombineSarifFiles(sarifObjects, githubVersion))) {
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}`,
@@ -172,15 +163,13 @@ export async function throwIfCombineSarifFilesDisabled(
// Checks whether combining SARIF files should be disabled.
async function shouldDisableCombineSarifFiles(
sarifObjects: util.SarifFile[],
features: FeatureEnablement,
githubVersion: GitHubVersion,
) {
// Never block on GHES versions before 3.18.0
if (
githubVersion.type === GitHubVariant.GHES &&
semver.lt(githubVersion.version, "3.18.0")
) {
return false;
if (githubVersion.type === GitHubVariant.GHES) {
// Never block on GHES versions before 3.18.
if (satisfiesGHESVersion(githubVersion.version, "<3.18", true)) {
return false;
}
}
if (areAllRunsUnique(sarifObjects)) {
@@ -188,7 +177,9 @@ async function shouldDisableCombineSarifFiles(
return false;
}
return features.getValue(Feature.DisableCombineSarifFiles);
// Combining SARIF files is not supported and Code Scanning will return an
// error if multiple runs with the same category are uploaded.
return true;
}
// Takes a list of paths to sarif files and combines them together using the
@@ -202,9 +193,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;
@@ -218,11 +206,7 @@ async function combineSarifFilesUsingCLI(
"For more information, see https://github.blog/changelog/2024-05-06-code-scanning-will-stop-combining-runs-from-a-single-upload";
if (!areAllRunsProducedByCodeQL(sarifObjects)) {
await throwIfCombineSarifFilesDisabled(
sarifObjects,
features,
gitHubVersion,
);
await throwIfCombineSarifFilesDisabled(sarifObjects, gitHubVersion);
logger.debug(
"Not all SARIF files were produced by CodeQL. Merging files in the action.",
@@ -259,8 +243,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"),
};
@@ -287,11 +273,7 @@ async function combineSarifFilesUsingCLI(
ToolsFeature.SarifMergeRunsFromEqualCategory,
))
) {
await throwIfCombineSarifFilesDisabled(
sarifObjects,
features,
gitHubVersion,
);
await throwIfCombineSarifFilesDisabled(sarifObjects, gitHubVersion);
logger.warning(
"The CodeQL CLI does not support merging SARIF files. Merging files in the action.",
@@ -722,6 +704,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], gitHubVersion);
}
sarif = filterAlertsByDiffRange(logger, sarif);
@@ -890,6 +875,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 (
+2 -1
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;
+25
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.
*