mirror of
https://github.com/github/codeql-action.git
synced 2026-04-25 16:28:48 +00:00
Merge branch 'main' into redsun82/rust
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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 " +
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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}-`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user