From 311292c28fcad4d4c3efb0b85ec3f23b2785a19f Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Wed, 3 Jun 2026 16:39:55 -0500 Subject: [PATCH] Cache the output of `codeql resolve languages` Repeated calls to `resolveLanguages()` will only pay the performance penalty of executing `codeql resolve languages` once. --- src/codeql.ts | 41 ++++++++++++++------------ src/environment.ts | 6 ++++ src/util.ts | 71 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/codeql.ts b/src/codeql.ts index 5d78337d2..0747830d0 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -731,26 +731,31 @@ async function getCodeQLForCmd( filterToLanguagesWithQueries: boolean; } = { filterToLanguagesWithQueries: false }, ) { - const codeqlArgs = [ - "resolve", - "languages", - "--format=betterjson", - "--extractor-options-verbosity=4", - "--extractor-include-aliases", - ...(filterToLanguagesWithQueries - ? ["--filter-to-languages-with-queries"] - : []), - ...getExtraOptionsFromEnv(["resolve", "languages"]), - ]; - const output = await runCli(cmd, codeqlArgs); + let result = util.getCachedCodeQlResolveLanguages(cmd); + if (result === undefined) { + const codeqlArgs = [ + "resolve", + "languages", + "--format=betterjson", + "--extractor-options-verbosity=4", + "--extractor-include-aliases", + ...(filterToLanguagesWithQueries + ? ["--filter-to-languages-with-queries"] + : []), + ...getExtraOptionsFromEnv(["resolve", "languages"]), + ]; + const output = await runCli(cmd, codeqlArgs); - try { - return JSON.parse(output) as ResolveLanguagesOutput; - } catch (e) { - throw new Error( - `Unexpected output from codeql resolve languages with --format=betterjson: ${e}`, - ); + try { + result = JSON.parse(output) as ResolveLanguagesOutput; + } catch (e) { + throw new Error( + `Unexpected output from codeql resolve languages with --format=betterjson: ${e}`, + ); + } + util.cacheCodeQlResolveLanguages(cmd, result); } + return result; }, async resolveBuildEnvironment( workingDir: string | undefined, diff --git a/src/environment.ts b/src/environment.ts index c3f54ebd2..1324abf28 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -23,6 +23,12 @@ export enum EnvVar { */ CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO", + /** + * `ResolveLanguagesOutput` for the CodeQL CLI, so later Actions steps can reuse it instead of + * invoking `codeql resolve languages` again. + */ + CODEQL_RESOLVE_LANGUAGES = "CODEQL_ACTION_CLI_RESOLVE_LANGUAGES", + /** Whether the CodeQL Action has invoked the Go autobuilder. */ DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG", diff --git a/src/util.ts b/src/util.ts index 200d68d2c..a7833b2df 100644 --- a/src/util.ts +++ b/src/util.ts @@ -10,7 +10,7 @@ import * as yaml from "js-yaml"; import * as semver from "semver"; import * as apiCompatibility from "./api-compatibility.json"; -import type { CodeQL, VersionInfo } from "./codeql"; +import type { CodeQL, VersionInfo, ResolveLanguagesOutput } from "./codeql"; import type { Pack } from "./config/db-config"; import type { Config } from "./config-utils"; import { EnvVar } from "./environment"; @@ -701,6 +701,75 @@ export function getCachedCodeQlVersion(cmd?: string): undefined | VersionInfo { return cachedCodeQlVersion; } +let cachedCodeQlResolveLanguages: undefined | ResolveLanguagesOutput = + undefined; + +interface PersistedResolveLanguagesOutput { + cmd: string; + output: ResolveLanguagesOutput; +} + +export function cacheCodeQlResolveLanguages( + cmd: string, + output: ResolveLanguagesOutput, +): void { + if (cachedCodeQlResolveLanguages !== undefined) { + throw new Error("cacheCodeQlResolveLanguages() should be called only once"); + } + cachedCodeQlResolveLanguages = output; + // Persist the output so that subsequent Actions steps, which run in separate + // processes, can reuse it rather than invoking `codeql resolve languages` again. We + // record the CLI path so that a different step using a different CodeQL bundle + // doesn't pick up a stale output. + core.exportVariable( + EnvVar.CODEQL_RESOLVE_LANGUAGES, + JSON.stringify({ cmd, output }), + ); +} + +function isPersistedResolveLanguagesOutput( + value: unknown, +): value is PersistedResolveLanguagesOutput { + return ( + typeof value === "object" && + value !== null && + typeof (value as Record).cmd === "string" && + typeof (value as Record).output === "object" && + (value as Record).output !== null + ); +} + +export function getCachedCodeQlResolveLanguages( + cmd?: string, +): undefined | ResolveLanguagesOutput { + if (cachedCodeQlResolveLanguages !== undefined) { + return cachedCodeQlResolveLanguages; + } + // Fall back to the value persisted by an earlier Actions step, if any. This is + // best-effort: any malformed or mismatched value is ignored so that the caller + // invokes `codeql resolve languages` instead. + const serialized = process.env[EnvVar.CODEQL_RESOLVE_LANGUAGES]; + if (!serialized) { + return undefined; + } + let persisted: unknown; + try { + persisted = JSON.parse(serialized); + } catch { + return undefined; + } + if ( + !isPersistedResolveLanguagesOutput(persisted) || + (cmd !== undefined && persisted.cmd !== cmd) + ) { + return undefined; + } + // Memoize the parsed value so that subsequent calls in this process don't + // re-parse the environment variable. + cachedCodeQlResolveLanguages = persisted.output; + return cachedCodeQlResolveLanguages; +} + export async function codeQlVersionAtLeast( codeql: CodeQL, requiredVersion: string,