diff --git a/lib/init-action.js b/lib/init-action.js index f1634c88b4..cd372ff404 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -106110,9 +106110,6 @@ function initFeatures(gitHubVersion, repositoryNwo, tempDir, logger) { } // src/diff-informed-analysis-utils.ts -async function shouldPerformDiffInformedAnalysis(codeql, features, logger) { - return await getDiffInformedAnalysisBranches(codeql, features, logger) !== void 0; -} async function getDiffInformedAnalysisBranches(codeql, features, logger) { if (!await features.getValue("diff_informed_queries" /* DiffInformedQueries */, codeql)) { return void 0; @@ -106129,6 +106126,37 @@ async function getDiffInformedAnalysisBranches(codeql, features, logger) { } return branches; } +async function prepareDiffInformedAnalysis(codeql, features, logger) { + try { + const branches = await getDiffInformedAnalysisBranches( + codeql, + features, + logger + ); + if (!branches) { + return { shouldRun: false, isAvailable: false }; + } + const isAvailable = await withGroupAsync( + "Computing PR diff ranges", + async () => { + try { + return await computeAndPersistDiffRanges(branches, logger); + } catch (e) { + logger.warning( + `Failed to compute diff-informed analysis ranges: ${getErrorMessage(e)}` + ); + return false; + } + } + ); + return { shouldRun: true, isAvailable }; + } catch (e) { + logger.warning( + `Failed to determine diff-informed analysis availability: ${getErrorMessage(e)}` + ); + return { shouldRun: true, isAvailable: false }; + } +} function writeDiffRangesJsonFile(logger, ranges) { const jsonContents = JSON.stringify(ranges, null, 2); const jsonFilePath = getDiffRangesJsonFilePath(); @@ -106159,6 +106187,18 @@ async function getPullRequestEditedDiffRanges(branches, logger) { } return results; } +async function computeAndPersistDiffRanges(branches, logger) { + const ranges = await getPullRequestEditedDiffRanges(branches, logger); + if (ranges === void 0) { + return false; + } + writeDiffRangesJsonFile(logger, ranges); + const distinctFiles = new Set(ranges.map((r) => r.path)).size; + logger.info( + `Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).` + ); + return true; +} async function getFileDiffsWithBasehead(branches, logger) { const repositoryNwo = getRepositoryNwoFromEnv( "CODE_SCANNING_REPOSITORY", @@ -106974,6 +107014,19 @@ function userConfigFromActionPath(tempDir) { function hasQueryCustomisation(userConfig) { return isDefined2(userConfig["disable-default-queries"]) || isDefined2(userConfig.queries) || isDefined2(userConfig["query-filters"]); } +function applyIncrementalAnalysisSettings(config, diffInformedAnalysis, logger) { + if (config.overlayDatabaseMode === "overlay" /* Overlay */ && diffInformedAnalysis.shouldRun && !diffInformedAnalysis.isAvailable) { + logger.warning( + `Diff-informed analysis is not available for this pull request. Reverting overlay database mode to ${"none" /* None */}.` + ); + config.overlayDatabaseMode = "none" /* None */; + } + if (config.overlayDatabaseMode === "overlay" /* Overlay */ || diffInformedAnalysis.isAvailable) { + config.extraQueryExclusions.push({ + exclude: { tags: "exclude-from-incremental" } + }); + } +} async function initConfig(features, inputs) { const { logger, tempDir } = inputs; if (inputs.configInput) { @@ -107083,15 +107136,12 @@ async function initConfig(features, inputs) { overlayDisabledReason ); } - if (config.overlayDatabaseMode === "overlay" /* Overlay */ || await shouldPerformDiffInformedAnalysis( + const diffInformedAnalysis = await prepareDiffInformedAnalysis( inputs.codeql, inputs.features, logger - )) { - config.extraQueryExclusions.push({ - exclude: { tags: "exclude-from-incremental" } - }); - } + ); + applyIncrementalAnalysisSettings(config, diffInformedAnalysis, logger); if (await isTrapCachingEnabled(features, config.overlayDatabaseMode)) { const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime( inputs.codeql, @@ -110359,7 +110409,6 @@ async function run(startedAt) { logFileCoverageOnPrsDeprecationWarning(logger); } await checkInstallPython311(config.languages, codeql); - await computeAndPersistDiffRanges(codeql, features, logger); } catch (unwrappedError) { const error3 = wrapError(unwrappedError); core15.setFailed(error3.message); @@ -110627,33 +110676,6 @@ async function loadRepositoryProperties(repositoryNwo, logger) { return new Failure(error3); } } -async function computeAndPersistDiffRanges(codeql, features, logger) { - await withGroupAsync("Computing PR diff ranges", async () => { - try { - const branches = await getDiffInformedAnalysisBranches( - codeql, - features, - logger - ); - if (!branches) { - return; - } - const ranges = await getPullRequestEditedDiffRanges(branches, logger); - if (ranges === void 0) { - return; - } - writeDiffRangesJsonFile(logger, ranges); - const distinctFiles = new Set(ranges.map((r) => r.path)).size; - logger.info( - `Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).` - ); - } catch (e) { - logger.warning( - `Failed to compute and persist PR diff ranges: ${getErrorMessage(e)}` - ); - } - }); -} async function recordZstdAvailability(config, zstdAvailability) { addNoLanguageDiagnostic( config, diff --git a/src/config-utils.test.ts b/src/config-utils.test.ts index e6485528f2..16db0d3fd9 100644 --- a/src/config-utils.test.ts +++ b/src/config-utils.test.ts @@ -2200,3 +2200,80 @@ test.serial( }); }, ); + +test.serial( + "applyIncrementalAnalysisSettings: no-op when mode is not Overlay and diff-informed is unavailable", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.None; + const logger = getRunnerLogger(true); + + configUtils.applyIncrementalAnalysisSettings( + config, + { shouldRun: false, isAvailable: false }, + logger, + ); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None); + t.deepEqual(config.extraQueryExclusions, []); + }, +); + +test.serial( + "applyIncrementalAnalysisSettings: keeps overlay mode and adds exclusions when diff-informed analysis is disabled", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.Overlay; + const logger = getRunnerLogger(true); + + configUtils.applyIncrementalAnalysisSettings( + config, + { shouldRun: false, isAvailable: false }, + logger, + ); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.Overlay); + t.deepEqual(config.extraQueryExclusions, [ + { exclude: { tags: "exclude-from-incremental" } }, + ]); + }, +); + +test.serial( + "applyIncrementalAnalysisSettings: reverts to None without exclusions when diff-informed analysis is unavailable", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = + OverlayDatabaseMode.Overlay as OverlayDatabaseMode; + const logger = getRunnerLogger(true); + + configUtils.applyIncrementalAnalysisSettings( + config, + { shouldRun: true, isAvailable: false }, + logger, + ); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None); + t.deepEqual(config.extraQueryExclusions, []); + }, +); + +test.serial( + "applyIncrementalAnalysisSettings: adds exclusions for diff-informed-only runs", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.None; + const logger = getRunnerLogger(true); + + configUtils.applyIncrementalAnalysisSettings( + config, + { shouldRun: true, isAvailable: true }, + logger, + ); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None); + t.deepEqual(config.extraQueryExclusions, [ + { exclude: { tags: "exclude-from-incremental" } }, + ]); + }, +); diff --git a/src/config-utils.ts b/src/config-utils.ts index e8d2ef8e1d..24c78f5dab 100644 --- a/src/config-utils.ts +++ b/src/config-utils.ts @@ -31,7 +31,10 @@ import { addNoLanguageDiagnostic, makeTelemetryDiagnostic, } from "./diagnostics"; -import { shouldPerformDiffInformedAnalysis } from "./diff-informed-analysis-utils"; +import { + type DiffInformedAnalysisPreparation, + prepareDiffInformedAnalysis, +} from "./diff-informed-analysis-utils"; import { EnvVar } from "./environment"; import * as errorMessages from "./error-messages"; import { Feature, FeatureEnablement } from "./feature-flags"; @@ -1076,6 +1079,42 @@ function hasQueryCustomisation(userConfig: UserConfig): boolean { ); } +/** + * Finalize the incremental-analysis configuration for this run. + * + * If overlay mode was selected for a PR but diff-informed analysis should have + * run and could not be prepared, fall back to a full non-overlay analysis. + * Query exclusions for incremental-only queries are applied only when the final + * configuration still uses overlay analysis or diff-informed analysis is + * actually available. + */ +export function applyIncrementalAnalysisSettings( + config: Config, + diffInformedAnalysis: DiffInformedAnalysisPreparation, + logger: Logger, +): void { + if ( + config.overlayDatabaseMode === OverlayDatabaseMode.Overlay && + diffInformedAnalysis.shouldRun && + !diffInformedAnalysis.isAvailable + ) { + logger.warning( + "Diff-informed analysis is not available for this pull request. " + + `Reverting overlay database mode to ${OverlayDatabaseMode.None}.`, + ); + config.overlayDatabaseMode = OverlayDatabaseMode.None; + } + + if ( + config.overlayDatabaseMode === OverlayDatabaseMode.Overlay || + diffInformedAnalysis.isAvailable + ) { + config.extraQueryExclusions.push({ + exclude: { tags: "exclude-from-incremental" }, + }); + } +} + /** * Load and return the config. * @@ -1230,18 +1269,13 @@ export async function initConfig( ); } - if ( - config.overlayDatabaseMode === OverlayDatabaseMode.Overlay || - (await shouldPerformDiffInformedAnalysis( - inputs.codeql, - inputs.features, - logger, - )) - ) { - config.extraQueryExclusions.push({ - exclude: { tags: "exclude-from-incremental" }, - }); - } + const diffInformedAnalysis = await prepareDiffInformedAnalysis( + inputs.codeql, + inputs.features, + logger, + ); + + applyIncrementalAnalysisSettings(config, diffInformedAnalysis, logger); if (await isTrapCachingEnabled(features, config.overlayDatabaseMode)) { const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime( diff --git a/src/diff-informed-analysis-utils.ts b/src/diff-informed-analysis-utils.ts index 1c98d4caca..503b9ddb07 100644 --- a/src/diff-informed-analysis-utils.ts +++ b/src/diff-informed-analysis-utils.ts @@ -5,9 +5,9 @@ import type { PullRequestBranches } from "./actions-util"; import { getApiClient, getGitHubVersion } from "./api-client"; import type { CodeQL } from "./codeql"; import { Feature, FeatureEnablement } from "./feature-flags"; -import { Logger } from "./logging"; +import { Logger, withGroupAsync } from "./logging"; import { getRepositoryNwoFromEnv } from "./repository"; -import { GitHubVariant, satisfiesGHESVersion } from "./util"; +import { getErrorMessage, GitHubVariant, satisfiesGHESVersion } from "./util"; /** * This interface is an abbreviated version of the file diff object returned by @@ -69,6 +69,61 @@ export async function getDiffInformedAnalysisBranches( return branches; } +export interface DiffInformedAnalysisPreparation { + /** + * Whether diff-informed analysis applies to this workflow run. + */ + shouldRun: boolean; + /** + * Whether the diff ranges were successfully prepared and can be used. + */ + isAvailable: boolean; +} + +/** + * Prepares the diff ranges needed for diff-informed analysis for the current + * run. + * + * @returns Whether diff-informed analysis applies to this run, and whether it + * was successfully prepared for use. + */ +export async function prepareDiffInformedAnalysis( + codeql: CodeQL, + features: FeatureEnablement, + logger: Logger, +): Promise { + try { + const branches = await getDiffInformedAnalysisBranches( + codeql, + features, + logger, + ); + if (!branches) { + return { shouldRun: false, isAvailable: false }; + } + + const isAvailable = await withGroupAsync( + "Computing PR diff ranges", + async () => { + try { + return await computeAndPersistDiffRanges(branches, logger); + } catch (e) { + logger.warning( + `Failed to compute diff-informed analysis ranges: ${getErrorMessage(e)}`, + ); + return false; + } + }, + ); + return { shouldRun: true, isAvailable }; + } catch (e) { + logger.warning( + `Failed to determine diff-informed analysis availability: ${getErrorMessage(e)}`, + ); + return { shouldRun: true, isAvailable: false }; + } +} + export interface DiffThunkRange { /** Relative path from the repository root, using forward slashes as separators. */ path: string; @@ -151,6 +206,33 @@ export async function getPullRequestEditedDiffRanges( return results; } +/** + * Compute and persist the diff ranges for a pull request. This fetches the + * diff from the GitHub API and writes it to the diff ranges JSON file so that + * CodeQL can use it for diff-informed analysis. + * + * @param branches The base and head branches of the pull request, as returned + * by `getDiffInformedAnalysisBranches`. + * @param logger + * @returns `true` if the diff ranges were successfully computed and persisted, + * otherwise `false`. + */ +export async function computeAndPersistDiffRanges( + branches: PullRequestBranches, + logger: Logger, +): Promise { + const ranges = await getPullRequestEditedDiffRanges(branches, logger); + if (ranges === undefined) { + return false; + } + writeDiffRangesJsonFile(logger, ranges); + const distinctFiles = new Set(ranges.map((r) => r.path)).size; + logger.info( + `Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).`, + ); + return true; +} + async function getFileDiffsWithBasehead( branches: PullRequestBranches, logger: Logger, diff --git a/src/init-action.ts b/src/init-action.ts index ffebf49742..86ff03d076 100644 --- a/src/init-action.ts +++ b/src/init-action.ts @@ -37,11 +37,6 @@ import { makeDiagnostic, makeTelemetryDiagnostic, } from "./diagnostics"; -import { - getDiffInformedAnalysisBranches, - getPullRequestEditedDiffRanges, - writeDiffRangesJsonFile, -} from "./diff-informed-analysis-utils"; import { EnvVar } from "./environment"; import { Feature, FeatureEnablement, initFeatures } from "./feature-flags"; import { @@ -59,7 +54,7 @@ import { runDatabaseInitCluster, } from "./init"; import { JavaEnvVars, KnownLanguage } from "./languages"; -import { getActionsLogger, Logger, withGroupAsync } from "./logging"; +import { getActionsLogger, Logger } from "./logging"; import { downloadOverlayBaseDatabaseFromCache, OverlayBaseDatabaseDownloadStats, @@ -427,7 +422,6 @@ async function run(startedAt: Date) { } await checkInstallPython311(config.languages, codeql); - await computeAndPersistDiffRanges(codeql, features, logger); } catch (unwrappedError) { const error = wrapError(unwrappedError); core.setFailed(error.message); @@ -818,42 +812,6 @@ async function loadRepositoryProperties( } } -/** - * Compute and persist diff ranges when diff-informed analysis is enabled - * (feature flag + PR context). This writes the standard pr-diff-range.json - * file for later reuse in the analyze step. Failures are logged but non-fatal. - */ -async function computeAndPersistDiffRanges( - codeql: CodeQL, - features: FeatureEnablement, - logger: Logger, -): Promise { - await withGroupAsync("Computing PR diff ranges", async () => { - try { - const branches = await getDiffInformedAnalysisBranches( - codeql, - features, - logger, - ); - if (!branches) { - return; - } - const ranges = await getPullRequestEditedDiffRanges(branches, logger); - if (ranges === undefined) { - return; - } - writeDiffRangesJsonFile(logger, ranges); - const distinctFiles = new Set(ranges.map((r) => r.path)).size; - logger.info( - `Persisted ${ranges.length} diff range(s) across ${distinctFiles} file(s).`, - ); - } catch (e) { - logger.warning( - `Failed to compute and persist PR diff ranges: ${getErrorMessage(e)}`, - ); - } - }); -} async function recordZstdAvailability( config: configUtils.Config, zstdAvailability: ZstdAvailability,