diff --git a/lib/init-action.js b/lib/init-action.js index ac1f72ea58..6a9b0b7d09 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -105442,6 +105442,17 @@ var CODEQL_OVERLAY_MINIMUM_VERSION_PYTHON = "2.23.9"; var CODEQL_OVERLAY_MINIMUM_VERSION_RUBY = "2.23.9"; var OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB = 7500; var OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_BYTES = OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB * 1e6; +function revertOverlayModeIfDiffInformedUnavailable(config, shouldRunDiffInformedAnalysis, diffInformedRangesPrepared, logger) { + if (config.overlayDatabaseMode !== "overlay" /* Overlay */) { + return; + } + if (shouldRunDiffInformedAnalysis && !diffInformedRangesPrepared) { + logger.warning( + `Diff-informed analysis is not available for this pull request. Reverting overlay database mode to ${"none" /* None */}.` + ); + config.overlayDatabaseMode = "none" /* None */; + } +} async function writeBaseDatabaseOidsFile(config, sourceRoot) { const gitFileOids = await getFileOidsUnderPath(sourceRoot); const gitFileOidsJson = JSON.stringify(gitFileOids); @@ -106234,9 +106245,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; @@ -106283,6 +106291,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", @@ -107207,15 +107227,46 @@ async function initConfig(features, inputs) { overlayDisabledReason ); } - if (config.overlayDatabaseMode === "overlay" /* Overlay */ || await shouldPerformDiffInformedAnalysis( - inputs.codeql, - inputs.features, - logger - )) { + let shouldRunDiffInformedAnalysis = false; + let diffInformedRangesPrepared = false; + try { + const diffInformedAnalysisBranches = await getDiffInformedAnalysisBranches( + inputs.codeql, + inputs.features, + logger + ); + shouldRunDiffInformedAnalysis = diffInformedAnalysisBranches !== void 0; + if (diffInformedAnalysisBranches) { + await withGroupAsync("Computing PR diff ranges", async () => { + try { + diffInformedRangesPrepared = await computeAndPersistDiffRanges( + diffInformedAnalysisBranches, + logger + ); + } catch (e) { + logger.warning( + `Failed to compute diff-informed analysis ranges: ${getErrorMessage(e)}` + ); + } + }); + } + } catch (e) { + logger.warning( + `Failed to determine diff-informed analysis availability: ${getErrorMessage(e)}` + ); + shouldRunDiffInformedAnalysis = true; + } + if (config.overlayDatabaseMode === "overlay" /* Overlay */ || shouldRunDiffInformedAnalysis) { config.extraQueryExclusions.push({ exclude: { tags: "exclude-from-incremental" } }); } + revertOverlayModeIfDiffInformedUnavailable( + config, + shouldRunDiffInformedAnalysis, + diffInformedRangesPrepared, + logger + ); if (await isTrapCachingEnabled(features, config.overlayDatabaseMode)) { const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime( inputs.codeql, @@ -110330,7 +110381,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); @@ -110598,33 +110648,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.ts b/src/config-utils.ts index 55d5afa727..5e2952437d 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 { + computeAndPersistDiffRanges, + getDiffInformedAnalysisBranches, +} from "./diff-informed-analysis-utils"; import { EnvVar } from "./environment"; import * as errorMessages from "./error-messages"; import { Feature, FeatureEnablement } from "./feature-flags"; @@ -49,8 +52,12 @@ import { isAnalyzingDefaultBranch, } from "./git-utils"; import { KnownLanguage, Language } from "./languages"; -import { Logger } from "./logging"; -import { CODEQL_OVERLAY_MINIMUM_VERSION, OverlayDatabaseMode } from "./overlay"; +import { Logger, withGroupAsync } from "./logging"; +import { + CODEQL_OVERLAY_MINIMUM_VERSION, + OverlayDatabaseMode, + revertOverlayModeIfDiffInformedUnavailable, +} from "./overlay"; import { addOverlayDisablementDiagnostics, OverlayDisabledReason, @@ -1229,19 +1236,55 @@ export async function initConfig( ); } - if ( - config.overlayDatabaseMode === OverlayDatabaseMode.Overlay || - (await shouldPerformDiffInformedAnalysis( + let shouldRunDiffInformedAnalysis = false; + let diffInformedRangesPrepared = false; + try { + const diffInformedAnalysisBranches = await getDiffInformedAnalysisBranches( inputs.codeql, inputs.features, logger, - )) + ); + shouldRunDiffInformedAnalysis = diffInformedAnalysisBranches !== undefined; + + if (diffInformedAnalysisBranches) { + await withGroupAsync("Computing PR diff ranges", async () => { + try { + diffInformedRangesPrepared = await computeAndPersistDiffRanges( + diffInformedAnalysisBranches, + logger, + ); + } catch (e) { + logger.warning( + `Failed to compute diff-informed analysis ranges: ${getErrorMessage(e)}`, + ); + } + }); + } + } catch (e) { + logger.warning( + `Failed to determine diff-informed analysis availability: ${getErrorMessage(e)}`, + ); + // Treat errors conservatively so overlay falls back if diff-informed + // availability could not be checked. + shouldRunDiffInformedAnalysis = true; + } + + if ( + config.overlayDatabaseMode === OverlayDatabaseMode.Overlay || + shouldRunDiffInformedAnalysis ) { config.extraQueryExclusions.push({ exclude: { tags: "exclude-from-incremental" }, }); } + revertOverlayModeIfDiffInformedUnavailable( + config, + shouldRunDiffInformedAnalysis, + diffInformedRangesPrepared, + logger, + ); + if (await isTrapCachingEnabled(features, config.overlayDatabaseMode)) { const { trapCaches, trapCacheDownloadTime } = await downloadCacheWithTime( inputs.codeql, diff --git a/src/diff-informed-analysis-utils.ts b/src/diff-informed-analysis-utils.ts index 1c98d4caca..30e91dc2ac 100644 --- a/src/diff-informed-analysis-utils.ts +++ b/src/diff-informed-analysis-utils.ts @@ -151,6 +151,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 bc95304bfa..f2d17ff30f 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, diff --git a/src/overlay/index.test.ts b/src/overlay/index.test.ts index 2d0b4d3fcb..80baf22178 100644 --- a/src/overlay/index.test.ts +++ b/src/overlay/index.test.ts @@ -24,6 +24,7 @@ import { getCacheRestoreKeyPrefix, getCacheSaveKey, OverlayDatabaseMode, + revertOverlayModeIfDiffInformedUnavailable, writeBaseDatabaseOidsFile, writeOverlayChangesFile, } from "."; @@ -602,3 +603,54 @@ test.serial("overlay-base database cache keys remain stable", async (t) => { `Expected save key "${saveKey}" to start with restore key prefix "${restoreKeyPrefix}"`, ); }); + +test.serial( + "revertOverlayModeIfDiffInformedUnavailable: no-op when mode is not Overlay", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.None; + const logger = getRunnerLogger(true); + + revertOverlayModeIfDiffInformedUnavailable(config, true, false, logger); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None); + }, +); + +test.serial( + "revertOverlayModeIfDiffInformedUnavailable: no-op when diff-informed analysis is available", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.Overlay; + const logger = getRunnerLogger(true); + + revertOverlayModeIfDiffInformedUnavailable(config, true, true, logger); + + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.Overlay); + }, +); + +test.serial( + "revertOverlayModeIfDiffInformedUnavailable: reverts to None when diff-informed analysis is unavailable", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = + OverlayDatabaseMode.Overlay as OverlayDatabaseMode; + const logger = getRunnerLogger(true); + + revertOverlayModeIfDiffInformedUnavailable(config, true, false, logger); + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.None); + }, +); + +test.serial( + "revertOverlayModeIfDiffInformedUnavailable: no-op when diff-informed analysis is disabled", + (t) => { + const config = createTestConfig({}); + config.overlayDatabaseMode = OverlayDatabaseMode.Overlay; + const logger = getRunnerLogger(true); + + revertOverlayModeIfDiffInformedUnavailable(config, false, false, logger); + t.is(config.overlayDatabaseMode, OverlayDatabaseMode.Overlay); + }, +); diff --git a/src/overlay/index.ts b/src/overlay/index.ts index 4905b254f7..09ed5c698a 100644 --- a/src/overlay/index.ts +++ b/src/overlay/index.ts @@ -62,6 +62,38 @@ const OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB = 7500; const OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_BYTES = OVERLAY_BASE_DATABASE_MAX_UPLOAD_SIZE_MB * 1_000_000; +/** + * If overlay analysis is enabled but diff-informed analysis could not be + * prepared, reverts the overlay database mode to `None`. Overlay without + * diff-informed is an untested combination that can produce inaccurate + * results, so we fall back to a full non-overlay analysis instead. + * + * @param config The configuration object whose `overlayDatabaseMode` may be mutated. + * @param shouldRunDiffInformedAnalysis Whether diff-informed analysis applies + * to this workflow run. + * @param diffInformedRangesPrepared Whether the diff ranges needed for + * diff-informed analysis were successfully prepared. + * @param logger The logger instance. + */ +export function revertOverlayModeIfDiffInformedUnavailable( + config: Config, + shouldRunDiffInformedAnalysis: boolean, + diffInformedRangesPrepared: boolean, + logger: Logger, +): void { + if (config.overlayDatabaseMode !== OverlayDatabaseMode.Overlay) { + return; + } + + if (shouldRunDiffInformedAnalysis && !diffInformedRangesPrepared) { + logger.warning( + "Diff-informed analysis is not available for this pull request. " + + `Reverting overlay database mode to ${OverlayDatabaseMode.None}.`, + ); + config.overlayDatabaseMode = OverlayDatabaseMode.None; + } +} + /** * Writes a JSON file containing Git OIDs for all tracked files (represented * by path relative to the source root) under the source root. The file is