mirror of
https://github.com/github/codeql-action.git
synced 2026-04-27 09:18:47 +00:00
Merge pull request #3064 from github/mbg/cq/allow-cq-only-analysis
Allow Code Quality only analysis
This commit is contained in:
@@ -41,3 +41,48 @@ export async function parseAnalysisKinds(
|
||||
|
||||
/** The queries to use for Code Quality analyses. */
|
||||
export const codeQualityQueries: string[] = ["code-quality"];
|
||||
|
||||
// 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",
|
||||
}
|
||||
|
||||
// Represents configurations for different analysis kinds.
|
||||
export interface AnalysisConfig {
|
||||
/** The analysis kind the configuration is for. */
|
||||
kind: AnalysisKind;
|
||||
/** A display friendly name for logs. */
|
||||
name: string;
|
||||
/** The API endpoint to upload SARIF files to. */
|
||||
target: SARIF_UPLOAD_ENDPOINT;
|
||||
/** The file extension for SARIF files generated by this kind of analysis. */
|
||||
sarifExtension: string;
|
||||
/** A predicate on filenames to decide whether a SARIF file
|
||||
* belongs to this kind of analysis. */
|
||||
sarifPredicate: (name: string) => boolean;
|
||||
/** A prefix for environment variables used to track the uniqueness of SARIF uploads. */
|
||||
sentinelPrefix: string;
|
||||
}
|
||||
|
||||
// Represents the Code Scanning analysis configuration.
|
||||
export const CodeScanning: AnalysisConfig = {
|
||||
kind: AnalysisKind.CodeScanning,
|
||||
name: "code scanning",
|
||||
target: SARIF_UPLOAD_ENDPOINT.CODE_SCANNING,
|
||||
sarifExtension: ".sarif",
|
||||
sarifPredicate: (name) =>
|
||||
name.endsWith(CodeScanning.sarifExtension) &&
|
||||
!CodeQuality.sarifPredicate(name),
|
||||
sentinelPrefix: "CODEQL_UPLOAD_SARIF_",
|
||||
};
|
||||
|
||||
// Represents the Code Quality analysis configuration.
|
||||
export const CodeQuality: AnalysisConfig = {
|
||||
kind: AnalysisKind.CodeQuality,
|
||||
name: "code quality",
|
||||
target: SARIF_UPLOAD_ENDPOINT.CODE_QUALITY,
|
||||
sarifExtension: ".quality.sarif",
|
||||
sarifPredicate: (name) => name.endsWith(CodeQuality.sarifExtension),
|
||||
sentinelPrefix: "CODEQL_UPLOAD_QUALITY_SARIF_",
|
||||
};
|
||||
|
||||
+19
-11
@@ -5,6 +5,7 @@ import { performance } from "perf_hooks";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import {
|
||||
CodeQLAnalysisError,
|
||||
dbIsFinalized,
|
||||
@@ -18,7 +19,12 @@ import { getApiDetails, getGitHubVersion } from "./api-client";
|
||||
import { runAutobuild } from "./autobuild";
|
||||
import { getTotalCacheSize, shouldStoreCache } from "./caching-utils";
|
||||
import { getCodeQL } from "./codeql";
|
||||
import { Config, getConfig, isCodeQualityEnabled } from "./config-utils";
|
||||
import {
|
||||
Config,
|
||||
getConfig,
|
||||
isCodeQualityEnabled,
|
||||
isCodeScanningEnabled,
|
||||
} from "./config-utils";
|
||||
import { uploadDatabases } from "./database-upload";
|
||||
import { uploadDependencyCaches } from "./dependency-caching";
|
||||
import { getDiffInformedAnalysisBranches } from "./diff-informed-analysis-utils";
|
||||
@@ -326,15 +332,17 @@ async function run() {
|
||||
core.setOutput("sarif-output", path.resolve(outputDir));
|
||||
const uploadInput = actionsUtil.getOptionalInput("upload");
|
||||
if (runStats && actionsUtil.getUploadValue(uploadInput) === "always") {
|
||||
uploadResult = await uploadLib.uploadFiles(
|
||||
outputDir,
|
||||
actionsUtil.getRequiredInput("checkout_path"),
|
||||
actionsUtil.getOptionalInput("category"),
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeScanningTarget,
|
||||
);
|
||||
core.setOutput("sarif-id", uploadResult.sarifID);
|
||||
if (isCodeScanningEnabled(config)) {
|
||||
uploadResult = await uploadLib.uploadFiles(
|
||||
outputDir,
|
||||
actionsUtil.getRequiredInput("checkout_path"),
|
||||
actionsUtil.getOptionalInput("category"),
|
||||
features,
|
||||
logger,
|
||||
analyses.CodeScanning,
|
||||
);
|
||||
core.setOutput("sarif-id", uploadResult.sarifID);
|
||||
}
|
||||
|
||||
if (isCodeQualityEnabled(config)) {
|
||||
const qualityUploadResult = await uploadLib.uploadFiles(
|
||||
@@ -346,7 +354,7 @@ async function run() {
|
||||
),
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeQualityTarget,
|
||||
analyses.CodeQuality,
|
||||
);
|
||||
core.setOutput("quality-sarif-id", qualityUploadResult.sarifID);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import test from "ava";
|
||||
import * as sinon from "sinon";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { CodeQuality, CodeScanning } from "./analyses";
|
||||
import {
|
||||
exportedForTesting,
|
||||
runQueries,
|
||||
defaultSuites,
|
||||
resolveQuerySuiteAlias,
|
||||
addSarifExtension,
|
||||
} from "./analyze";
|
||||
import { createStubCodeQL } from "./codeql";
|
||||
import { Feature } from "./feature-flags";
|
||||
@@ -348,3 +350,13 @@ test("resolveQuerySuiteAlias", (t) => {
|
||||
t.deepEqual(resolveQuerySuiteAlias(KnownLanguage.go, name), name);
|
||||
}
|
||||
});
|
||||
|
||||
test("addSarifExtension", (t) => {
|
||||
for (const language of Object.values(KnownLanguage)) {
|
||||
t.deepEqual(addSarifExtension(CodeScanning, language), `${language}.sarif`);
|
||||
t.deepEqual(
|
||||
addSarifExtension(CodeQuality, language),
|
||||
`${language}.quality.sarif`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+79
-27
@@ -608,6 +608,16 @@ export function resolveQuerySuiteAlias(
|
||||
return maybeSuite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the appropriate file extension for the given analysis configuration to the given base filename.
|
||||
*/
|
||||
export function addSarifExtension(
|
||||
analysis: analyses.AnalysisConfig,
|
||||
base: string,
|
||||
): string {
|
||||
return `${base}${analysis.sarifExtension}`;
|
||||
}
|
||||
|
||||
// Runs queries and creates sarif files in the given folder
|
||||
export async function runQueries(
|
||||
sarifFolder: string,
|
||||
@@ -650,15 +660,25 @@ export async function runQueries(
|
||||
? `--sarif-run-property=incrementalMode=${incrementalMode.join(",")}`
|
||||
: undefined;
|
||||
|
||||
const dbAnalysisConfig = configUtils.getPrimaryAnalysisConfig(config);
|
||||
|
||||
for (const language of config.languages) {
|
||||
try {
|
||||
const sarifFile = path.join(sarifFolder, `${language}.sarif`);
|
||||
|
||||
// This should be empty to run only the query suite that was generated when
|
||||
// the database was initialised.
|
||||
const queries: string[] = [];
|
||||
if (configUtils.isCodeQualityEnabled(config)) {
|
||||
|
||||
// If multiple analysis kinds are enabled, the database is initialised for Code Scanning.
|
||||
// To avoid duplicate work, we want to run queries for all analyses at the same time.
|
||||
// To do this, we invoke `run-queries` once with the generated query suite that was created
|
||||
// when the database was initialised + the queries for other analysis kinds.
|
||||
if (config.analysisKinds.length > 1) {
|
||||
queries.push(util.getGeneratedSuitePath(config, language));
|
||||
for (const qualityQuery of analyses.codeQualityQueries) {
|
||||
queries.push(resolveQuerySuiteAlias(language, qualityQuery));
|
||||
|
||||
if (configUtils.isCodeQualityEnabled(config)) {
|
||||
for (const qualityQuery of analyses.codeQualityQueries) {
|
||||
queries.push(resolveQuerySuiteAlias(language, qualityQuery));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,48 +696,49 @@ export async function runQueries(
|
||||
statusReport[`analyze_builtin_queries_${language}_duration_ms`] =
|
||||
new Date().getTime() - startTimeRunQueries;
|
||||
|
||||
logger.startGroup(`Interpreting results for ${language}`);
|
||||
// There is always at least one analysis kind enabled. Running `interpret-results`
|
||||
// produces the SARIF file for the analysis kind that the database was initialised with.
|
||||
const startTimeInterpretResults = new Date();
|
||||
const analysisSummary = await runInterpretResults(
|
||||
language,
|
||||
undefined,
|
||||
sarifFile,
|
||||
config.debugMode,
|
||||
automationDetailsId,
|
||||
);
|
||||
const { summary: analysisSummary, sarifFile } =
|
||||
await runInterpretResultsFor(
|
||||
dbAnalysisConfig,
|
||||
language,
|
||||
undefined,
|
||||
config.debugMode,
|
||||
);
|
||||
|
||||
// This case is only needed if Code Quality is not the sole analysis kind.
|
||||
// In this case, we will have run queries for all analysis kinds. The previous call to
|
||||
// `interpret-results` will have produced a SARIF file for Code Scanning and we now
|
||||
// need to produce an additional SARIF file for Code Quality.
|
||||
let qualityAnalysisSummary: string | undefined;
|
||||
if (configUtils.isCodeQualityEnabled(config)) {
|
||||
logger.info(`Interpreting quality results for ${language}`);
|
||||
const qualityCategory = fixCodeQualityCategory(
|
||||
logger,
|
||||
automationDetailsId,
|
||||
);
|
||||
const qualitySarifFile = path.join(
|
||||
sarifFolder,
|
||||
`${language}.quality.sarif`,
|
||||
);
|
||||
qualityAnalysisSummary = await runInterpretResults(
|
||||
if (
|
||||
config.analysisKinds.length > 1 &&
|
||||
configUtils.isCodeQualityEnabled(config)
|
||||
) {
|
||||
const qualityResult = await runInterpretResultsFor(
|
||||
analyses.CodeQuality,
|
||||
language,
|
||||
analyses.codeQualityQueries.map((i) =>
|
||||
resolveQuerySuiteAlias(language, i),
|
||||
),
|
||||
qualitySarifFile,
|
||||
config.debugMode,
|
||||
qualityCategory,
|
||||
);
|
||||
qualityAnalysisSummary = qualityResult.summary;
|
||||
}
|
||||
const endTimeInterpretResults = new Date();
|
||||
statusReport[`interpret_results_${language}_duration_ms`] =
|
||||
endTimeInterpretResults.getTime() - startTimeInterpretResults.getTime();
|
||||
logger.endGroup();
|
||||
logger.info(analysisSummary);
|
||||
|
||||
logger.info(analysisSummary);
|
||||
if (qualityAnalysisSummary) {
|
||||
logger.info(qualityAnalysisSummary);
|
||||
}
|
||||
|
||||
if (await features.getValue(Feature.QaTelemetryEnabled)) {
|
||||
// Note: QA adds the `code-quality` query suite to the `queries` input,
|
||||
// so this is fine since there is no `.quality.sarif`.
|
||||
const perQueryAlertCounts = getPerQueryAlertCounts(sarifFile);
|
||||
|
||||
const perQueryAlertCountEventReport: EventReport = {
|
||||
@@ -748,6 +769,37 @@ export async function runQueries(
|
||||
|
||||
return statusReport;
|
||||
|
||||
async function runInterpretResultsFor(
|
||||
analysis: analyses.AnalysisConfig,
|
||||
language: Language,
|
||||
queries: string[] | undefined,
|
||||
enableDebugLogging: boolean,
|
||||
): Promise<{ summary: string; sarifFile: string }> {
|
||||
logger.info(`Interpreting ${analysis.name} results for ${language}`);
|
||||
|
||||
// If this is a Code Quality analysis, correct the category to one
|
||||
// accepted by the Code Quality backend.
|
||||
let category = automationDetailsId;
|
||||
if (dbAnalysisConfig.kind === analyses.AnalysisKind.CodeQuality) {
|
||||
category = fixCodeQualityCategory(logger, automationDetailsId);
|
||||
}
|
||||
|
||||
const sarifFile = path.join(
|
||||
sarifFolder,
|
||||
addSarifExtension(analysis, language),
|
||||
);
|
||||
|
||||
const summary = await runInterpretResults(
|
||||
language,
|
||||
queries,
|
||||
sarifFile,
|
||||
enableDebugLogging,
|
||||
category,
|
||||
);
|
||||
|
||||
return { summary, sarifFile };
|
||||
}
|
||||
|
||||
async function runInterpretResults(
|
||||
language: Language,
|
||||
queries: string[] | undefined,
|
||||
|
||||
@@ -171,6 +171,63 @@ test("load empty config", async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
test("load code quality config", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const logger = getRunnerLogger(true);
|
||||
const languages = "actions";
|
||||
|
||||
const codeql = createStubCodeQL({
|
||||
async betterResolveLanguages() {
|
||||
return {
|
||||
extractors: {
|
||||
actions: [{ extractor_root: "" }],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const config = await configUtils.initConfig(
|
||||
createTestInitConfigInputs({
|
||||
analysisKindsInput: "code-quality",
|
||||
languagesInput: languages,
|
||||
repository: { owner: "github", repo: "example" },
|
||||
tempDir,
|
||||
codeql,
|
||||
logger,
|
||||
}),
|
||||
);
|
||||
|
||||
// And the config we expect it to result in
|
||||
const expectedConfig: configUtils.Config = {
|
||||
analysisKinds: [AnalysisKind.CodeQuality],
|
||||
languages: [KnownLanguage.actions],
|
||||
buildMode: undefined,
|
||||
originalUserInput: {},
|
||||
// This gets set because we only have `AnalysisKind.CodeQuality`
|
||||
computedConfig: {
|
||||
"disable-default-queries": true,
|
||||
queries: [{ uses: "code-quality" }],
|
||||
"query-filters": [],
|
||||
},
|
||||
tempDir,
|
||||
codeQLCmd: codeql.getPath(),
|
||||
gitHubVersion: githubVersion,
|
||||
dbLocation: path.resolve(tempDir, "codeql_databases"),
|
||||
debugMode: false,
|
||||
debugArtifactName: "",
|
||||
debugDatabaseName: "",
|
||||
trapCaches: {},
|
||||
trapCacheDownloadTime: 0,
|
||||
dependencyCachingEnabled: CachingKind.None,
|
||||
extraQueryExclusions: [],
|
||||
overlayDatabaseMode: OverlayDatabaseMode.None,
|
||||
useOverlayDatabaseCaching: false,
|
||||
};
|
||||
|
||||
t.deepEqual(config, expectedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
test("loading config saves config", async (t) => {
|
||||
return await withTmpDir(async (tempDir) => {
|
||||
const logger = getRunnerLogger(true);
|
||||
|
||||
+73
-1
@@ -6,7 +6,14 @@ import * as yaml from "js-yaml";
|
||||
import * as semver from "semver";
|
||||
|
||||
import { isAnalyzingPullRequest } from "./actions-util";
|
||||
import { AnalysisKind, parseAnalysisKinds } from "./analyses";
|
||||
import {
|
||||
AnalysisConfig,
|
||||
AnalysisKind,
|
||||
CodeQuality,
|
||||
codeQualityQueries,
|
||||
CodeScanning,
|
||||
parseAnalysisKinds,
|
||||
} from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { CachingKind, getCachingKind } from "./caching-utils";
|
||||
import { type CodeQL } from "./codeql";
|
||||
@@ -28,6 +35,7 @@ import {
|
||||
BuildMode,
|
||||
codeQlVersionAtLeast,
|
||||
cloneObject,
|
||||
isDefined,
|
||||
} from "./util";
|
||||
|
||||
// Property names from the user-supplied config file.
|
||||
@@ -1074,6 +1082,19 @@ function userConfigFromActionPath(tempDir: string): string {
|
||||
return path.resolve(tempDir, "user-config-from-action.yml");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given `UserConfig` contains any query customisations.
|
||||
*
|
||||
* @returns Returns `true` if the `UserConfig` customises which queries are run.
|
||||
*/
|
||||
function hasQueryCustomisation(userConfig: UserConfig): boolean {
|
||||
return (
|
||||
isDefined(userConfig["disable-default-queries"]) ||
|
||||
isDefined(userConfig.queries) ||
|
||||
isDefined(userConfig["query-filters"])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and return the config.
|
||||
*
|
||||
@@ -1110,6 +1131,25 @@ export async function initConfig(inputs: InitConfigInputs): Promise<Config> {
|
||||
|
||||
const config = await initActionState(inputs, userConfig);
|
||||
|
||||
// If Code Quality analysis is the only enabled analysis kind, then we will initialise
|
||||
// the database for Code Quality. That entails disabling the default queries and only
|
||||
// running quality queries. We do not currently support query customisations in that case.
|
||||
if (config.analysisKinds.length === 1 && isCodeQualityEnabled(config)) {
|
||||
// Warn if any query customisations are present in the computed configuration.
|
||||
if (hasQueryCustomisation(config.computedConfig)) {
|
||||
throw new ConfigurationError(
|
||||
"Query customizations are unsupported, because only `code-quality` analysis is enabled.",
|
||||
);
|
||||
}
|
||||
|
||||
const queries = codeQualityQueries.map((v) => ({ uses: v }));
|
||||
|
||||
// Set the query customisation options for Code Quality only analysis.
|
||||
config.computedConfig["disable-default-queries"] = true;
|
||||
config.computedConfig.queries = queries;
|
||||
config.computedConfig["query-filters"] = [];
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1509,9 +1549,41 @@ export function appendExtraQueryExclusions(
|
||||
return augmentedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if Code Scanning analysis is enabled, or `false` if not.
|
||||
*/
|
||||
export function isCodeScanningEnabled(config: Config): boolean {
|
||||
return config.analysisKinds.includes(AnalysisKind.CodeScanning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if Code Quality analysis is enabled, or `false` if not.
|
||||
*/
|
||||
export function isCodeQualityEnabled(config: Config): boolean {
|
||||
return config.analysisKinds.includes(AnalysisKind.CodeQuality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary analysis kind that the Action is initialised with. This is
|
||||
* always `AnalysisKind.CodeScanning` unless `AnalysisKind.CodeScanning` is not enabled.
|
||||
*
|
||||
* @returns Returns `AnalysisKind.CodeScanning` if `AnalysisKind.CodeScanning` is enabled;
|
||||
* otherwise `AnalysisKind.CodeQuality`.
|
||||
*/
|
||||
export function getPrimaryAnalysisKind(config: Config): AnalysisKind {
|
||||
return isCodeScanningEnabled(config)
|
||||
? AnalysisKind.CodeScanning
|
||||
: AnalysisKind.CodeQuality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary analysis configuration that the Action is initialised with. This is
|
||||
* always `CodeScanning` unless `CodeScanning` is not enabled.
|
||||
*
|
||||
* @returns Returns `CodeScanning` if `AnalysisKind.CodeScanning` is enabled; otherwise `CodeQuality`.
|
||||
*/
|
||||
export function getPrimaryAnalysisConfig(config: Config): AnalysisConfig {
|
||||
return getPrimaryAnalysisKind(config) === AnalysisKind.CodeScanning
|
||||
? CodeScanning
|
||||
: CodeQuality;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as core from "@actions/core";
|
||||
import * as github from "@actions/github";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { CodeScanning } from "./analyses";
|
||||
import { getApiClient } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
import { Config } from "./config-utils";
|
||||
@@ -104,7 +105,7 @@ async function maybeUploadFailedSarif(
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
uploadLib.CodeScanningTarget,
|
||||
CodeScanning,
|
||||
);
|
||||
await uploadLib.waitForProcessing(
|
||||
repositoryNwo,
|
||||
|
||||
+1
-10
@@ -2,7 +2,7 @@ import * as core from "@actions/core";
|
||||
|
||||
import { KnownLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { ConfigurationError } from "./util";
|
||||
import { ConfigurationError, isDefined } from "./util";
|
||||
|
||||
export type Credential = {
|
||||
type: string;
|
||||
@@ -65,15 +65,6 @@ const LANGUAGE_TO_REGISTRY_TYPE: Partial<Record<KnownLanguage, string[]>> = {
|
||||
go: ["goproxy_server", "git_source"],
|
||||
} 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.
|
||||
|
||||
+185
-45
@@ -3,6 +3,7 @@ import * as path from "path";
|
||||
|
||||
import test from "ava";
|
||||
|
||||
import { CodeQuality, CodeScanning } from "./analyses";
|
||||
import { getRunnerLogger, Logger } from "./logging";
|
||||
import { setupTests } from "./testing-utils";
|
||||
import * as uploadLib from "./upload-lib";
|
||||
@@ -128,7 +129,7 @@ test("finding SARIF files", async (t) => {
|
||||
|
||||
const sarifFiles = uploadLib.findSarifFilesInDir(
|
||||
tmpDir,
|
||||
uploadLib.CodeScanningTarget.sarifPredicate,
|
||||
CodeScanning.sarifPredicate,
|
||||
);
|
||||
|
||||
t.deepEqual(sarifFiles, [
|
||||
@@ -140,7 +141,7 @@ test("finding SARIF files", async (t) => {
|
||||
|
||||
const qualitySarifFiles = uploadLib.findSarifFilesInDir(
|
||||
tmpDir,
|
||||
uploadLib.CodeQualityTarget.sarifPredicate,
|
||||
CodeQuality.sarifPredicate,
|
||||
);
|
||||
|
||||
t.deepEqual(qualitySarifFiles, [
|
||||
@@ -211,109 +212,237 @@ test("populateRunAutomationDetails", (t) => {
|
||||
});
|
||||
|
||||
test("validateUniqueCategory when empty", (t) => {
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif()));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif()));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUniqueCategory for automation details id", (t) => {
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif("abc")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("AbC")));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("AbC"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif("def")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("def")));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
// Our category sanitization is not perfect. Here are some examples
|
||||
// of where we see false clashes
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc/def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc/def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc@def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc@def")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc_def")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc def")));
|
||||
|
||||
// this one is fine
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc_ def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_ def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUniqueCategory for tool name", (t) => {
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "AbC")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "AbC"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
// Our category sanitization is not perfect. Here are some examples
|
||||
// of where we see false clashes
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc/def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc/def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc@def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc@def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc_def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc_def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif(undefined, "abc def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(undefined, "abc def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
// this one is fine
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc_ def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_ def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUniqueCategory for automation details id and tool name", (t) => {
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc", "abc")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc", "abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc", "abc")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc", "abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc_", "def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_", "def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc_", "def")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_", "def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("ghi", "_jkl")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("ghi", "_jkl"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("ghi", "_jkl")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("ghi", "_jkl"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
// Our category sanitization is not perfect. Here are some examples
|
||||
// of where we see false clashes
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif("abc")));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc", "_")));
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("abc", "def__")),
|
||||
);
|
||||
t.throws(() => uploadLib.validateUniqueCategory(createMockSarif("abc_def")));
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("mno_", "pqr")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(createMockSarif("mno", "_pqr")),
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc", "_"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc", "def__"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("abc_def"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("mno_", "pqr"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif("mno", "_pqr"),
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -323,19 +452,30 @@ test("validateUniqueCategory for multiple runs", (t) => {
|
||||
|
||||
// duplicate categories are allowed within the same sarif file
|
||||
const multiSarif = { runs: [sarif1.runs[0], sarif1.runs[0], sarif2.runs[0]] };
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(multiSarif));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(multiSarif, CodeScanning.sentinelPrefix),
|
||||
);
|
||||
|
||||
// should throw if there are duplicate categories in separate validations
|
||||
t.throws(() => uploadLib.validateUniqueCategory(sarif1));
|
||||
t.throws(() => uploadLib.validateUniqueCategory(sarif2));
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(sarif1, CodeScanning.sentinelPrefix),
|
||||
);
|
||||
t.throws(() =>
|
||||
uploadLib.validateUniqueCategory(sarif2, CodeScanning.sentinelPrefix),
|
||||
);
|
||||
});
|
||||
|
||||
test("validateUniqueCategory with different prefixes", (t) => {
|
||||
t.notThrows(() => uploadLib.validateUniqueCategory(createMockSarif()));
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(),
|
||||
uploadLib.CodeQualityTarget.sentinelPrefix,
|
||||
CodeScanning.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
t.notThrows(() =>
|
||||
uploadLib.validateUniqueCategory(
|
||||
createMockSarif(),
|
||||
CodeQuality.sentinelPrefix,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
+5
-35
@@ -8,6 +8,7 @@ import { OctokitResponse } from "@octokit/types";
|
||||
import * as jsonschema from "jsonschema";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import * as api from "./api-client";
|
||||
import { getGitHubVersion, wrapApiConfigurationError } from "./api-client";
|
||||
import { CodeQL, getCodeQL } from "./codeql";
|
||||
@@ -345,19 +346,13 @@ 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,
|
||||
target: analyses.SARIF_UPLOAD_ENDPOINT,
|
||||
): Promise<string> {
|
||||
logger.info("Uploading results");
|
||||
|
||||
@@ -616,31 +611,6 @@ 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.
|
||||
@@ -651,7 +621,7 @@ export async function uploadFiles(
|
||||
category: string | undefined,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
uploadTarget: UploadTarget,
|
||||
uploadTarget: analyses.AnalysisConfig,
|
||||
): Promise<UploadResult> {
|
||||
const sarifPaths = getSarifFilePaths(
|
||||
inputSarifPath,
|
||||
@@ -677,7 +647,7 @@ export async function uploadSpecifiedFiles(
|
||||
category: string | undefined,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
uploadTarget: UploadTarget = CodeScanningTarget,
|
||||
uploadTarget: analyses.AnalysisConfig,
|
||||
): Promise<UploadResult> {
|
||||
logger.startGroup(`Uploading ${uploadTarget.name} results`);
|
||||
logger.info(`Processing sarif files: ${JSON.stringify(sarifPaths)}`);
|
||||
@@ -943,7 +913,7 @@ function handleProcessingResultForUnsuccessfulExecution(
|
||||
|
||||
export function validateUniqueCategory(
|
||||
sarif: SarifFile,
|
||||
sentinelPrefix: string = CodeScanningTarget.sentinelPrefix,
|
||||
sentinelPrefix: string,
|
||||
): void {
|
||||
// duplicate categories are allowed in the same sarif file
|
||||
// but not across multiple sarif files
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as core from "@actions/core";
|
||||
|
||||
import * as actionsUtil from "./actions-util";
|
||||
import { getActionVersion, getTemporaryDirectory } from "./actions-util";
|
||||
import * as analyses from "./analyses";
|
||||
import { getGitHubVersion } from "./api-client";
|
||||
import { Features } from "./feature-flags";
|
||||
import { Logger, getActionsLogger } from "./logging";
|
||||
@@ -95,7 +96,7 @@ async function run() {
|
||||
category,
|
||||
features,
|
||||
logger,
|
||||
upload_lib.CodeScanningTarget,
|
||||
analyses.CodeScanning,
|
||||
);
|
||||
core.setOutput("sarif-id", uploadResult.sarifID);
|
||||
|
||||
@@ -105,7 +106,7 @@ async function run() {
|
||||
if (fs.lstatSync(sarifPath).isDirectory()) {
|
||||
const qualitySarifFiles = upload_lib.findSarifFilesInDir(
|
||||
sarifPath,
|
||||
upload_lib.CodeQualityTarget.sarifPredicate,
|
||||
analyses.CodeQuality.sarifPredicate,
|
||||
);
|
||||
|
||||
if (qualitySarifFiles.length !== 0) {
|
||||
@@ -115,7 +116,7 @@ async function run() {
|
||||
actionsUtil.fixCodeQualityCategory(logger, category),
|
||||
features,
|
||||
logger,
|
||||
upload_lib.CodeQualityTarget,
|
||||
analyses.CodeQuality,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,3 +1278,12 @@ export async function asyncSome<T>(
|
||||
const results = await Promise.all(array.map(predicate));
|
||||
return results.some((result) => result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
export function isDefined<T>(value: T | null | undefined): value is T {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user