mirror of
https://github.com/github/codeql-action.git
synced 2026-04-26 00:38:48 +00:00
Merge branch 'main' into henrymercer/language-extensibility
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");
|
||||
|
||||
+28
-1
@@ -27,6 +27,10 @@ import { EnvVar } from "./environment";
|
||||
import { Features } from "./feature-flags";
|
||||
import { KnownLanguage } 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 {
|
||||
@@ -297,8 +301,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(
|
||||
@@ -333,8 +344,21 @@ async function run() {
|
||||
actionsUtil.getOptionalInput("category"),
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeScanningTarget,
|
||||
);
|
||||
core.setOutput("sarif-id", uploadResult.sarifID);
|
||||
|
||||
if (config.augmentationProperties.qualityQueriesInput !== undefined) {
|
||||
const qualityUploadResult = await uploadLib.uploadFiles(
|
||||
outputDir,
|
||||
actionsUtil.getRequiredInput("checkout_path"),
|
||||
actionsUtil.getOptionalInput("category"),
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeQualityTarget,
|
||||
);
|
||||
core.setOutput("quality-sarif-id", qualityUploadResult.sarifID);
|
||||
}
|
||||
} else {
|
||||
logger.info("Not uploading results");
|
||||
}
|
||||
@@ -342,6 +366,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);
|
||||
|
||||
+30
-1
@@ -5,7 +5,12 @@ import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { exportedForTesting, runQueries } from "./analyze";
|
||||
import {
|
||||
exportedForTesting,
|
||||
runQueries,
|
||||
defaultSuites,
|
||||
resolveQuerySuiteAlias,
|
||||
} from "./analyze";
|
||||
import { setCodeQL } from "./codeql";
|
||||
import { Feature } from "./feature-flags";
|
||||
import { KnownLanguage } from "./languages";
|
||||
@@ -109,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`,
|
||||
@@ -320,3 +327,25 @@ test("getDiffRanges: malformed thunk header", async (t) => {
|
||||
const diffRanges = runGetDiffRanges(2, ["@@ 30 +50,2 @@", "+1", "+2"]);
|
||||
t.deepEqual(diffRanges, undefined);
|
||||
});
|
||||
|
||||
test("resolveQuerySuiteAlias", (t) => {
|
||||
// default query suite names should resolve to something language-specific ending in `.qls`.
|
||||
for (const suite of defaultSuites) {
|
||||
const resolved = resolveQuerySuiteAlias(KnownLanguage.go, suite);
|
||||
t.assert(
|
||||
resolved.endsWith(".qls"),
|
||||
"Resolved default suite doesn't end in .qls",
|
||||
);
|
||||
t.assert(
|
||||
resolved.indexOf(KnownLanguage.go) >= 0,
|
||||
"Resolved default suite doesn't contain language name",
|
||||
);
|
||||
}
|
||||
|
||||
// other inputs should be returned unchanged
|
||||
const names = ["foo", "bar", "codeql/go-queries@1.0"];
|
||||
|
||||
for (const name of names) {
|
||||
t.deepEqual(resolveQuerySuiteAlias(KnownLanguage.go, name), name);
|
||||
}
|
||||
});
|
||||
|
||||
+103
-11
@@ -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 { KnownLanguage, 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. */
|
||||
@@ -396,7 +412,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) {
|
||||
@@ -502,10 +518,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,
|
||||
@@ -565,6 +578,34 @@ extensions:
|
||||
return diffRangeDir;
|
||||
}
|
||||
|
||||
// A set of default query suite names that are understood by the CLI.
|
||||
export const defaultSuites: Set<string> = new Set([
|
||||
"security-experimental",
|
||||
"security-extended",
|
||||
"security-and-quality",
|
||||
"code-quality",
|
||||
"code-scanning",
|
||||
]);
|
||||
|
||||
/**
|
||||
* If `maybeSuite` is the name of a default query suite, it is resolved into the corresponding
|
||||
* query suite name for the given `language`. Otherwise, `maybeSuite` is returned as is.
|
||||
*
|
||||
* @param language The language for which to resolve the default query suite name.
|
||||
* @param maybeSuite The string that potentially contains the name of a default query suite.
|
||||
* @returns Returns the resolved query suite name, or the unmodified input.
|
||||
*/
|
||||
export function resolveQuerySuiteAlias(
|
||||
language: Language,
|
||||
maybeSuite: string,
|
||||
): string {
|
||||
if (defaultSuites.has(maybeSuite)) {
|
||||
return `${language}-${maybeSuite}.qls`;
|
||||
}
|
||||
|
||||
return maybeSuite;
|
||||
}
|
||||
|
||||
// Runs queries and creates sarif files in the given folder
|
||||
export async function runQueries(
|
||||
sarifFolder: string,
|
||||
@@ -580,6 +621,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");
|
||||
@@ -589,10 +631,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);
|
||||
|
||||
@@ -600,6 +658,22 @@ export async function runQueries(
|
||||
try {
|
||||
const sarifFile = path.join(sarifFolder, `${language}.sarif`);
|
||||
|
||||
const queries: string[] = [];
|
||||
if (config.augmentationProperties.qualityQueriesInput !== undefined) {
|
||||
queries.push(
|
||||
path.join(
|
||||
util.getCodeQLDatabasePath(config, language),
|
||||
"temp",
|
||||
"config-queries.qls",
|
||||
),
|
||||
);
|
||||
|
||||
for (const qualityQuery of config.augmentationProperties
|
||||
.qualityQueriesInput) {
|
||||
queries.push(resolveQuerySuiteAlias(language, qualityQuery.uses));
|
||||
}
|
||||
}
|
||||
|
||||
// The work needed to generate the query suites
|
||||
// is done in the CLI. We just need to make a single
|
||||
// call to run all the queries for each language and
|
||||
@@ -607,7 +681,7 @@ export async function runQueries(
|
||||
logger.startGroup(`Running queries for ${language}`);
|
||||
const startTimeRunQueries = new Date().getTime();
|
||||
const databasePath = util.getCodeQLDatabasePath(config, language);
|
||||
await codeql.databaseRunQueries(databasePath, queryFlags);
|
||||
await codeql.databaseRunQueries(databasePath, queryFlags, queries);
|
||||
logger.debug(`Finished running queries for ${language}.`);
|
||||
// TODO should not be using `builtin` here. We should be using `all` instead.
|
||||
// The status report does not support `all` yet.
|
||||
@@ -622,6 +696,24 @@ export async function runQueries(
|
||||
sarifFile,
|
||||
config.debugMode,
|
||||
);
|
||||
if (config.augmentationProperties.qualityQueriesInput !== undefined) {
|
||||
logger.info(`Interpreting quality results for ${language}`);
|
||||
const qualitySarifFile = path.join(
|
||||
sarifFolder,
|
||||
`${language}.quality.sarif`,
|
||||
);
|
||||
const qualityAnalysisSummary = await runInterpretResults(
|
||||
language,
|
||||
config.augmentationProperties.qualityQueriesInput.map((i) =>
|
||||
resolveQuerySuiteAlias(language, i.uses),
|
||||
),
|
||||
qualitySarifFile,
|
||||
config.debugMode,
|
||||
);
|
||||
|
||||
// TODO: move
|
||||
logger.info(qualityAnalysisSummary);
|
||||
}
|
||||
const endTimeInterpretResults = new Date();
|
||||
statusReport[`interpret_results_${language}_duration_ms`] =
|
||||
endTimeInterpretResults.getTime() - startTimeInterpretResults.getTime();
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"maximumVersion": "3.18", "minimumVersion": "3.13"}
|
||||
{"maximumVersion": "3.18", "minimumVersion": "3.14"}
|
||||
|
||||
+14
-18
@@ -14,13 +14,16 @@ import * as actionsUtil from "./actions-util";
|
||||
import { GitHubApiDetails } from "./api-client";
|
||||
import { CliError } from "./cli-errors";
|
||||
import * as codeql from "./codeql";
|
||||
import { AugmentationProperties, Config } from "./config-utils";
|
||||
import {
|
||||
AugmentationProperties,
|
||||
Config,
|
||||
defaultAugmentationProperties,
|
||||
} from "./config-utils";
|
||||
import * as defaults from "./defaults.json";
|
||||
import { DocUrl } from "./doc-url";
|
||||
import { FeatureEnablement } from "./feature-flags";
|
||||
import { KnownLanguage } from "./languages";
|
||||
import { getRunnerLogger } from "./logging";
|
||||
import { OverlayDatabaseMode } from "./overlay-database-utils";
|
||||
import { ToolsSource } from "./setup-codeql";
|
||||
import {
|
||||
setupTests,
|
||||
@@ -520,7 +523,6 @@ const injectedConfigMacro = test.macro({
|
||||
"",
|
||||
undefined,
|
||||
undefined,
|
||||
OverlayDatabaseMode.None,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -546,8 +548,7 @@ test(
|
||||
"basic",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
packsInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
},
|
||||
{},
|
||||
{},
|
||||
@@ -557,8 +558,7 @@ test(
|
||||
"injected packs from input",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
packsInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
packsInput: ["xxx", "yyy"],
|
||||
},
|
||||
{},
|
||||
@@ -571,7 +571,7 @@ test(
|
||||
"injected packs from input with existing packs combines",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
packsInputCombines: true,
|
||||
packsInput: ["xxx", "yyy"],
|
||||
},
|
||||
@@ -593,8 +593,7 @@ test(
|
||||
"injected packs from input with existing packs overrides",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
packsInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
packsInput: ["xxx", "yyy"],
|
||||
},
|
||||
{
|
||||
@@ -614,8 +613,7 @@ test(
|
||||
"injected queries from input",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
packsInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
|
||||
},
|
||||
{},
|
||||
@@ -635,8 +633,7 @@ test(
|
||||
"injected queries from input overrides",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
packsInputCombines: false,
|
||||
...defaultAugmentationProperties,
|
||||
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
|
||||
},
|
||||
{
|
||||
@@ -660,8 +657,8 @@ test(
|
||||
"injected queries from input combines",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
...defaultAugmentationProperties,
|
||||
queriesInputCombines: true,
|
||||
packsInputCombines: false,
|
||||
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
|
||||
},
|
||||
{
|
||||
@@ -688,6 +685,7 @@ test(
|
||||
"injected queries from input combines 2",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
...defaultAugmentationProperties,
|
||||
queriesInputCombines: true,
|
||||
packsInputCombines: true,
|
||||
queriesInput: [{ uses: "xxx" }, { uses: "yyy" }],
|
||||
@@ -709,6 +707,7 @@ test(
|
||||
"injected queries and packs, but empty",
|
||||
injectedConfigMacro,
|
||||
{
|
||||
...defaultAugmentationProperties,
|
||||
queriesInputCombines: true,
|
||||
packsInputCombines: true,
|
||||
queriesInput: [],
|
||||
@@ -732,7 +731,6 @@ test("passes a code scanning config AND qlconfig to the CLI", async (t: Executio
|
||||
"",
|
||||
undefined,
|
||||
"/path/to/qlconfig.yml",
|
||||
OverlayDatabaseMode.None,
|
||||
getRunnerLogger(true),
|
||||
);
|
||||
|
||||
@@ -761,7 +759,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),
|
||||
);
|
||||
|
||||
@@ -1011,7 +1008,6 @@ test("Avoids duplicating --overwrite flag if specified in CODEQL_ACTION_EXTRA_OP
|
||||
"sourceRoot",
|
||||
undefined,
|
||||
undefined,
|
||||
OverlayDatabaseMode.None,
|
||||
getRunnerLogger(false),
|
||||
);
|
||||
|
||||
|
||||
+23
-80
@@ -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>;
|
||||
|
||||
@@ -95,7 +95,6 @@ export interface CodeQL {
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
overlayDatabaseMode: OverlayDatabaseMode,
|
||||
logger: Logger,
|
||||
): Promise<void>;
|
||||
/**
|
||||
@@ -165,9 +164,15 @@ export interface CodeQL {
|
||||
dbName: string,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Run 'codeql database run-queries'.
|
||||
* Run 'codeql database run-queries'. If no `queries` are specified, then the CLI
|
||||
* will automatically use the `config-queries.qls` (if it exists) or default queries
|
||||
* for the language.
|
||||
*/
|
||||
databaseRunQueries(databasePath: string, flags: string[]): Promise<void>;
|
||||
databaseRunQueries(
|
||||
databasePath: string,
|
||||
flags: string[],
|
||||
queries?: string[],
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Run 'codeql database interpret-results'.
|
||||
*/
|
||||
@@ -293,17 +298,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++";
|
||||
@@ -580,7 +585,6 @@ export async function getCodeQLForCmd(
|
||||
sourceRoot: string,
|
||||
processName: string | undefined,
|
||||
qlconfigFile: string | undefined,
|
||||
overlayDatabaseMode: OverlayDatabaseMode,
|
||||
logger: Logger,
|
||||
) {
|
||||
const extraArgs = config.languages.map(
|
||||
@@ -592,7 +596,7 @@ export async function getCodeQLForCmd(
|
||||
extraArgs.push(`--trace-process-name=${processName}`);
|
||||
}
|
||||
|
||||
const codeScanningConfigFile = await generateCodeScanningConfig(
|
||||
const codeScanningConfigFile = await writeCodeScanningConfigFile(
|
||||
config,
|
||||
logger,
|
||||
);
|
||||
@@ -618,6 +622,8 @@ export async function getCodeQLForCmd(
|
||||
? "--force-overwrite"
|
||||
: "--overwrite";
|
||||
|
||||
const overlayDatabaseMode =
|
||||
config.augmentationProperties.overlayDatabaseMode;
|
||||
if (overlayDatabaseMode === OverlayDatabaseMode.Overlay) {
|
||||
const overlayChangesFile = await writeOverlayChangesFile(
|
||||
config,
|
||||
@@ -828,6 +834,7 @@ export async function getCodeQLForCmd(
|
||||
async databaseRunQueries(
|
||||
databasePath: string,
|
||||
flags: string[],
|
||||
queries: string[] = [],
|
||||
): Promise<void> {
|
||||
const codeqlArgs = [
|
||||
"database",
|
||||
@@ -837,6 +844,7 @@ export async function getCodeQLForCmd(
|
||||
"--intra-layer-parallelism",
|
||||
"--min-disk-free=1024", // Try to leave at least 1GB free
|
||||
"-v",
|
||||
...queries,
|
||||
...getExtraOptionsFromEnv(["database", "run-queries"], {
|
||||
ignoringOptions: ["--expect-discarded-cache"],
|
||||
}),
|
||||
@@ -1231,62 +1239,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"] = [
|
||||
...(config.augmentationProperties.defaultQueryFilters || []),
|
||||
...(augmentedConfig["query-filters"] || []),
|
||||
];
|
||||
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}`,
|
||||
@@ -1370,21 +1331,3 @@ async function getJobRunUuidSarifOptions(codeql: CodeQL) {
|
||||
? [`--sarif-run-property=jobRunUuid=${jobRunUuid}`]
|
||||
: [];
|
||||
}
|
||||
|
||||
export async function getSupportedLanguageMap(
|
||||
codeql: CodeQL,
|
||||
): Promise<Record<string, string>> {
|
||||
const resolveResult = await codeql.betterResolveLanguages();
|
||||
const supportedLanguages: Record<string, string> = {};
|
||||
// Populate canonical language names
|
||||
for (const extractor of Object.keys(resolveResult.extractors)) {
|
||||
supportedLanguages[extractor] = extractor;
|
||||
}
|
||||
// Populate language aliases
|
||||
if (resolveResult.aliases) {
|
||||
for (const [alias, extractor] of Object.entries(resolveResult.aliases)) {
|
||||
supportedLanguages[alias] = extractor;
|
||||
}
|
||||
}
|
||||
return supportedLanguages;
|
||||
}
|
||||
|
||||
+706
-30
@@ -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 { KnownLanguage, 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,
|
||||
@@ -47,6 +51,7 @@ function createTestInitConfigInputs(
|
||||
{
|
||||
languagesInput: undefined,
|
||||
queriesInput: undefined,
|
||||
qualityQueriesInput: undefined,
|
||||
packsInput: undefined,
|
||||
configFile: undefined,
|
||||
dbLocation: undefined,
|
||||
@@ -61,6 +66,7 @@ function createTestInitConfigInputs(
|
||||
tempDir: "",
|
||||
codeql: {} as CodeQL,
|
||||
workspacePath: "",
|
||||
sourceRoot: "",
|
||||
githubVersion,
|
||||
apiDetails: {
|
||||
auth: "token",
|
||||
@@ -844,17 +850,16 @@ const calculateAugmentationMacro = test.macro({
|
||||
_title: string,
|
||||
rawPacksInput: string | undefined,
|
||||
rawQueriesInput: string | undefined,
|
||||
rawQualityQueriesInput: string | undefined,
|
||||
languages: Language[],
|
||||
expectedAugmentationProperties: configUtils.AugmentationProperties,
|
||||
) => {
|
||||
const actualAugmentationProperties =
|
||||
await configUtils.calculateAugmentation(
|
||||
getCachedCodeQL(),
|
||||
createFeatures([]),
|
||||
rawPacksInput,
|
||||
rawQueriesInput,
|
||||
rawQualityQueriesInput,
|
||||
languages,
|
||||
mockLogger,
|
||||
);
|
||||
t.deepEqual(actualAugmentationProperties, expectedAugmentationProperties);
|
||||
},
|
||||
@@ -866,14 +871,11 @@ test(
|
||||
"All empty",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
queriesInput: undefined,
|
||||
packsInputCombines: false,
|
||||
packsInput: undefined,
|
||||
defaultQueryFilters: [],
|
||||
} as configUtils.AugmentationProperties,
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
@@ -881,14 +883,12 @@ test(
|
||||
"With queries",
|
||||
undefined,
|
||||
" a, b , c, d",
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
|
||||
packsInputCombines: false,
|
||||
packsInput: undefined,
|
||||
defaultQueryFilters: [],
|
||||
} as configUtils.AugmentationProperties,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
@@ -896,14 +896,50 @@ test(
|
||||
"With queries combining",
|
||||
undefined,
|
||||
" + a, b , c, d ",
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
queriesInputCombines: true,
|
||||
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
|
||||
packsInputCombines: false,
|
||||
packsInput: undefined,
|
||||
defaultQueryFilters: [],
|
||||
} as configUtils.AugmentationProperties,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
calculateAugmentationMacro,
|
||||
"With quality queries",
|
||||
undefined,
|
||||
undefined,
|
||||
" a, b , c, d",
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
qualityQueriesInput: [
|
||||
{ uses: "a" },
|
||||
{ uses: "b" },
|
||||
{ uses: "c" },
|
||||
{ uses: "d" },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
calculateAugmentationMacro,
|
||||
"With security and quality queries",
|
||||
undefined,
|
||||
" a, b , c, d",
|
||||
"e, f , g,h",
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
queriesInput: [{ uses: "a" }, { uses: "b" }, { uses: "c" }, { uses: "d" }],
|
||||
qualityQueriesInput: [
|
||||
{ uses: "e" },
|
||||
{ uses: "f" },
|
||||
{ uses: "g" },
|
||||
{ uses: "h" },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
@@ -911,14 +947,12 @@ test(
|
||||
"With packs",
|
||||
" codeql/a , codeql/b , codeql/c , codeql/d ",
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
queriesInput: undefined,
|
||||
packsInputCombines: false,
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
|
||||
defaultQueryFilters: [],
|
||||
} as configUtils.AugmentationProperties,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
@@ -926,14 +960,13 @@ test(
|
||||
"With packs combining",
|
||||
" + codeql/a, codeql/b, codeql/c, codeql/d",
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
{
|
||||
queriesInputCombines: false,
|
||||
queriesInput: undefined,
|
||||
...configUtils.defaultAugmentationProperties,
|
||||
packsInputCombines: true,
|
||||
packsInput: ["codeql/a", "codeql/b", "codeql/c", "codeql/d"],
|
||||
defaultQueryFilters: [],
|
||||
} as configUtils.AugmentationProperties,
|
||||
},
|
||||
);
|
||||
|
||||
const calculateAugmentationErrorMacro = test.macro({
|
||||
@@ -942,18 +975,17 @@ const calculateAugmentationErrorMacro = test.macro({
|
||||
_title: string,
|
||||
rawPacksInput: string | undefined,
|
||||
rawQueriesInput: string | undefined,
|
||||
rawQualityQueriesInput: string | undefined,
|
||||
languages: Language[],
|
||||
expectedError: RegExp | string,
|
||||
) => {
|
||||
await t.throwsAsync(
|
||||
() =>
|
||||
configUtils.calculateAugmentation(
|
||||
getCachedCodeQL(),
|
||||
createFeatures([]),
|
||||
rawPacksInput,
|
||||
rawQueriesInput,
|
||||
rawQualityQueriesInput,
|
||||
languages,
|
||||
mockLogger,
|
||||
),
|
||||
{ message: expectedError },
|
||||
);
|
||||
@@ -966,6 +998,7 @@ test(
|
||||
"Plus (+) with nothing else (queries)",
|
||||
undefined,
|
||||
" + ",
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
/The workflow property "queries" is invalid/,
|
||||
);
|
||||
@@ -975,6 +1008,7 @@ test(
|
||||
"Plus (+) with nothing else (packs)",
|
||||
" + ",
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
/The workflow property "packs" is invalid/,
|
||||
);
|
||||
@@ -984,6 +1018,7 @@ test(
|
||||
"Packs input with multiple languages",
|
||||
" + a/b, c/d ",
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript, KnownLanguage.java],
|
||||
/Cannot specify a 'packs' input in a multi-language analysis/,
|
||||
);
|
||||
@@ -993,6 +1028,7 @@ test(
|
||||
"Packs input with no languages",
|
||||
" + a/b, c/d ",
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
/No languages specified/,
|
||||
);
|
||||
@@ -1002,6 +1038,7 @@ test(
|
||||
"Invalid packs",
|
||||
" a-pack-without-a-scope ",
|
||||
undefined,
|
||||
undefined,
|
||||
[KnownLanguage.javascript],
|
||||
/"a-pack-without-a-scope" is not a valid pack/,
|
||||
);
|
||||
@@ -1196,3 +1233,642 @@ 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: [KnownLanguage.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 traced languages
|
||||
sinon
|
||||
.stub(codeql, "isTracedLanguage")
|
||||
.callsFake(async (lang: Language) => {
|
||||
return [KnownLanguage.java].includes(lang as KnownLanguage);
|
||||
});
|
||||
|
||||
// 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: [KnownLanguage.javascript],
|
||||
features: [Feature.OverlayAnalysis, Feature.OverlayAnalysisJavascript],
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
getOverlayDatabaseModeMacro,
|
||||
"Overlay-base database on default branch when feature enabled",
|
||||
{
|
||||
languages: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.javascript],
|
||||
features: [Feature.OverlayAnalysis],
|
||||
isDefaultBranch: true,
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
getOverlayDatabaseModeMacro,
|
||||
"Overlay analysis on PR when feature enabled",
|
||||
{
|
||||
languages: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.javascript],
|
||||
features: [Feature.OverlayAnalysisCodeScanningJavascript],
|
||||
isPullRequest: true,
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
getOverlayDatabaseModeMacro,
|
||||
"No overlay analysis on PR when language-specific feature disabled",
|
||||
{
|
||||
languages: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.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: [KnownLanguage.java],
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
getOverlayDatabaseModeMacro,
|
||||
"Fallback due to no build mode with traced language",
|
||||
{
|
||||
overlayDatabaseEnvVar: "overlay",
|
||||
buildMode: undefined,
|
||||
languages: [KnownLanguage.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 KnownLanguage) {
|
||||
test(
|
||||
getOverlayDatabaseModeMacro,
|
||||
`Check default overlay analysis feature for ${language}`,
|
||||
{
|
||||
languages: [language],
|
||||
features: [Feature.OverlayAnalysis],
|
||||
isPullRequest: true,
|
||||
},
|
||||
{
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+381
-110
@@ -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, getSupportedLanguageMap } from "./codeql";
|
||||
import { type CodeQL } from "./codeql";
|
||||
import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils";
|
||||
import { Feature, FeatureEnablement } from "./feature-flags";
|
||||
import { getGitRoot, isAnalyzingDefaultBranch } from "./git-utils";
|
||||
import { KnownLanguage, Language } 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.
|
||||
@@ -170,6 +178,11 @@ export interface AugmentationProperties {
|
||||
*/
|
||||
queriesInput?: Array<{ uses: string }>;
|
||||
|
||||
/**
|
||||
* The quality queries input from the `with` block of the action declaration.
|
||||
*/
|
||||
qualityQueriesInput?: Array<{ uses: string }>;
|
||||
|
||||
/**
|
||||
* Whether or not the packs input combines with the packs in the config.
|
||||
*/
|
||||
@@ -181,9 +194,26 @@ export interface AugmentationProperties {
|
||||
packsInput?: string[];
|
||||
|
||||
/**
|
||||
* Default query filters to apply to the queries in the config.
|
||||
* Extra query exclusions to append to the config.
|
||||
*/
|
||||
defaultQueryFilters?: QueryFilter[];
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +225,10 @@ export const defaultAugmentationProperties: AugmentationProperties = {
|
||||
packsInputCombines: false,
|
||||
packsInput: undefined,
|
||||
queriesInput: undefined,
|
||||
defaultQueryFilters: [],
|
||||
qualityQueriesInput: undefined,
|
||||
extraQueryExclusions: [],
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
};
|
||||
export type Packs = Partial<Record<Language, string[]>>;
|
||||
|
||||
@@ -270,6 +303,24 @@ export function getUnknownLanguagesError(languages: string[]): string {
|
||||
return `Did not recognize the following languages: ${languages.join(", ")}`;
|
||||
}
|
||||
|
||||
export async function getSupportedLanguageMap(
|
||||
codeql: CodeQL,
|
||||
): Promise<Record<string, string>> {
|
||||
const resolveResult = await codeql.betterResolveLanguages();
|
||||
const supportedLanguages: Record<string, string> = {};
|
||||
// Populate canonical language names
|
||||
for (const extractor of Object.keys(resolveResult.extractors)) {
|
||||
supportedLanguages[extractor] = extractor;
|
||||
}
|
||||
// Populate language aliases
|
||||
if (resolveResult.aliases) {
|
||||
for (const [alias, extractor] of Object.entries(resolveResult.aliases)) {
|
||||
supportedLanguages[alias] = extractor;
|
||||
}
|
||||
}
|
||||
return supportedLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the set of languages in the current repository.
|
||||
*/
|
||||
@@ -385,6 +436,7 @@ export async function getRawLanguages(
|
||||
export interface InitConfigInputs {
|
||||
languagesInput: string | undefined;
|
||||
queriesInput: string | undefined;
|
||||
qualityQueriesInput: string | undefined;
|
||||
packsInput: string | undefined;
|
||||
configFile: string | undefined;
|
||||
dbLocation: string | undefined;
|
||||
@@ -399,27 +451,20 @@ 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,
|
||||
queriesInput,
|
||||
qualityQueriesInput,
|
||||
packsInput,
|
||||
buildModeInput,
|
||||
dbLocation,
|
||||
@@ -434,7 +479,7 @@ export async function getDefaultConfig({
|
||||
githubVersion,
|
||||
features,
|
||||
logger,
|
||||
}: GetDefaultConfigInputs): Promise<Config> {
|
||||
}: InitConfigInputs): Promise<Config> {
|
||||
const languages = await getLanguages(
|
||||
codeql,
|
||||
languagesInput,
|
||||
@@ -450,12 +495,10 @@ export async function getDefaultConfig({
|
||||
);
|
||||
|
||||
const augmentationProperties = await calculateAugmentation(
|
||||
codeql,
|
||||
features,
|
||||
packsInput,
|
||||
queriesInput,
|
||||
qualityQueriesInput,
|
||||
languages,
|
||||
logger,
|
||||
);
|
||||
|
||||
const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime(
|
||||
@@ -502,32 +545,12 @@ async function downloadCacheWithTime(
|
||||
return { trapCaches, trapCacheDownloadTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the config from the given file.
|
||||
*/
|
||||
async function loadConfig({
|
||||
languagesInput,
|
||||
queriesInput,
|
||||
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.
|
||||
@@ -539,57 +562,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,
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -599,14 +575,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.
|
||||
*
|
||||
@@ -615,12 +588,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(
|
||||
@@ -634,17 +605,20 @@ export async function calculateAugmentation(
|
||||
queriesInputCombines,
|
||||
);
|
||||
|
||||
const defaultQueryFilters: QueryFilter[] = [];
|
||||
if (await shouldPerformDiffInformedAnalysis(codeql, features, logger)) {
|
||||
defaultQueryFilters.push({ exclude: { tags: "exclude-from-incremental" } });
|
||||
}
|
||||
const qualityQueriesInput = parseQueriesFromInput(
|
||||
rawQualityQueriesInput,
|
||||
false,
|
||||
);
|
||||
|
||||
return {
|
||||
packsInputCombines,
|
||||
packsInput: packsInput?.[languages[0]],
|
||||
queriesInput,
|
||||
queriesInputCombines,
|
||||
defaultQueryFilters,
|
||||
qualityQueriesInput,
|
||||
extraQueryExclusions: [],
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -671,6 +645,201 @@ 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 &&
|
||||
(
|
||||
await Promise.all(
|
||||
languages.map(async (l) => await codeql.isTracedLanguage(l)),
|
||||
)
|
||||
).some(Boolean)
|
||||
) {
|
||||
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.
|
||||
@@ -844,8 +1013,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
|
||||
@@ -860,13 +1027,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
|
||||
@@ -1146,3 +1356,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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ test("sanitizeArtifactName", (t) => {
|
||||
debugArtifacts.sanitizeArtifactName("*m)a&n^y%i££n+v!a:l[i]d"),
|
||||
"manyinvalid",
|
||||
);
|
||||
t.deepEqual(
|
||||
debugArtifacts.sanitizeArtifactName("\\foo\\bar//baz"),
|
||||
"foobarbaz",
|
||||
);
|
||||
});
|
||||
|
||||
// These next tests check the correctness of the logic to determine whether or not
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "./util";
|
||||
|
||||
export function sanitizeArtifactName(name: string): string {
|
||||
return name.replace(/[^a-zA-Z0-9_\\-]+/g, "");
|
||||
return name.replace(/[^a-zA-Z0-9_-]+/g, "");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"bundleVersion": "codeql-bundle-v2.21.3",
|
||||
"cliVersion": "2.21.3",
|
||||
"priorBundleVersion": "codeql-bundle-v2.21.2",
|
||||
"priorCliVersion": "2.21.2"
|
||||
"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
-12
@@ -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";
|
||||
@@ -51,6 +52,27 @@ export enum Feature {
|
||||
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",
|
||||
RustAnalysis = "rust_analysis",
|
||||
@@ -110,7 +132,7 @@ export const featureConfig: Record<
|
||||
minimumVersion: "2.15.0",
|
||||
},
|
||||
[Feature.DiffInformedQueries]: {
|
||||
defaultValue: false,
|
||||
defaultValue: true,
|
||||
envVar: "CODEQL_ACTION_DIFF_INFORMED_QUERIES",
|
||||
minimumVersion: "2.21.0",
|
||||
},
|
||||
@@ -142,6 +164,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 +616,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:",
|
||||
);
|
||||
|
||||
@@ -104,6 +104,7 @@ async function maybeUploadFailedSarif(
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeScanningTarget,
|
||||
);
|
||||
await uploadLib.waitForProcessing(
|
||||
repositoryNwo,
|
||||
|
||||
+89
-16
@@ -36,14 +36,17 @@ import { Feature, featureConfig, Features } from "./feature-flags";
|
||||
import {
|
||||
checkInstallPython311,
|
||||
cleanupDatabaseClusterDirectory,
|
||||
getOverlayDatabaseMode,
|
||||
initCodeQL,
|
||||
initConfig,
|
||||
runInit,
|
||||
} from "./init";
|
||||
import { KnownLanguage } 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 {
|
||||
@@ -105,6 +108,10 @@ interface InitWithConfigStatusReport extends InitStatusReport {
|
||||
trap_cache_download_size_bytes: number;
|
||||
/** Time taken to download TRAP caches, in milliseconds. */
|
||||
trap_cache_download_duration_ms: number;
|
||||
/** Size of the overlay-base database that we downloaded, in bytes. */
|
||||
overlay_base_database_download_size_bytes?: number;
|
||||
/** Time taken to download the overlay-base database, in milliseconds. */
|
||||
overlay_base_database_download_duration_ms?: number;
|
||||
/** Stringified JSON array of registry configuration objects, from the 'registries' config field
|
||||
or workflow input. **/
|
||||
registries: string;
|
||||
@@ -132,6 +139,7 @@ async function sendCompletedStatusReport(
|
||||
toolsFeatureFlagsValid: boolean | undefined,
|
||||
toolsSource: ToolsSource,
|
||||
toolsVersion: string,
|
||||
overlayBaseDatabaseStats: OverlayBaseDatabaseDownloadStats | undefined,
|
||||
logger: Logger,
|
||||
error?: Error,
|
||||
) {
|
||||
@@ -238,6 +246,10 @@ async function sendCompletedStatusReport(
|
||||
),
|
||||
),
|
||||
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"] ?? [],
|
||||
),
|
||||
@@ -300,6 +312,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,
|
||||
@@ -370,6 +390,7 @@ async function run() {
|
||||
config = await initConfig({
|
||||
languagesInput: getOptionalInput("languages"),
|
||||
queriesInput: getOptionalInput("queries"),
|
||||
qualityQueriesInput: getOptionalInput("quality-queries"),
|
||||
packsInput: getOptionalInput("packs"),
|
||||
buildModeInput: getOptionalInput("build-mode"),
|
||||
configFile,
|
||||
@@ -390,6 +411,7 @@ async function run() {
|
||||
tempDir: getTemporaryDirectory(),
|
||||
codeql,
|
||||
workspacePath: getRequiredEnvParam("GITHUB_WORKSPACE"),
|
||||
sourceRoot,
|
||||
githubVersion: gitHubVersion,
|
||||
apiDetails,
|
||||
features,
|
||||
@@ -416,21 +438,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);
|
||||
}
|
||||
|
||||
@@ -615,6 +659,34 @@ async function run() {
|
||||
core.exportVariable(bmnVar, value);
|
||||
}
|
||||
|
||||
// For rust: set CODEQL_ENABLE_EXPERIMENTAL_FEATURES, unless codeql already supports rust without it
|
||||
if (
|
||||
config.languages.includes(KnownLanguage.rust) &&
|
||||
!(await codeql.resolveLanguages()).rust
|
||||
) {
|
||||
const feat = Feature.RustAnalysis;
|
||||
const minVer = featureConfig[feat].minimumVersion as string;
|
||||
const envVar = "CODEQL_ENABLE_EXPERIMENTAL_FEATURES";
|
||||
// if in default setup, it means the feature flag was on when rust was enabled
|
||||
// if the feature flag gets turned off, let's not have rust analysis throwing a configuration error
|
||||
// in that case rust analysis will be disabled only when default setup is refreshed
|
||||
if (isDefaultSetup() || (await features.getValue(feat, codeql))) {
|
||||
core.exportVariable(envVar, "true");
|
||||
}
|
||||
if (process.env[envVar] !== "true") {
|
||||
throw new ConfigurationError(
|
||||
`Experimental and not officially supported Rust analysis requires setting ${envVar}=true in the environment`,
|
||||
);
|
||||
}
|
||||
const actualVer = (await codeql.getVersion()).version;
|
||||
if (semver.lt(actualVer, minVer)) {
|
||||
throw new ConfigurationError(
|
||||
`Experimental rust analysis is supported by CodeQL CLI version ${minVer} or higher, but found version ${actualVer}`,
|
||||
);
|
||||
}
|
||||
logger.info("Experimental rust analysis enabled");
|
||||
}
|
||||
|
||||
// Restore dependency cache(s), if they exist.
|
||||
if (shouldRestoreCache(config.dependencyCachingEnabled)) {
|
||||
await downloadDependencyCaches(config.languages, logger);
|
||||
@@ -674,7 +746,6 @@ async function run() {
|
||||
"Runner.Worker.exe",
|
||||
getOptionalInput("registries"),
|
||||
apiDetails,
|
||||
overlayDatabaseMode,
|
||||
logger,
|
||||
);
|
||||
if (tracerConfig !== undefined) {
|
||||
@@ -700,6 +771,7 @@ async function run() {
|
||||
toolsFeatureFlagsValid,
|
||||
toolsSource,
|
||||
toolsVersion,
|
||||
overlayBaseDatabaseStats,
|
||||
logger,
|
||||
error,
|
||||
);
|
||||
@@ -715,6 +787,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 { KnownLanguage, 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,
|
||||
|
||||
@@ -11,9 +11,9 @@ import { Credential, getCredentials } from "./start-proxy";
|
||||
import * as util from "./util";
|
||||
|
||||
const UPDATEJOB_PROXY = "update-job-proxy";
|
||||
const UPDATEJOB_PROXY_VERSION = "v2.0.20250424171100";
|
||||
const UPDATEJOB_PROXY_VERSION = "v2.0.20250624110901";
|
||||
const UPDATEJOB_PROXY_URL_PREFIX =
|
||||
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.21.1/";
|
||||
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.22.0/";
|
||||
const KEY_SIZE = 2048;
|
||||
const KEY_EXPIRY_YEARS = 2;
|
||||
|
||||
|
||||
+54
-8
@@ -8,6 +8,9 @@ import { setupTests } from "./testing-utils";
|
||||
|
||||
setupTests(test);
|
||||
|
||||
const toEncodedJSON = (data: any) =>
|
||||
Buffer.from(JSON.stringify(data)).toString("base64");
|
||||
|
||||
test("getCredentials prefers registriesCredentials over registrySecrets", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([
|
||||
@@ -28,9 +31,9 @@ test("getCredentials prefers registriesCredentials over registrySecrets", async
|
||||
t.is(credentials[0].host, "npm.pkg.github.com");
|
||||
});
|
||||
|
||||
test("getCredentials throws error when credential missing host and url", async (t) => {
|
||||
test("getCredentials throws an error when configurations are not an array", async (t) => {
|
||||
const registryCredentials = Buffer.from(
|
||||
JSON.stringify([{ type: "npm_registry", token: "abc" }]),
|
||||
JSON.stringify({ type: "npm_registry", token: "abc" }),
|
||||
).toString("base64");
|
||||
|
||||
t.throws(
|
||||
@@ -42,22 +45,66 @@ test("getCredentials throws error when credential missing host and url", async (
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must specify host or url",
|
||||
message:
|
||||
"Expected credentials data to be an array of configurations, but it is not.",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("getCredentials throws error when credential is not an object", async (t) => {
|
||||
const testCredentials = [["foo"], [null]].map(toEncodedJSON);
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must be an object",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials throws error when credential missing host and url", async (t) => {
|
||||
const testCredentials = [
|
||||
[{ type: "npm_registry", token: "abc" }],
|
||||
[{ type: "npm_registry", token: "abc", host: null }],
|
||||
[{ type: "npm_registry", token: "abc", url: null }],
|
||||
].map(toEncodedJSON);
|
||||
|
||||
for (const testCredential of testCredentials) {
|
||||
t.throws(
|
||||
() =>
|
||||
startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
testCredential,
|
||||
undefined,
|
||||
),
|
||||
{
|
||||
message: "Invalid credentials - must specify host or url",
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("getCredentials filters by language when specified", async (t) => {
|
||||
const mixedCredentials = [
|
||||
{ type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
|
||||
{ type: "maven_repository", host: "maven.pkg.github.com", token: "def" },
|
||||
{ type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" },
|
||||
{ type: "goproxy_server", host: "goproxy.example.com", token: "jkl" },
|
||||
];
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
undefined,
|
||||
Buffer.from(JSON.stringify(mixedCredentials)).toString("base64"),
|
||||
toEncodedJSON(mixedCredentials),
|
||||
"java",
|
||||
);
|
||||
t.is(credentials.length, 1);
|
||||
@@ -69,10 +116,9 @@ test("getCredentials returns all credentials when no language specified", async
|
||||
{ type: "npm_registry", host: "npm.pkg.github.com", token: "abc" },
|
||||
{ type: "maven_repository", host: "maven.pkg.github.com", token: "def" },
|
||||
{ type: "nuget_feed", host: "nuget.pkg.github.com", token: "ghi" },
|
||||
{ type: "goproxy_server", host: "goproxy.example.com", token: "jkl" },
|
||||
];
|
||||
const credentialsInput = Buffer.from(
|
||||
JSON.stringify(mixedCredentials),
|
||||
).toString("base64");
|
||||
const credentialsInput = toEncodedJSON(mixedCredentials);
|
||||
|
||||
const credentials = startProxyExports.getCredentials(
|
||||
getRunnerLogger(true),
|
||||
@@ -80,7 +126,7 @@ test("getCredentials returns all credentials when no language specified", async
|
||||
credentialsInput,
|
||||
undefined,
|
||||
);
|
||||
t.is(credentials.length, 3);
|
||||
t.is(credentials.length, mixedCredentials.length);
|
||||
});
|
||||
|
||||
test("getCredentials throws an error when non-printable characters are used", async (t) => {
|
||||
|
||||
+31
-1
@@ -1,3 +1,5 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import { KnownLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { ConfigurationError } from "./util";
|
||||
@@ -64,6 +66,15 @@ const LANGUAGE_TO_REGISTRY_TYPE: Partial<Record<KnownLanguage, string>> = {
|
||||
go: "goproxy_server",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Checks that `value` is neither `undefined` nor `null`.
|
||||
* @param value The value to test.
|
||||
* @returns Narrows the type of `value` to exclude `undefined` and `null`.
|
||||
*/
|
||||
function isDefined<T>(value: T | null | undefined): value is T {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
// getCredentials returns registry credentials from action inputs.
|
||||
// It prefers `registries_credentials` over `registry_secrets`.
|
||||
// If neither is set, it returns an empty array.
|
||||
@@ -100,9 +111,28 @@ export function getCredentials(
|
||||
throw new ConfigurationError("Invalid credentials format.");
|
||||
}
|
||||
|
||||
// Check that the parsed data is indeed an array.
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new ConfigurationError(
|
||||
"Expected credentials data to be an array of configurations, but it is not.",
|
||||
);
|
||||
}
|
||||
|
||||
const out: Credential[] = [];
|
||||
for (const e of parsed) {
|
||||
if (e.url === undefined && e.host === undefined) {
|
||||
if (e === null || typeof e !== "object") {
|
||||
throw new ConfigurationError("Invalid credentials - must be an object");
|
||||
}
|
||||
|
||||
// Mask credentials to reduce chance of accidental leakage in logs.
|
||||
if (isDefined(e.password)) {
|
||||
core.setSecret(e.password);
|
||||
}
|
||||
if (isDefined(e.token)) {
|
||||
core.setSecret(e.token);
|
||||
}
|
||||
|
||||
if (!isDefined(e.url) && !isDefined(e.host)) {
|
||||
// The proxy needs one of these to work. If both are defined, the url has the precedence.
|
||||
throw new ConfigurationError(
|
||||
"Invalid credentials - must specify host or url",
|
||||
|
||||
+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";
|
||||
|
||||
+184
-1
@@ -122,7 +122,14 @@ test("finding SARIF files", async (t) => {
|
||||
"file",
|
||||
);
|
||||
|
||||
const sarifFiles = uploadLib.findSarifFilesInDir(tmpDir);
|
||||
// add some `.quality.sarif` files that should be ignored, unless we look for them specifically
|
||||
fs.writeFileSync(path.join(tmpDir, "a.quality.sarif"), "");
|
||||
fs.writeFileSync(path.join(tmpDir, "dir1", "b.quality.sarif"), "");
|
||||
|
||||
const sarifFiles = uploadLib.findSarifFilesInDir(
|
||||
tmpDir,
|
||||
uploadLib.CodeScanningTarget.sarifPredicate,
|
||||
);
|
||||
|
||||
t.deepEqual(sarifFiles, [
|
||||
path.join(tmpDir, "a.sarif"),
|
||||
@@ -130,6 +137,16 @@ test("finding SARIF files", async (t) => {
|
||||
path.join(tmpDir, "dir1", "d.sarif"),
|
||||
path.join(tmpDir, "dir1", "dir2", "e.sarif"),
|
||||
]);
|
||||
|
||||
const qualitySarifFiles = uploadLib.findSarifFilesInDir(
|
||||
tmpDir,
|
||||
uploadLib.CodeQualityTarget.sarifPredicate,
|
||||
);
|
||||
|
||||
t.deepEqual(qualitySarifFiles, [
|
||||
path.join(tmpDir, "a.quality.sarif"),
|
||||
path.join(tmpDir, "dir1", "b.quality.sarif"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,6 +330,16 @@ test("validateUniqueCategory for multiple runs", (t) => {
|
||||
t.throws(() => uploadLib.validateUniqueCategory(sarif2));
|
||||
});
|
||||
|
||||
test("validateUniqueCategory with different prefixes", (t) => {
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif()));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(),
|
||||
uploadLib.CodeQualityTarget.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("accept results with invalid artifactLocation.uri value", (t) => {
|
||||
const loggedMessages: string[] = [];
|
||||
const mockLogger = {
|
||||
@@ -371,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(
|
||||
@@ -417,6 +456,150 @@ test("shouldShowCombineSarifFilesDeprecationWarning when environment variable is
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled when on dotcom", async (t) => {
|
||||
await t.throwsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
|
||||
{
|
||||
type: GitHubVariant.DOTCOM,
|
||||
},
|
||||
),
|
||||
{
|
||||
message:
|
||||
/The CodeQL Action does not support uploading multiple SARIF runs with the same category/,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled when on GHES 3.13", async (t) => {
|
||||
await t.notThrowsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
|
||||
{
|
||||
type: GitHubVariant.GHES,
|
||||
version: "3.13.2",
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled when on GHES 3.14", async (t) => {
|
||||
await t.notThrowsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "def"), createMockSarif("abc", "def")],
|
||||
{
|
||||
type: GitHubVariant.GHES,
|
||||
version: "3.14.0",
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
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")],
|
||||
{
|
||||
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",
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled with only 1 run", async (t) => {
|
||||
await t.notThrowsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "def")],
|
||||
{
|
||||
type: GitHubVariant.DOTCOM,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled with distinct categories", async (t) => {
|
||||
await t.notThrowsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "def"), createMockSarif("def", "def")],
|
||||
{
|
||||
type: GitHubVariant.DOTCOM,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("throwIfCombineSarifFilesDisabled with distinct tools", async (t) => {
|
||||
await t.notThrowsAsync(
|
||||
uploadLib.throwIfCombineSarifFilesDisabled(
|
||||
[createMockSarif("abc", "abc"), createMockSarif("abc", "def")],
|
||||
{
|
||||
type: GitHubVariant.DOTCOM,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("shouldConsiderConfigurationError correctly detects configuration errors", (t) => {
|
||||
const error1 = [
|
||||
"CodeQL analyses from advanced configurations cannot be processed when the default setup is enabled",
|
||||
|
||||
+139
-27
@@ -6,10 +6,8 @@ import * as core from "@actions/core";
|
||||
import { OctokitResponse } from "@octokit/types";
|
||||
import fileUrl from "file-url";
|
||||
import * as jsonschema from "jsonschema";
|
||||
import * as semver from "semver";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getOptionalInput, getRequiredInput } from "./actions-util";
|
||||
import * as api from "./api-client";
|
||||
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
@@ -30,6 +28,7 @@ import {
|
||||
getRequiredEnvParam,
|
||||
GitHubVariant,
|
||||
GitHubVersion,
|
||||
satisfiesGHESVersion,
|
||||
SarifFile,
|
||||
SarifRun,
|
||||
} from "./util";
|
||||
@@ -132,7 +131,7 @@ export async function shouldShowCombineSarifFilesDeprecationWarning(
|
||||
// Do not show this warning on GHES versions before 3.14.0
|
||||
if (
|
||||
githubVersion.type === GitHubVariant.GHES &&
|
||||
semver.lt(githubVersion.version, "3.14.0")
|
||||
satisfiesGHESVersion(githubVersion.version, "<3.14", true)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -145,6 +144,44 @@ export async function shouldShowCombineSarifFilesDeprecationWarning(
|
||||
);
|
||||
}
|
||||
|
||||
export async function throwIfCombineSarifFilesDisabled(
|
||||
sarifObjects: util.SarifFile[],
|
||||
githubVersion: GitHubVersion,
|
||||
) {
|
||||
if (!(await shouldDisableCombineSarifFiles(sarifObjects, githubVersion))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deprecationMoreInformationMessage =
|
||||
"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}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Checks whether combining SARIF files should be disabled.
|
||||
async function shouldDisableCombineSarifFiles(
|
||||
sarifObjects: util.SarifFile[],
|
||||
githubVersion: GitHubVersion,
|
||||
) {
|
||||
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)) {
|
||||
// If all runs are unique, we can safely combine them.
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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
|
||||
// CLI `github merge-results` command when all SARIF files are produced by
|
||||
// CodeQL. Otherwise, it will fall back to combining the files in the action.
|
||||
@@ -156,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;
|
||||
@@ -167,11 +201,13 @@ async function combineSarifFilesUsingCLI(
|
||||
const deprecationWarningMessage =
|
||||
gitHubVersion.type === GitHubVariant.GHES
|
||||
? "and will be removed in GitHub Enterprise Server 3.18"
|
||||
: "and will be removed on June 4, 2025";
|
||||
: "and will be removed in July 2025";
|
||||
const deprecationMoreInformationMessage =
|
||||
"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, gitHubVersion);
|
||||
|
||||
logger.debug(
|
||||
"Not all SARIF files were produced by CodeQL. Merging files in the action.",
|
||||
);
|
||||
@@ -207,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"),
|
||||
};
|
||||
@@ -235,6 +273,8 @@ async function combineSarifFilesUsingCLI(
|
||||
ToolsFeature.SarifMergeRunsFromEqualCategory,
|
||||
))
|
||||
) {
|
||||
await throwIfCombineSarifFilesDisabled(sarifObjects, gitHubVersion);
|
||||
|
||||
logger.warning(
|
||||
"The CodeQL CLI does not support merging SARIF files. Merging files in the action.",
|
||||
);
|
||||
@@ -306,12 +346,19 @@ function getAutomationID(
|
||||
return api.computeAutomationID(analysis_key, environment);
|
||||
}
|
||||
|
||||
// Enumerates API endpoints that accept SARIF files.
|
||||
export enum SARIF_UPLOAD_ENDPOINT {
|
||||
CODE_SCANNING = "PUT /repos/:owner/:repo/code-scanning/analysis",
|
||||
CODE_QUALITY = "PUT /repos/:owner/:repo/code-quality/analysis",
|
||||
}
|
||||
|
||||
// Upload the given payload.
|
||||
// If the request fails then this will retry a small number of times.
|
||||
async function uploadPayload(
|
||||
payload: any,
|
||||
repositoryNwo: RepositoryNwo,
|
||||
logger: Logger,
|
||||
target: SARIF_UPLOAD_ENDPOINT,
|
||||
): Promise<string> {
|
||||
logger.info("Uploading results");
|
||||
|
||||
@@ -332,14 +379,11 @@ async function uploadPayload(
|
||||
const client = api.getApiClient();
|
||||
|
||||
try {
|
||||
const response = await client.request(
|
||||
"PUT /repos/:owner/:repo/code-scanning/analysis",
|
||||
{
|
||||
owner: repositoryNwo.owner,
|
||||
repo: repositoryNwo.repo,
|
||||
data: payload,
|
||||
},
|
||||
);
|
||||
const response = await client.request(target, {
|
||||
owner: repositoryNwo.owner,
|
||||
repo: repositoryNwo.repo,
|
||||
data: payload,
|
||||
});
|
||||
|
||||
logger.debug(`response status: ${response.status}`);
|
||||
logger.info("Successfully uploaded results");
|
||||
@@ -378,12 +422,15 @@ export interface UploadResult {
|
||||
|
||||
// Recursively walks a directory and returns all SARIF files it finds.
|
||||
// Does not follow symlinks.
|
||||
export function findSarifFilesInDir(sarifPath: string): string[] {
|
||||
export function findSarifFilesInDir(
|
||||
sarifPath: string,
|
||||
isSarif: (name: string) => boolean,
|
||||
): string[] {
|
||||
const sarifFiles: string[] = [];
|
||||
const walkSarifFiles = (dir: string) => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith(".sarif")) {
|
||||
if (entry.isFile() && isSarif(entry.name)) {
|
||||
sarifFiles.push(path.resolve(dir, entry.name));
|
||||
} else if (entry.isDirectory()) {
|
||||
walkSarifFiles(path.resolve(dir, entry.name));
|
||||
@@ -394,7 +441,10 @@ export function findSarifFilesInDir(sarifPath: string): string[] {
|
||||
return sarifFiles;
|
||||
}
|
||||
|
||||
function getSarifFilePaths(sarifPath: string) {
|
||||
export function getSarifFilePaths(
|
||||
sarifPath: string,
|
||||
isSarif: (name: string) => boolean,
|
||||
) {
|
||||
if (!fs.existsSync(sarifPath)) {
|
||||
// This is always a configuration error, even for first-party runs.
|
||||
throw new ConfigurationError(`Path does not exist: ${sarifPath}`);
|
||||
@@ -402,7 +452,7 @@ function getSarifFilePaths(sarifPath: string) {
|
||||
|
||||
let sarifFiles: string[];
|
||||
if (fs.lstatSync(sarifPath).isDirectory()) {
|
||||
sarifFiles = findSarifFilesInDir(sarifPath);
|
||||
sarifFiles = findSarifFilesInDir(sarifPath, isSarif);
|
||||
if (sarifFiles.length === 0) {
|
||||
// This is always a configuration error, even for first-party runs.
|
||||
throw new ConfigurationError(
|
||||
@@ -567,6 +617,31 @@ export function buildPayload(
|
||||
return payloadObj;
|
||||
}
|
||||
|
||||
// Represents configurations for different services that we can upload SARIF to.
|
||||
export interface UploadTarget {
|
||||
name: string;
|
||||
target: SARIF_UPLOAD_ENDPOINT;
|
||||
sarifPredicate: (name: string) => boolean;
|
||||
sentinelPrefix: string;
|
||||
}
|
||||
|
||||
// Represents the Code Scanning upload target.
|
||||
export const CodeScanningTarget: UploadTarget = {
|
||||
name: "code scanning",
|
||||
target: SARIF_UPLOAD_ENDPOINT.CODE_SCANNING,
|
||||
sarifPredicate: (name) =>
|
||||
name.endsWith(".sarif") && !CodeQualityTarget.sarifPredicate(name),
|
||||
sentinelPrefix: "CODEQL_UPLOAD_SARIF_",
|
||||
};
|
||||
|
||||
// Represents the Code Quality upload target.
|
||||
export const CodeQualityTarget: UploadTarget = {
|
||||
name: "code quality",
|
||||
target: SARIF_UPLOAD_ENDPOINT.CODE_QUALITY,
|
||||
sarifPredicate: (name) => name.endsWith(".quality.sarif"),
|
||||
sentinelPrefix: "CODEQL_UPLOAD_QUALITY_SARIF_",
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a single SARIF file or a directory of SARIF files depending on what `inputSarifPath` refers
|
||||
* to.
|
||||
@@ -577,10 +652,35 @@ export async function uploadFiles(
|
||||
category: string | undefined,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
uploadTarget: UploadTarget,
|
||||
): Promise<UploadResult> {
|
||||
const sarifPaths = getSarifFilePaths(inputSarifPath);
|
||||
const sarifPaths = getSarifFilePaths(
|
||||
inputSarifPath,
|
||||
uploadTarget.sarifPredicate,
|
||||
);
|
||||
|
||||
logger.startGroup("Uploading results");
|
||||
return uploadSpecifiedFiles(
|
||||
sarifPaths,
|
||||
checkoutPath,
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
uploadTarget,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given array of SARIF files.
|
||||
*/
|
||||
export async function uploadSpecifiedFiles(
|
||||
sarifPaths: string[],
|
||||
checkoutPath: string,
|
||||
category: string | undefined,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
uploadTarget: UploadTarget = CodeScanningTarget,
|
||||
): Promise<UploadResult> {
|
||||
logger.startGroup(`Uploading ${uploadTarget.name} results`);
|
||||
logger.info(`Processing sarif files: ${JSON.stringify(sarifPaths)}`);
|
||||
|
||||
const gitHubVersion = await getGitHubVersion();
|
||||
@@ -604,6 +704,9 @@ export async function uploadFiles(
|
||||
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);
|
||||
@@ -621,7 +724,7 @@ export async function uploadFiles(
|
||||
const toolNames = util.getToolNames(sarif);
|
||||
|
||||
logger.debug(`Validating that each SARIF run has a unique category`);
|
||||
validateUniqueCategory(sarif);
|
||||
validateUniqueCategory(sarif, uploadTarget.sentinelPrefix);
|
||||
logger.debug(`Serializing SARIF for upload`);
|
||||
const sarifPayload = JSON.stringify(sarif);
|
||||
logger.debug(`Compressing serialized SARIF`);
|
||||
@@ -651,7 +754,12 @@ export async function uploadFiles(
|
||||
logger.debug(`Number of results in upload: ${numResultInSarif}`);
|
||||
|
||||
// Make the upload
|
||||
const sarifID = await uploadPayload(payload, getRepositoryNwo(), logger);
|
||||
const sarifID = await uploadPayload(
|
||||
payload,
|
||||
getRepositoryNwo(),
|
||||
logger,
|
||||
uploadTarget.target,
|
||||
);
|
||||
|
||||
logger.endGroup();
|
||||
|
||||
@@ -767,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 (
|
||||
@@ -833,7 +942,10 @@ function handleProcessingResultForUnsuccessfulExecution(
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUniqueCategory(sarif: SarifFile): void {
|
||||
export function validateUniqueCategory(
|
||||
sarif: SarifFile,
|
||||
sentinelPrefix: string = CodeScanningTarget.sentinelPrefix,
|
||||
): void {
|
||||
// duplicate categories are allowed in the same sarif file
|
||||
// but not across multiple sarif files
|
||||
const categories = {} as Record<string, { id?: string; tool?: string }>;
|
||||
@@ -846,7 +958,7 @@ export function validateUniqueCategory(sarif: SarifFile): void {
|
||||
}
|
||||
|
||||
for (const [category, { id, tool }] of Object.entries(categories)) {
|
||||
const sentinelEnvVar = `CODEQL_UPLOAD_SARIF_${category}`;
|
||||
const sentinelEnvVar = `${sentinelPrefix}${category}`;
|
||||
if (process.env[sentinelEnvVar]) {
|
||||
// This is always a configuration error, even for first-party runs.
|
||||
throw new ConfigurationError(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
@@ -83,15 +85,41 @@ async function run() {
|
||||
}
|
||||
|
||||
try {
|
||||
const sarifPath = actionsUtil.getRequiredInput("sarif_file");
|
||||
const checkoutPath = actionsUtil.getRequiredInput("checkout_path");
|
||||
const category = actionsUtil.getOptionalInput("category");
|
||||
|
||||
const uploadResult = await upload_lib.uploadFiles(
|
||||
actionsUtil.getRequiredInput("sarif_file"),
|
||||
actionsUtil.getRequiredInput("checkout_path"),
|
||||
actionsUtil.getOptionalInput("category"),
|
||||
sarifPath,
|
||||
checkoutPath,
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
upload_lib.CodeScanningTarget,
|
||||
);
|
||||
core.setOutput("sarif-id", uploadResult.sarifID);
|
||||
|
||||
// If there are `.quality.sarif` files in `sarifPath`, then upload those to the code quality service.
|
||||
// Code quality can currently only be enabled on top of security, so we'd currently always expect to
|
||||
// have a directory for the results here.
|
||||
if (fs.lstatSync(sarifPath).isDirectory()) {
|
||||
const qualitySarifFiles = upload_lib.findSarifFilesInDir(
|
||||
sarifPath,
|
||||
upload_lib.CodeQualityTarget.sarifPredicate,
|
||||
);
|
||||
|
||||
if (qualitySarifFiles.length !== 0) {
|
||||
await upload_lib.uploadSpecifiedFiles(
|
||||
qualitySarifFiles,
|
||||
checkoutPath,
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
upload_lib.CodeQualityTarget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't upload results in test mode, so don't wait for processing
|
||||
if (isInTestMode()) {
|
||||
core.debug("In test mode. Waiting for processing is disabled.");
|
||||
@@ -101,6 +129,8 @@ async function run() {
|
||||
uploadResult.sarifID,
|
||||
logger,
|
||||
);
|
||||
// The code quality service does not currently have an endpoint to wait for SARIF processing,
|
||||
// so we can't wait for that here.
|
||||
}
|
||||
await sendSuccessStatusReport(startedAt, uploadResult.statusReport, logger);
|
||||
} catch (unwrappedError) {
|
||||
|
||||
+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