Merge branch 'main' into checkout_v4

This commit is contained in:
David Leal 2023-09-28 14:31:04 -06:00 committed by GitHub
commit 66572c69b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
138 changed files with 119004 additions and 1814 deletions

View file

@ -1 +1 @@
{"maximumVersion": "3.11", "minimumVersion": "3.6"}
{"maximumVersion": "3.11", "minimumVersion": "3.7"}

View file

@ -1,7 +1,14 @@
import { getCodeQL } from "./codeql";
import * as core from "@actions/core";
import { getTemporaryDirectory, getWorkflowEventName } from "./actions-util";
import { getGitHubVersion } from "./api-client";
import { CodeQL, getCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { Language, isTracedLanguage } from "./languages";
import { Feature, featureConfig, Features } from "./feature-flags";
import { isTracedLanguage, Language } from "./languages";
import { Logger } from "./logging";
import { parseRepositoryNwo } from "./repository";
import { getRequiredEnvParam } from "./util";
export async function determineAutobuildLanguages(
config: configUtils.Config,
@ -91,6 +98,47 @@ export async function determineAutobuildLanguages(
return languages;
}
async function setupCppAutobuild(codeql: CodeQL, logger: Logger) {
const envVar = featureConfig[Feature.CppDependencyInstallation].envVar;
const featureName = "C++ automatic installation of dependencies";
const envDoc =
"https://docs.github.com/en/actions/learn-github-actions/variables#defining-environment-variables-for-a-single-workflow";
const gitHubVersion = await getGitHubVersion();
const repositoryNwo = parseRepositoryNwo(
getRequiredEnvParam("GITHUB_REPOSITORY"),
);
const features = new Features(
gitHubVersion,
repositoryNwo,
getTemporaryDirectory(),
logger,
);
if (await features.getValue(Feature.CppDependencyInstallation, codeql)) {
// disable autoinstall on self-hosted runners unless explicitly requested
if (
process.env["RUNNER_ENVIRONMENT"] === "self-hosted" &&
process.env[envVar] !== "true"
) {
logger.info(
`Disabling ${featureName} as we are on a self-hosted runner.${
getWorkflowEventName() !== "dynamic"
? ` To override this, set the ${envVar} environment variable to 'true' in your workflow (see ${envDoc}).`
: ""
}`,
);
core.exportVariable(envVar, "false");
} else {
logger.info(
`Enabling ${featureName}. This can be disabled by setting the ${envVar} environment variable to 'false' (see ${envDoc}).`,
);
core.exportVariable(envVar, "true");
}
} else {
logger.info(`Disabling ${featureName}.`);
core.exportVariable(envVar, "false");
}
}
export async function runAutobuild(
language: Language,
config: configUtils.Config,
@ -98,6 +146,9 @@ export async function runAutobuild(
) {
logger.startGroup(`Attempting to automatically build ${language} code`);
const codeQL = await getCodeQL(config.codeQLCmd);
if (language === Language.cpp) {
await setupCppAutobuild(codeQL, logger);
}
await codeQL.runAutobuild(language);
logger.endGroup();
}

View file

@ -564,7 +564,7 @@ test("databaseInitCluster() without injected codescanning config", async (t) =>
await util.withTmpDir(async (tempDir) => {
const runnerConstructorStub = stubToolRunnerConstructor();
const codeqlObject = await codeql.getCodeQLForTesting();
sinon.stub(codeqlObject, "getVersion").resolves("2.9.4");
sinon.stub(codeqlObject, "getVersion").resolves("2.10.5");
// safeWhich throws because of the test CodeQL object.
sinon.stub(safeWhich, "safeWhich").resolves("");

View file

@ -20,6 +20,7 @@ import {
Feature,
FeatureEnablement,
useCodeScanningConfigInCli,
CODEQL_VERSION_SUBLANGUAGE_FILE_COVERAGE,
} from "./feature-flags";
import { isTracedLanguage, Language } from "./languages";
import { Logger } from "./logging";
@ -276,7 +277,7 @@ let cachedCodeQL: CodeQL | undefined = undefined;
* The version flags below can be used to conditionally enable certain features
* on versions newer than this.
*/
const CODEQL_MINIMUM_VERSION = "2.9.4";
const CODEQL_MINIMUM_VERSION = "2.10.5";
/**
* This version will shortly become the oldest version of CodeQL that the Action will run with.
@ -293,21 +294,13 @@ const GHES_VERSION_MOST_RECENTLY_DEPRECATED = "3.6";
*/
const GHES_MOST_RECENT_DEPRECATION_DATE = "2023-09-12";
/**
/*
* Versions of CodeQL that version-flag certain functionality in the Action.
* For convenience, please keep these in descending order. Once a version
* flag is older than the oldest supported version above, it may be removed.
*/
const CODEQL_VERSION_LUA_TRACER_CONFIG = "2.10.0";
const CODEQL_VERSION_LUA_TRACING_GO_WINDOWS_FIXED = "2.10.4";
export const CODEQL_VERSION_GHES_PACK_DOWNLOAD = "2.10.4";
const CODEQL_VERSION_FILE_BASELINE_INFORMATION = "2.11.3";
/**
* Previous versions had the option already, but were missing the
* --extractor-options-verbosity that we need.
*/
export const CODEQL_VERSION_BETTER_RESOLVE_LANGUAGES = "2.10.3";
const CODEQL_VERSION_FILE_BASELINE_INFORMATION = "2.11.3";
/**
* Versions 2.11.1+ of the CodeQL Bundle include a `security-experimental` built-in query suite for
@ -558,24 +551,6 @@ export async function getCodeQLForCmd(
extraArgs.push("--begin-tracing");
extraArgs.push(...(await getTrapCachingExtractorConfigArgs(config)));
extraArgs.push(`--trace-process-name=${processName}`);
if (
// There's a bug in Lua tracing for Go on Windows in versions earlier than
// `CODEQL_VERSION_LUA_TRACING_GO_WINDOWS_FIXED`, so don't use Lua tracing
// when tracing Go on Windows on these CodeQL versions.
(await util.codeQlVersionAbove(
this,
CODEQL_VERSION_LUA_TRACER_CONFIG,
)) &&
config.languages.includes(Language.go) &&
isTracedLanguage(Language.go) &&
process.platform === "win32" &&
!(await util.codeQlVersionAbove(
this,
CODEQL_VERSION_LUA_TRACING_GO_WINDOWS_FIXED,
))
) {
extraArgs.push("--no-internal-use-lua-tracing");
}
}
// A code scanning config file is only generated if the CliConfigFileEnabled feature flag is enabled.
@ -611,6 +586,19 @@ export async function getCodeQLForCmd(
extraArgs.push("--calculate-language-specific-baseline");
}
if (
await features.getValue(Feature.SublanguageFileCoverageEnabled, this)
) {
extraArgs.push("--sublanguage-file-coverage");
} else if (
await util.codeQlVersionAbove(
this,
CODEQL_VERSION_SUBLANGUAGE_FILE_COVERAGE,
)
) {
extraArgs.push("--no-sublanguage-file-coverage");
}
await runTool(
cmd,
[

View file

@ -7,12 +7,7 @@ import * as yaml from "js-yaml";
import * as sinon from "sinon";
import * as api from "./api-client";
import {
CODEQL_VERSION_GHES_PACK_DOWNLOAD,
getCachedCodeQL,
PackDownloadOutput,
setCodeQL,
} from "./codeql";
import { getCachedCodeQL, PackDownloadOutput, setCodeQL } from "./codeql";
import * as configUtils from "./config-utils";
import { Feature } from "./feature-flags";
import { Language } from "./languages";
@ -2217,20 +2212,20 @@ test(
"0.0.1",
);
// Test that ML-powered queries ~0.3.0 are run on all platforms running `security-extended` on
// CodeQL CLI 2.9.4+.
// CodeQL CLI 2.10.5+.
test(
mlPoweredQueriesMacro,
"2.9.4",
"2.10.5",
true,
undefined,
"security-extended",
"~0.3.0",
);
// Test that ML-powered queries ~0.3.0 are run on all platforms running `security-and-quality` on
// CodeQL CLI 2.9.4+.
// CodeQL CLI 2.10.5+.
test(
mlPoweredQueriesMacro,
"2.9.4",
"2.10.5",
true,
undefined,
"security-and-quality",
@ -2554,48 +2549,6 @@ test("downloadPacks-with-registries", async (t) => {
});
});
test("downloadPacks-with-registries fails on 2.10.3", async (t) => {
// same thing, but this time include a registries block and
// associated env vars
return await withTmpDir(async (tmpDir) => {
process.env.GITHUB_TOKEN = "not-a-token";
process.env.CODEQL_REGISTRIES_AUTH = "not-a-registries-auth";
const logger = getRunnerLogger(true);
const registriesInput = yaml.dump([
{
url: "http://ghcr.io",
packages: ["codeql/*", "codeql-testing/*"],
token: "not-a-token",
},
{
url: "https://containers.GHEHOSTNAME1/v2/",
packages: "semmle/*",
token: "still-not-a-token",
},
]);
const codeQL = setCodeQL({
getVersion: () => Promise.resolve("2.10.3"),
});
await t.throwsAsync(
async () => {
return await configUtils.downloadPacks(
codeQL,
[Language.javascript, Language.java, Language.python],
{},
sampleApiDetails,
registriesInput,
tmpDir,
logger,
);
},
{ instanceOf: Error },
"'registries' input is not supported on CodeQL versions less than 2.10.4.",
);
});
});
test("downloadPacks-with-registries fails with invalid registries block", async (t) => {
// same thing, but this time include a registries block and
// associated env vars
@ -2638,51 +2591,12 @@ test("downloadPacks-with-registries fails with invalid registries block", async
});
});
// the happy path for generateRegistries is already tested in downloadPacks.
// these following tests are for the error cases and when nothing is generated.
test("no generateRegistries when CLI is too old", async (t) => {
return await withTmpDir(async (tmpDir) => {
const registriesInput = yaml.dump([
{
// no slash
url: "http://ghcr.io",
packages: ["codeql/*", "codeql-testing/*"],
token: "not-a-token",
},
]);
const codeQL = setCodeQL({
// Accepted CLI versions are 2.10.4 or higher
getVersion: () => Promise.resolve("2.10.3"),
});
const logger = getRunnerLogger(true);
await t.throwsAsync(
async () =>
await configUtils.generateRegistries(
registriesInput,
codeQL,
tmpDir,
logger,
),
undefined,
"'registries' input is not supported on CodeQL versions less than 2.10.4.",
);
});
});
test("no generateRegistries when registries is undefined", async (t) => {
return await withTmpDir(async (tmpDir) => {
const registriesInput = undefined;
const codeQL = setCodeQL({
// Accepted CLI versions are 2.10.4 or higher
getVersion: () => Promise.resolve(CODEQL_VERSION_GHES_PACK_DOWNLOAD),
});
const logger = getRunnerLogger(true);
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(
registriesInput,
codeQL,
tmpDir,
logger,
);
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
t.is(registriesAuthTokens, undefined);
t.is(qlconfigFile, undefined);
@ -2699,18 +2613,9 @@ test("generateRegistries prefers original CODEQL_REGISTRIES_AUTH", async (t) =>
token: "not-a-token",
},
]);
const codeQL = setCodeQL({
// Accepted CLI versions are 2.10.4 or higher
getVersion: () => Promise.resolve(CODEQL_VERSION_GHES_PACK_DOWNLOAD),
});
const logger = getRunnerLogger(true);
const { registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(
registriesInput,
codeQL,
tmpDir,
logger,
);
await configUtils.generateRegistries(registriesInput, tmpDir, logger);
t.is(registriesAuthTokens, "original");
t.is(qlconfigFile, path.join(tmpDir, "qlconfig.yml"));

View file

@ -8,7 +8,6 @@ import * as semver from "semver";
import * as api from "./api-client";
import {
CodeQL,
CODEQL_VERSION_GHES_PACK_DOWNLOAD,
CODEQL_VERSION_LANGUAGE_ALIASING,
CODEQL_VERSION_SECURITY_EXPERIMENTAL_SUITE,
ResolveQueriesOutput,
@ -1977,7 +1976,6 @@ export async function downloadPacks(
// This code path is only used when config parsing occurs in the Action.
const { registriesAuthTokens, qlconfigFile } = await generateRegistries(
registriesInput,
codeQL,
tempDir,
logger,
);
@ -2033,7 +2031,6 @@ export async function downloadPacks(
*/
export async function generateRegistries(
registriesInput: string | undefined,
codeQL: CodeQL,
tempDir: string,
logger: Logger,
) {
@ -2041,14 +2038,6 @@ export async function generateRegistries(
let registriesAuthTokens: string | undefined;
let qlconfigFile: string | undefined;
if (registries) {
if (
!(await codeQlVersionAbove(codeQL, CODEQL_VERSION_GHES_PACK_DOWNLOAD))
) {
throw new UserError(
`The 'registries' input is not supported on CodeQL CLI versions earlier than ${CODEQL_VERSION_GHES_PACK_DOWNLOAD}. Please upgrade to CodeQL CLI version ${CODEQL_VERSION_GHES_PACK_DOWNLOAD} or later.`,
);
}
// generate a qlconfig.yml file to hold the registry configs.
const qlconfig = createRegistriesBlock(registries);
qlconfigFile = path.join(tempDir, "qlconfig.yml");

View file

@ -1,6 +1,6 @@
{
"bundleVersion": "codeql-bundle-v2.14.5",
"cliVersion": "2.14.5",
"priorBundleVersion": "codeql-bundle-v2.14.4",
"priorCliVersion": "2.14.4"
"bundleVersion": "codeql-bundle-v2.14.6",
"cliVersion": "2.14.6",
"priorBundleVersion": "codeql-bundle-v2.14.5",
"priorCliVersion": "2.14.5"
}

View file

@ -24,9 +24,15 @@ export const CODEQL_VERSION_BUNDLE_SEMANTICALLY_VERSIONED = "2.13.4";
export const CODEQL_VERSION_ANALYSIS_SUMMARY_V2 = "2.14.0";
/**
* Versions 2.14.0+ of the CodeQL CLI support intra-layer parallelism (aka fine-grained parallelism) options.
* Versions 2.14.0+ of the CodeQL CLI support intra-layer parallelism (aka fine-grained parallelism) options, but we
* limit to 2.14.6 onwards, since that's the version that has mitigations against OOM failures.
*/
export const CODEQL_VERSION_INTRA_LAYER_PARALLELISM = "2.14.0";
export const CODEQL_VERSION_INTRA_LAYER_PARALLELISM = "2.14.6";
/**
* Versions 2.15.0+ of the CodeQL CLI support sub-language file coverage information.
*/
export const CODEQL_VERSION_SUBLANGUAGE_FILE_COVERAGE = "2.15.0";
export interface CodeQLDefaultVersionInfo {
cliVersion: string;
@ -51,12 +57,14 @@ export enum Feature {
AnalysisSummaryV2Enabled = "analysis_summary_v2_enabled",
CliConfigFileEnabled = "cli_config_file_enabled",
CodeqlJavaLombokEnabled = "codeql_java_lombok_enabled",
CppDependencyInstallation = "cpp_dependency_installation_enabled",
DisableKotlinAnalysisEnabled = "disable_kotlin_analysis_enabled",
DisablePythonDependencyInstallationEnabled = "disable_python_dependency_installation_enabled",
EvaluatorIntraLayerParallelismEnabled = "evaluator_intra_layer_parallelism_enabled",
ExportDiagnosticsEnabled = "export_diagnostics_enabled",
MlPoweredQueriesEnabled = "ml_powered_queries_enabled",
QaTelemetryEnabled = "qa_telemetry_enabled",
SublanguageFileCoverageEnabled = "sublanguage_file_coverage_enabled",
UploadFailedSarifEnabled = "upload_failed_sarif_enabled",
}
@ -74,6 +82,11 @@ export const featureConfig: Record<
minimumVersion: "2.14.0",
defaultValue: false,
},
[Feature.CppDependencyInstallation]: {
envVar: "CODEQL_EXTRACTOR_CPP_AUTOINSTALL_DEPENDENCIES",
minimumVersion: "2.15.0",
defaultValue: false,
},
[Feature.DisableKotlinAnalysisEnabled]: {
envVar: "CODEQL_DISABLE_KOTLIN_ANALYSIS",
minimumVersion: undefined,
@ -104,6 +117,11 @@ export const featureConfig: Record<
minimumVersion: undefined,
defaultValue: false,
},
[Feature.SublanguageFileCoverageEnabled]: {
envVar: "CODEQL_ACTION_SUBLANGUAGE_FILE_COVERAGE",
minimumVersion: CODEQL_VERSION_SUBLANGUAGE_FILE_COVERAGE,
defaultValue: false,
},
[Feature.UploadFailedSarifEnabled]: {
envVar: "CODEQL_ACTION_UPLOAD_FAILED_SARIF",
minimumVersion: "2.11.3",

View file

@ -217,8 +217,6 @@ async function run() {
core.exportVariable(EnvVar.JOB_RUN_UUID, uuidV4());
try {
const workflowErrors = await validateWorkflow(logger);
if (
!(await sendStatusReport(
await createStatusReportBase(
@ -226,7 +224,6 @@ async function run() {
"starting",
startedAt,
await checkDiskUsage(logger),
workflowErrors,
),
))
) {
@ -250,6 +247,8 @@ async function run() {
toolsVersion = initCodeQLResult.toolsVersion;
toolsSource = initCodeQLResult.toolsSource;
await validateWorkflow(codeql, logger);
config = await initConfig(
getOptionalInput("languages"),
getOptionalInput("queries"),

View file

@ -118,7 +118,6 @@ export async function runInit(
({ registriesAuthTokens, qlconfigFile } =
await configUtils.generateRegistries(
registriesInput,
codeql,
config.tempDir,
logger,
));

View file

@ -4,11 +4,11 @@ import * as path from "path";
import * as cache from "@actions/cache";
import * as actionsUtil from "./actions-util";
import { CodeQL, CODEQL_VERSION_BETTER_RESOLVE_LANGUAGES } from "./codeql";
import { CodeQL } from "./codeql";
import type { Config } from "./config-utils";
import { Language } from "./languages";
import { Logger } from "./logging";
import { codeQlVersionAbove, tryGetFolderBytes, withTimeout } from "./util";
import { tryGetFolderBytes, withTimeout } from "./util";
// This constant should be bumped if we make a breaking change
// to how the CodeQL Action stores or retrieves the TRAP cache,
@ -164,10 +164,6 @@ export async function getLanguagesSupportingCaching(
logger: Logger,
): Promise<Language[]> {
const result: Language[] = [];
if (
!(await codeQlVersionAbove(codeql, CODEQL_VERSION_BETTER_RESOLVE_LANGUAGES))
)
return result;
const resolveResult = await codeql.betterResolveLanguages();
outer: for (const lang of languages) {
const extractorsForLanguage = resolveResult.extractors[lang];

View file

@ -260,6 +260,7 @@ function getCgroupMemoryLimitBytes(
}
const limit = Number(fs.readFileSync(limitFile, "utf8"));
if (!Number.isInteger(limit)) {
logger.debug(
`While resolving RAM, ignored the file ${limitFile} that may contain a cgroup memory limit ` +
@ -269,6 +270,14 @@ function getCgroupMemoryLimitBytes(
}
const displayLimit = `${Math.floor(limit / (1024 * 1024))} MiB`;
if (limit > os.totalmem()) {
logger.debug(
`While resolving RAM, ignored the file ${limitFile} that may contain a cgroup memory limit as ` +
`its contents ${displayLimit} were greater than the total amount of system memory.`,
);
return undefined;
}
if (limit < MINIMUM_CGROUP_MEMORY_LIMIT_BYTES) {
logger.info(
`While resolving RAM, ignored a cgroup limit of ${displayLimit} in ${limitFile} as it was below ${

View file

@ -1,6 +1,8 @@
import test from "ava";
import test, { ExecutionContext } from "ava";
import * as yaml from "js-yaml";
import * as sinon from "sinon";
import { getCodeQLForTesting } from "./codeql";
import { setupTests } from "./testing-utils";
import {
CodedError,
@ -22,227 +24,395 @@ function errorCodes(
setupTests(test);
test("getWorkflowErrors() when on is empty", (t) => {
const errors = getWorkflowErrors({ on: {} });
test("getWorkflowErrors() when on is empty", async (t) => {
const errors = await getWorkflowErrors(
{ on: {} },
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is an array missing pull_request", (t) => {
const errors = getWorkflowErrors({ on: ["push"] });
test("getWorkflowErrors() when on.push is an array missing pull_request", async (t) => {
const errors = await getWorkflowErrors(
{ on: ["push"] },
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is an array missing push", (t) => {
const errors = getWorkflowErrors({ on: ["pull_request"] });
test("getWorkflowErrors() when on.push is an array missing push", async (t) => {
const errors = await getWorkflowErrors(
{ on: ["pull_request"] },
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, [WorkflowErrors.MissingPushHook]));
});
test("getWorkflowErrors() when on.push is valid", (t) => {
const errors = getWorkflowErrors({
on: ["push", "pull_request"],
});
test("getWorkflowErrors() when on.push is valid", async (t) => {
const errors = await getWorkflowErrors(
{
on: ["push", "pull_request"],
},
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is a valid superset", (t) => {
const errors = getWorkflowErrors({
on: ["push", "pull_request", "schedule"],
});
test("getWorkflowErrors() when on.push is a valid superset", async (t) => {
const errors = await getWorkflowErrors(
{
on: ["push", "pull_request", "schedule"],
},
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is a correct object", (t) => {
const errors = getWorkflowErrors({
on: { push: { branches: ["main"] }, pull_request: { branches: ["main"] } },
});
test("getWorkflowErrors() when on.push is a correct object", async (t) => {
const errors = await getWorkflowErrors(
{
on: {
push: { branches: ["main"] },
pull_request: { branches: ["main"] },
},
},
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.pull_requests is a string and correct", (t) => {
const errors = getWorkflowErrors({
on: { push: { branches: "*" }, pull_request: { branches: "*" } },
});
test("getWorkflowErrors() when on.pull_requests is a string and correct", async (t) => {
const errors = await getWorkflowErrors(
{
on: { push: { branches: "*" }, pull_request: { branches: "*" } },
},
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is correct with empty objects", (t) => {
const errors = getWorkflowErrors(
test("getWorkflowErrors() when on.push is correct with empty objects", async (t) => {
const errors = await getWorkflowErrors(
yaml.load(`
on:
push:
pull_request:
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push is not mismatched", (t) => {
const errors = getWorkflowErrors({
on: {
push: { branches: ["main", "feature"] },
pull_request: { branches: ["main"] },
test("getWorkflowErrors() when on.push is not mismatched", async (t) => {
const errors = await getWorkflowErrors(
{
on: {
push: { branches: ["main", "feature"] },
pull_request: { branches: ["main"] },
},
},
});
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() for a range of malformed workflows", (t) => {
test("getWorkflowErrors() for a range of malformed workflows", async (t) => {
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: {
push: 1,
pull_request: 1,
},
} as Workflow),
[],
),
);
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: 1,
} as Workflow),
[],
),
);
t.deepEqual(
...errorCodes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkflowErrors({
on: 1,
jobs: 1,
} as any),
[],
),
);
t.deepEqual(
...errorCodes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkflowErrors({
on: 1,
jobs: [1],
} as any),
[],
),
);
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: 1,
jobs: { 1: 1 },
} as Workflow),
[],
),
);
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: 1,
jobs: { test: 1 },
} as Workflow),
[],
),
);
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: 1,
jobs: { test: [1] },
} as Workflow),
[],
),
);
t.deepEqual(
...errorCodes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkflowErrors({
on: 1,
jobs: { test: { steps: 1 } },
} as any),
[],
),
);
t.deepEqual(
...errorCodes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkflowErrors({
on: 1,
jobs: { test: { steps: [{ notrun: "git checkout HEAD^2" }] } },
} as any),
[],
),
);
t.deepEqual(
...errorCodes(
getWorkflowErrors({
on: 1,
jobs: { test: [undefined] },
} as Workflow),
[],
),
);
t.deepEqual(...errorCodes(getWorkflowErrors(1 as Workflow), []));
t.deepEqual(
...errorCodes(
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
getWorkflowErrors({
on: {
push: {
branches: 1,
await getWorkflowErrors(
{
on: {
push: 1,
pull_request: 1,
},
pull_request: {
branches: 1,
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: 1,
} as unknown as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: [1],
} as unknown as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { 1: 1 },
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { test: 1 },
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { test: [1] },
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { test: { steps: 1 } },
} as unknown as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { test: { steps: [{ notrun: "git checkout HEAD^2" }] } },
} as unknown as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: 1,
jobs: { test: [undefined] },
} as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(1 as Workflow, await getCodeQLForTesting()),
[],
),
);
t.deepEqual(
...errorCodes(
await getWorkflowErrors(
{
on: {
push: {
branches: 1,
},
pull_request: {
branches: 1,
},
},
},
} as any),
} as unknown as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
});
test("getWorkflowErrors() when on.pull_request for wildcard branches", (t) => {
const errors = getWorkflowErrors({
on: {
push: { branches: ["feature/*"] },
pull_request: { branches: "feature/moose" },
test("getWorkflowErrors() when on.pull_request for wildcard branches", async (t) => {
const errors = await getWorkflowErrors(
{
on: {
push: { branches: ["feature/*"] },
pull_request: { branches: "feature/moose" },
},
},
});
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when HEAD^2 is checked out", (t) => {
test("getWorkflowErrors() when HEAD^2 is checked out", async (t) => {
process.env.GITHUB_JOB = "test";
const errors = getWorkflowErrors({
on: ["push", "pull_request"],
jobs: { test: { steps: [{ run: "git checkout HEAD^2" }] } },
});
const errors = await getWorkflowErrors(
{
on: ["push", "pull_request"],
jobs: { test: { steps: [{ run: "git checkout HEAD^2" }] } },
},
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, [WorkflowErrors.CheckoutWrongHead]));
});
test("getWorkflowErrors() produces an error for workflow with language name and its alias", async (t) => {
await testLanguageAliases(
t,
["java", "kotlin"],
{ java: ["java-kotlin", "kotlin"] },
[
"CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " +
"parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " +
"matrix parameter to keep only one of the following: 'java', 'kotlin'.",
],
);
});
test("getWorkflowErrors() produces an error for workflow with two aliases same language", async (t) => {
await testLanguageAliases(
t,
["java-kotlin", "kotlin"],
{ java: ["java-kotlin", "kotlin"] },
[
"CodeQL language 'java' is referenced by more than one entry in the 'language' matrix " +
"parameter for job 'test'. This may result in duplicate alerts. Please edit the 'language' " +
"matrix parameter to keep only one of the following: 'java-kotlin', 'kotlin'.",
],
);
});
test("getWorkflowErrors() does not produce an error for workflow with two distinct languages", async (t) => {
await testLanguageAliases(
t,
["java", "typescript"],
{
java: ["java-kotlin", "kotlin"],
javascript: ["javascript-typescript", "typescript"],
},
[],
);
});
test("getWorkflowErrors() does not produce an error if codeql doesn't support language aliases", async (t) => {
await testLanguageAliases(t, ["java-kotlin", "kotlin"], undefined, []);
});
async function testLanguageAliases(
t: ExecutionContext<unknown>,
matrixLanguages: string[],
aliases: { [languageName: string]: string[] } | undefined,
expectedErrorMessages: string[],
) {
process.env.GITHUB_JOB = "test";
const codeql = await getCodeQLForTesting();
sinon.stub(codeql, "betterResolveLanguages").resolves({
aliases:
aliases !== undefined
? // Remap from languageName -> aliases to alias -> languageName
Object.assign(
{},
...Object.entries(aliases).flatMap(([language, languageAliases]) =>
languageAliases.map((alias) => ({
[alias]: language,
})),
),
)
: undefined,
extractors: {
java: [
{
extractor_root: "",
},
],
},
});
const errors = await getWorkflowErrors(
{
on: ["push", "pull_request"],
jobs: {
test: {
strategy: {
matrix: {
language: matrixLanguages,
},
},
steps: [
{ uses: "actions/checkout@v2" },
{ uses: "github/codeql-action/init@v2" },
{ uses: "github/codeql-action/analyze@v2" },
],
},
},
} as Workflow,
codeql,
);
t.is(errors.length, expectedErrorMessages.length);
t.deepEqual(
errors.map((e) => e.message),
expectedErrorMessages,
);
}
test("formatWorkflowErrors() when there is one error", (t) => {
const message = formatWorkflowErrors([WorkflowErrors.CheckoutWrongHead]);
t.true(message.startsWith("1 issue was detected with this workflow:"));
@ -297,8 +467,8 @@ test("patternIsSuperset()", (t) => {
);
});
test("getWorkflowErrors() when branches contain dots", (t) => {
const errors = getWorkflowErrors(
test("getWorkflowErrors() when branches contain dots", async (t) => {
const errors = await getWorkflowErrors(
yaml.load(`
on:
push:
@ -307,13 +477,14 @@ test("getWorkflowErrors() when branches contain dots", (t) => {
# The branches below must be a subset of the branches above
branches: [4.1, master]
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on.push has a trailing comma", (t) => {
const errors = getWorkflowErrors(
test("getWorkflowErrors() when on.push has a trailing comma", async (t) => {
const errors = await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on:
@ -323,15 +494,16 @@ test("getWorkflowErrors() when on.push has a trailing comma", (t) => {
# The branches below must be a subset of the branches above
branches: [master]
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() should only report the current job's CheckoutWrongHead", (t) => {
test("getWorkflowErrors() should only report the current job's CheckoutWrongHead", async (t) => {
process.env.GITHUB_JOB = "test";
const errors = getWorkflowErrors(
const errors = await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on:
@ -352,15 +524,16 @@ test("getWorkflowErrors() should only report the current job's CheckoutWrongHead
test3:
steps: []
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, [WorkflowErrors.CheckoutWrongHead]));
});
test("getWorkflowErrors() should not report a different job's CheckoutWrongHead", (t) => {
test("getWorkflowErrors() should not report a different job's CheckoutWrongHead", async (t) => {
process.env.GITHUB_JOB = "test3";
const errors = getWorkflowErrors(
const errors = await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on:
@ -381,29 +554,32 @@ test("getWorkflowErrors() should not report a different job's CheckoutWrongHead"
test3:
steps: []
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() when on is missing", (t) => {
const errors = getWorkflowErrors(
test("getWorkflowErrors() when on is missing", async (t) => {
const errors = await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
`) as Workflow,
await getCodeQLForTesting(),
);
t.deepEqual(...errorCodes(errors, []));
});
test("getWorkflowErrors() with a different on setup", (t) => {
test("getWorkflowErrors() with a different on setup", async (t) => {
t.deepEqual(
...errorCodes(
getWorkflowErrors(
await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on: "workflow_dispatch"
`) as Workflow,
await getCodeQLForTesting(),
),
[],
),
@ -411,11 +587,12 @@ test("getWorkflowErrors() with a different on setup", (t) => {
t.deepEqual(
...errorCodes(
getWorkflowErrors(
await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on: [workflow_dispatch]
`) as Workflow,
await getCodeQLForTesting(),
),
[],
),
@ -423,28 +600,30 @@ test("getWorkflowErrors() with a different on setup", (t) => {
t.deepEqual(
...errorCodes(
getWorkflowErrors(
await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on:
workflow_dispatch: {}
`) as Workflow,
await getCodeQLForTesting(),
),
[],
),
);
});
test("getWorkflowErrors() should not report an error if PRs are totally unconfigured", (t) => {
test("getWorkflowErrors() should not report an error if PRs are totally unconfigured", async (t) => {
t.deepEqual(
...errorCodes(
getWorkflowErrors(
await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on:
push:
branches: [master]
`) as Workflow,
await getCodeQLForTesting(),
),
[],
),
@ -452,11 +631,12 @@ test("getWorkflowErrors() should not report an error if PRs are totally unconfig
t.deepEqual(
...errorCodes(
getWorkflowErrors(
await getWorkflowErrors(
yaml.load(`
name: "CodeQL"
on: ["push"]
`) as Workflow,
await getCodeQLForTesting(),
),
[],
),

View file

@ -6,6 +6,7 @@ import * as core from "@actions/core";
import * as yaml from "js-yaml";
import * as api from "./api-client";
import { CodeQL } from "./codeql";
import { EnvVar } from "./environment";
import { Logger } from "./logging";
import { getRequiredEnvParam, isInTestMode } from "./util";
@ -21,6 +22,7 @@ interface WorkflowJob {
name?: string;
"runs-on"?: string;
steps?: WorkflowJobStep[];
strategy?: { matrix: { [key: string]: string[] } };
uses?: string;
}
@ -104,7 +106,37 @@ export const WorkflowErrors = toCodedErrors({
CheckoutWrongHead: `git checkout HEAD^2 is no longer necessary. Please remove this step as Code Scanning recommends analyzing the merge commit for best results.`,
});
export function getWorkflowErrors(doc: Workflow): CodedError[] {
/**
* Groups the given list of CodeQL languages by their extractor name.
*
* Resolves to `undefined` if the CodeQL version does not support language aliasing.
*/
async function groupLanguagesByExtractor(
languages: string[],
codeql: CodeQL,
): Promise<{ [extractorName: string]: string[] } | undefined> {
const resolveResult = await codeql.betterResolveLanguages();
if (!resolveResult.aliases) {
return undefined;
}
const aliases = resolveResult.aliases;
const languagesByExtractor: {
[extractorName: string]: string[];
} = {};
for (const language of languages) {
const extractorName = aliases[language] || language;
if (!languagesByExtractor[extractorName]) {
languagesByExtractor[extractorName] = [];
}
languagesByExtractor[extractorName].push(language);
}
return languagesByExtractor;
}
export async function getWorkflowErrors(
doc: Workflow,
codeql: CodeQL,
): Promise<CodedError[]> {
const errors: CodedError[] = [];
const jobName = process.env.GITHUB_JOB;
@ -112,6 +144,38 @@ export function getWorkflowErrors(doc: Workflow): CodedError[] {
if (jobName) {
const job = doc?.jobs?.[jobName];
if (job?.strategy?.matrix?.language) {
const matrixLanguages = job.strategy.matrix.language;
if (Array.isArray(matrixLanguages)) {
// Map extractors to entries in the `language` matrix parameter. This will allow us to
// detect languages which are analyzed in more than one job.
const matrixLanguagesByExtractor = await groupLanguagesByExtractor(
matrixLanguages,
codeql,
);
// If the CodeQL version does not support language aliasing, then `matrixLanguagesByExtractor`
// will be `undefined`. In this case, we cannot detect duplicate languages in the matrix.
if (matrixLanguagesByExtractor !== undefined) {
// Check for duplicate languages in the matrix
for (const [extractor, languages] of Object.entries(
matrixLanguagesByExtractor,
)) {
if (languages.length > 1) {
errors.push({
message:
`CodeQL language '${extractor}' is referenced by more than one entry in the ` +
`'language' matrix parameter for job '${jobName}'. This may result in duplicate alerts. ` +
`Please edit the 'language' matrix parameter to keep only one of the following: ${languages
.map((language) => `'${language}'`)
.join(", ")}.`,
code: "DuplicateLanguageInMatrix",
});
}
}
}
}
}
const steps = job?.steps;
if (Array.isArray(steps)) {
@ -163,6 +227,7 @@ export function getWorkflowErrors(doc: Workflow): CodedError[] {
}
export async function validateWorkflow(
codeql: CodeQL,
logger: Logger,
): Promise<undefined | string> {
let workflow: Workflow;
@ -173,7 +238,7 @@ export async function validateWorkflow(
}
let workflowErrors: CodedError[];
try {
workflowErrors = getWorkflowErrors(workflow);
workflowErrors = await getWorkflowErrors(workflow, codeql);
} catch (e) {
return `error: getWorkflowErrors() failed: ${String(e)}`;
}