From fe0376ed1f08e19edee56b6f1dc6fc3cf2eb6b9f Mon Sep 17 00:00:00 2001 From: "Michael B. Gale" Date: Mon, 29 Sep 2025 11:19:30 +0100 Subject: [PATCH] Add `getGroupedSarifFilePaths` with tests --- lib/analyze-action.js | 4 +- lib/init-action-post.js | 4 +- lib/upload-lib.js | 80 ++++++++++++++++++++++++++++----- lib/upload-sarif-action.js | 4 +- src/analyses.ts | 6 +++ src/upload-lib.test.ts | 92 +++++++++++++++++++++++++++++++++----- src/upload-lib.ts | 62 +++++++++++++++++++++++++ 7 files changed, 225 insertions(+), 27 deletions(-) diff --git a/lib/analyze-action.js b/lib/analyze-action.js index 38336e09e..9cee6b6c7 100644 --- a/lib/analyze-action.js +++ b/lib/analyze-action.js @@ -28149,11 +28149,11 @@ var require_out = __commonJS({ async.read(path20, getSettings(optionsOrSettingsOrCallback), callback); } exports2.stat = stat; - function statSync3(path20, optionsOrSettings) { + function statSync4(path20, optionsOrSettings) { const settings = getSettings(optionsOrSettings); return sync.read(path20, settings); } - exports2.statSync = statSync3; + exports2.statSync = statSync4; function getSettings(settingsOrOptions = {}) { if (settingsOrOptions instanceof settings_1.default) { return settingsOrOptions; diff --git a/lib/init-action-post.js b/lib/init-action-post.js index d56b9982a..bc8c22d10 100644 --- a/lib/init-action-post.js +++ b/lib/init-action-post.js @@ -28149,11 +28149,11 @@ var require_out = __commonJS({ async.read(path19, getSettings(optionsOrSettingsOrCallback), callback); } exports2.stat = stat; - function statSync2(path19, optionsOrSettings) { + function statSync3(path19, optionsOrSettings) { const settings = getSettings(optionsOrSettings); return sync.read(path19, settings); } - exports2.statSync = statSync2; + exports2.statSync = statSync3; function getSettings(settingsOrOptions = {}) { if (settingsOrOptions instanceof settings_1.default) { return settingsOrOptions; diff --git a/lib/upload-lib.js b/lib/upload-lib.js index 27ad93408..148eb55eb 100644 --- a/lib/upload-lib.js +++ b/lib/upload-lib.js @@ -29446,11 +29446,11 @@ var require_out = __commonJS({ async.read(path15, getSettings(optionsOrSettingsOrCallback), callback); } exports2.stat = stat; - function statSync2(path15, optionsOrSettings) { + function statSync3(path15, optionsOrSettings) { const settings = getSettings(optionsOrSettings); return sync.read(path15, settings); } - exports2.statSync = statSync2; + exports2.statSync = statSync3; function getSettings(settingsOrOptions = {}) { if (settingsOrOptions instanceof settings_1.default) { return settingsOrOptions; @@ -84821,6 +84821,7 @@ __export(upload_lib_exports, { InvalidSarifUploadError: () => InvalidSarifUploadError, buildPayload: () => buildPayload, findSarifFilesInDir: () => findSarifFilesInDir, + getGroupedSarifFilePaths: () => getGroupedSarifFilePaths, getSarifFilePaths: () => getSarifFilePaths, populateRunAutomationDetails: () => populateRunAutomationDetails, readSarifFile: () => readSarifFile, @@ -88530,6 +88531,31 @@ async function runTool(cmd, args = [], opts = {}) { return stdout; } +// src/analyses.ts +var AnalysisKind = /* @__PURE__ */ ((AnalysisKind2) => { + AnalysisKind2["CodeScanning"] = "code-scanning"; + AnalysisKind2["CodeQuality"] = "code-quality"; + return AnalysisKind2; +})(AnalysisKind || {}); +var supportedAnalysisKinds = new Set(Object.values(AnalysisKind)); +var CodeScanning = { + kind: "code-scanning" /* CodeScanning */, + name: "code scanning", + target: "PUT /repos/:owner/:repo/code-scanning/analysis" /* CODE_SCANNING */, + sarifExtension: ".sarif", + sarifPredicate: (name) => name.endsWith(CodeScanning.sarifExtension) && !CodeQuality.sarifPredicate(name), + sentinelPrefix: "CODEQL_UPLOAD_SARIF_" +}; +var CodeQuality = { + kind: "code-quality" /* CodeQuality */, + name: "code quality", + target: "PUT /repos/:owner/:repo/code-quality/analysis" /* CODE_QUALITY */, + sarifExtension: ".quality.sarif", + sarifPredicate: (name) => name.endsWith(CodeQuality.sarifExtension), + sentinelPrefix: "CODEQL_UPLOAD_QUALITY_SARIF_" +}; +var SarifScanOrder = [CodeQuality, CodeScanning]; + // src/api-client.ts var core5 = __toESM(require_core()); var githubUtils = __toESM(require_utils4()); @@ -88921,14 +88947,6 @@ function wrapCliConfigurationError(cliError) { var fs7 = __toESM(require("fs")); var path9 = __toESM(require("path")); -// src/analyses.ts -var AnalysisKind = /* @__PURE__ */ ((AnalysisKind2) => { - AnalysisKind2["CodeScanning"] = "code-scanning"; - AnalysisKind2["CodeQuality"] = "code-quality"; - return AnalysisKind2; -})(AnalysisKind || {}); -var supportedAnalysisKinds = new Set(Object.values(AnalysisKind)); - // src/caching-utils.ts var core6 = __toESM(require_core()); @@ -92391,6 +92409,47 @@ function getSarifFilePaths(sarifPath, isSarif) { } return sarifFiles; } +async function getGroupedSarifFilePaths(logger, sarifPath) { + const stats = fs13.statSync(sarifPath, { throwIfNoEntry: false }); + if (stats === void 0) { + throw new ConfigurationError(`Path does not exist: ${sarifPath}`); + } + const results = {}; + if (stats.isDirectory()) { + let sarifFiles = findSarifFilesInDir( + sarifPath, + (name) => name.endsWith(".sarif") + ); + logger.debug( + `Found the following .sarif files in ${sarifPath}: ${sarifFiles.join(", ")}` + ); + for (const analysisConfig of SarifScanOrder) { + const files = sarifFiles.filter(analysisConfig.sarifPredicate); + if (files.length > 0) { + logger.debug( + `The following SARIF files are for ${analysisConfig.name}: ${files.join(", ")}` + ); + sarifFiles = sarifFiles.filter( + (name) => !analysisConfig.sarifPredicate(name) + ); + results[analysisConfig.kind] = files; + } else { + logger.debug(`Found no SARIF files for ${analysisConfig.name}`); + } + } + } else { + for (const analysisConfig of SarifScanOrder) { + if (analysisConfig.kind === "code-scanning" /* CodeScanning */ || analysisConfig.sarifPredicate(sarifPath)) { + logger.debug( + `Using '${sarifPath}' as a SARIF file for ${analysisConfig.name}.` + ); + results[analysisConfig.kind] = [sarifPath]; + break; + } + } + } + return results; +} function countResultsInSarif(sarif) { let numResults = 0; const parsedSarif = JSON.parse(sarif); @@ -92750,6 +92809,7 @@ function filterAlertsByDiffRange(logger, sarif) { InvalidSarifUploadError, buildPayload, findSarifFilesInDir, + getGroupedSarifFilePaths, getSarifFilePaths, populateRunAutomationDetails, readSarifFile, diff --git a/lib/upload-sarif-action.js b/lib/upload-sarif-action.js index 317e6290b..9e46c71e7 100644 --- a/lib/upload-sarif-action.js +++ b/lib/upload-sarif-action.js @@ -28149,11 +28149,11 @@ var require_out = __commonJS({ async.read(path16, getSettings(optionsOrSettingsOrCallback), callback); } exports2.stat = stat; - function statSync2(path16, optionsOrSettings) { + function statSync3(path16, optionsOrSettings) { const settings = getSettings(optionsOrSettings); return sync.read(path16, settings); } - exports2.statSync = statSync2; + exports2.statSync = statSync3; function getSettings(settingsOrOptions = {}) { if (settingsOrOptions instanceof settings_1.default) { return settingsOrOptions; diff --git a/src/analyses.ts b/src/analyses.ts index 8d8cd1d61..53f17a70a 100644 --- a/src/analyses.ts +++ b/src/analyses.ts @@ -86,3 +86,9 @@ export const CodeQuality: AnalysisConfig = { sarifPredicate: (name) => name.endsWith(CodeQuality.sarifExtension), sentinelPrefix: "CODEQL_UPLOAD_QUALITY_SARIF_", }; + +// Since we have overlapping extensions (i.e. ".sarif" includes ".quality.sarif"), +// we want to scan a folder containing SARIF files in an order that finds the more +// specific extensions first. This constant defines an array in the order of analyis +// configurations with more specific extensions to less specific extensions. +export const SarifScanOrder = [CodeQuality, CodeScanning]; diff --git a/src/upload-lib.test.ts b/src/upload-lib.test.ts index bfa5e9844..a1fdf218e 100644 --- a/src/upload-lib.test.ts +++ b/src/upload-lib.test.ts @@ -3,7 +3,7 @@ import * as path from "path"; import test from "ava"; -import { CodeQuality, CodeScanning } from "./analyses"; +import { AnalysisKind, CodeQuality, CodeScanning } from "./analyses"; import { getRunnerLogger, Logger } from "./logging"; import { setupTests } from "./testing-utils"; import * as uploadLib from "./upload-lib"; @@ -127,27 +127,97 @@ test("finding SARIF files", async (t) => { fs.writeFileSync(path.join(tmpDir, "a.quality.sarif"), ""); fs.writeFileSync(path.join(tmpDir, "dir1", "b.quality.sarif"), ""); + const expectedSarifFiles = [ + path.join(tmpDir, "a.sarif"), + path.join(tmpDir, "b.sarif"), + path.join(tmpDir, "dir1", "d.sarif"), + path.join(tmpDir, "dir1", "dir2", "e.sarif"), + ]; const sarifFiles = uploadLib.findSarifFilesInDir( tmpDir, CodeScanning.sarifPredicate, ); - t.deepEqual(sarifFiles, [ - path.join(tmpDir, "a.sarif"), - path.join(tmpDir, "b.sarif"), - path.join(tmpDir, "dir1", "d.sarif"), - path.join(tmpDir, "dir1", "dir2", "e.sarif"), - ]); + t.deepEqual(sarifFiles, expectedSarifFiles); + const expectedQualitySarifFiles = [ + path.join(tmpDir, "a.quality.sarif"), + path.join(tmpDir, "dir1", "b.quality.sarif"), + ]; const qualitySarifFiles = uploadLib.findSarifFilesInDir( tmpDir, CodeQuality.sarifPredicate, ); - t.deepEqual(qualitySarifFiles, [ - path.join(tmpDir, "a.quality.sarif"), - path.join(tmpDir, "dir1", "b.quality.sarif"), - ]); + t.deepEqual(qualitySarifFiles, expectedQualitySarifFiles); + + const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths( + getRunnerLogger(true), + tmpDir, + ); + + t.not(groupedSarifFiles, undefined); + t.not(groupedSarifFiles[AnalysisKind.CodeScanning], undefined); + t.not(groupedSarifFiles[AnalysisKind.CodeQuality], undefined); + t.deepEqual( + groupedSarifFiles[AnalysisKind.CodeScanning], + expectedSarifFiles, + ); + t.deepEqual( + groupedSarifFiles[AnalysisKind.CodeQuality], + expectedQualitySarifFiles, + ); + }); +}); + +test("getGroupedSarifFilePaths - Code Quality file", async (t) => { + await withTmpDir(async (tmpDir) => { + const sarifPath = path.join(tmpDir, "a.quality.sarif"); + fs.writeFileSync(sarifPath, ""); + + const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths( + getRunnerLogger(true), + sarifPath, + ); + + t.not(groupedSarifFiles, undefined); + t.is(groupedSarifFiles[AnalysisKind.CodeScanning], undefined); + t.not(groupedSarifFiles[AnalysisKind.CodeQuality], undefined); + t.deepEqual(groupedSarifFiles[AnalysisKind.CodeQuality], [sarifPath]); + }); +}); + +test("getGroupedSarifFilePaths - Code Scanning file", async (t) => { + await withTmpDir(async (tmpDir) => { + const sarifPath = path.join(tmpDir, "a.sarif"); + fs.writeFileSync(sarifPath, ""); + + const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths( + getRunnerLogger(true), + sarifPath, + ); + + t.not(groupedSarifFiles, undefined); + t.not(groupedSarifFiles[AnalysisKind.CodeScanning], undefined); + t.is(groupedSarifFiles[AnalysisKind.CodeQuality], undefined); + t.deepEqual(groupedSarifFiles[AnalysisKind.CodeScanning], [sarifPath]); + }); +}); + +test("getGroupedSarifFilePaths - Other file", async (t) => { + await withTmpDir(async (tmpDir) => { + const sarifPath = path.join(tmpDir, "a.json"); + fs.writeFileSync(sarifPath, ""); + + const groupedSarifFiles = await uploadLib.getGroupedSarifFilePaths( + getRunnerLogger(true), + sarifPath, + ); + + t.not(groupedSarifFiles, undefined); + t.not(groupedSarifFiles[AnalysisKind.CodeScanning], undefined); + t.is(groupedSarifFiles[AnalysisKind.CodeQuality], undefined); + t.deepEqual(groupedSarifFiles[AnalysisKind.CodeScanning], [sarifPath]); }); }); diff --git a/src/upload-lib.ts b/src/upload-lib.ts index 650e7a803..f4b171e9e 100644 --- a/src/upload-lib.ts +++ b/src/upload-lib.ts @@ -459,6 +459,68 @@ export function getSarifFilePaths( return sarifFiles; } +/** + * Finds SARIF files in `sarifPath`, and groups them by analysis kind, following `SarifScanOrder`. + * + * @param logger The logger to use. + * @param sarifPath The path of a file or directory to recursively scan for SARIF files. + * @returns The `.sarif` files found in `sarifPath`, grouped by analysis kind. + */ +export async function getGroupedSarifFilePaths( + logger: Logger, + sarifPath: string, +): Promise>> { + const stats = fs.statSync(sarifPath, { throwIfNoEntry: false }); + + if (stats === undefined) { + // This is always a configuration error, even for first-party runs. + throw new ConfigurationError(`Path does not exist: ${sarifPath}`); + } + + const results = {}; + + if (stats.isDirectory()) { + let sarifFiles = findSarifFilesInDir(sarifPath, (name) => + name.endsWith(".sarif"), + ); + logger.debug( + `Found the following .sarif files in ${sarifPath}: ${sarifFiles.join(", ")}`, + ); + + for (const analysisConfig of analyses.SarifScanOrder) { + const files = sarifFiles.filter(analysisConfig.sarifPredicate); + if (files.length > 0) { + logger.debug( + `The following SARIF files are for ${analysisConfig.name}: ${files.join(", ")}`, + ); + // Looping through the array a second time is not efficient, but more readable. + // Change this to one loop for both calls to `filter` if this becomes a bottleneck. + sarifFiles = sarifFiles.filter( + (name) => !analysisConfig.sarifPredicate(name), + ); + results[analysisConfig.kind] = files; + } else { + logger.debug(`Found no SARIF files for ${analysisConfig.name}`); + } + } + } else { + for (const analysisConfig of analyses.SarifScanOrder) { + if ( + analysisConfig.kind === analyses.AnalysisKind.CodeScanning || + analysisConfig.sarifPredicate(sarifPath) + ) { + logger.debug( + `Using '${sarifPath}' as a SARIF file for ${analysisConfig.name}.`, + ); + results[analysisConfig.kind] = [sarifPath]; + break; + } + } + } + + return results; +} + // Counts the number of results in the given SARIF file function countResultsInSarif(sarif: string): number { let numResults = 0;