Move cache-related util functions into dedicated module

This commit is contained in:
Mario Campos
2026-08-11 13:39:19 -05:00
parent 4dc327a942
commit 1332611f51
8 changed files with 1104 additions and 1081 deletions

1795
lib/entry-points.js generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,111 @@
import * as fs from "fs";
import path from "path";
import test from "ava";
import { EnvVar } from "../environment";
import { getTestEnv, setupTests } from "../testing-utils";
import * as util from "../util";
import * as outputCache from "./output-cache";
setupTests(test);
test.serial(
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "codeql-action-command-cache.json");
fs.writeFileSync(
cacheFile,
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0" },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.deepEqual(outputCache.getCachedCodeQlVersion("/path/to/codeql", env), {
version: "2.20.0",
});
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(
cacheFile,
JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(
outputCache.getCachedCodeQlVersion("/path/to/codeql", env),
undefined,
);
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a malformed persisted value",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(cacheFile, "not valid json", "utf8");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(
outputCache.getCachedCodeQlVersion("/path/to/codeql", env),
undefined,
);
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const testValues = [
{ cmd: "/path/to/codeql" },
{ cmd: "/path/to/codeql", version: {} },
{ cmd: "/path/to/codeql", version: { version: 2 } },
{ version: { version: "2.20.0" } },
{
cmd: "/path/to/codeql",
version: { version: "2.20.0", overlayVersion: "1" },
},
{
cmd: "/path/to/codeql",
version: { version: "2.20.0", features: "nope" },
},
].map((v) => JSON.stringify(v));
for (const value of testValues) {
fs.writeFileSync(cacheFile, value, "utf8");
t.is(
outputCache.getCachedCodeQlVersion("/path/to/codeql", env),
undefined,
value,
);
}
});
},
);
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(outputCache.getCachedCodeQlVersion("/path/to/codeql", env), undefined);
});
});

View File

@@ -1,6 +1,10 @@
import * as fs from "fs";
import path from "path";
import { getTemporaryDirectory } from "../actions-util";
import { VersionInfo } from "../codeql";
import { Env, getEnv } from "../environment";
import { isPersistedVersionInfo } from "../util";
/**
* The name of the temporary file that backs the on-disk cache of
@@ -8,10 +12,89 @@ import { getTemporaryDirectory } from "../actions-util";
*/
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
/**
* The module-global variable that caches the CodeQL CLI version in-memory.
*/
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
/**
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
* which exercise multiple "steps" within a single process.
*/
export function resetCachedCodeQlVersion(): void {
cachedCodeQlVersion = undefined;
}
/**
* Returns the path to the temporary file that backs the
* on-disk cache of CLI responses between workflow steps.
*/
function getCommandCacheFilePath(): string {
return path.join(getTemporaryDirectory(), COMMAND_CACHE_FILENAME);
function getCommandCacheFilePath(env: Env): string {
return path.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
}
/**
* Caches the CodeQL CLI version both in-memory and on disk.
* @param cmd The path to the CodeQL CLI.
* @param version The version information to cache.
* @param env The environment variables to use.
*/
export function cacheCodeQlVersion(
cmd: string,
version: VersionInfo,
env: Env = getEnv(),
): void {
if (cachedCodeQlVersion !== undefined) {
throw new Error("cacheCodeQlVersion() should be called only once");
}
cachedCodeQlVersion = version;
// Persist the version so that subsequent Actions steps, which run in separate
// processes, can reuse it rather than invoking `codeql version` again. We
// record the CLI path so that a different step using a different CodeQL bundle
// doesn't pick up a stale version.
fs.writeFileSync(
getCommandCacheFilePath(env),
JSON.stringify({ cmd, version }),
"utf8",
);
}
/**
* Returns the cached CodeQL CLI version, if any. If not cached,
* attempts to read and parse it from disk.
* @param cmd The path to the CodeQL CLI.
* @param env The environment variables to use.
*/
export function getCachedCodeQlVersion(
cmd?: string,
env: Env = getEnv(),
): undefined | VersionInfo {
if (cachedCodeQlVersion !== undefined) {
return cachedCodeQlVersion;
}
// 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 version` instead.
let serialized: string;
try {
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
} catch {
return undefined;
}
let persisted: unknown;
try {
persisted = JSON.parse(serialized);
} catch {
return undefined;
}
if (
!isPersistedVersionInfo(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.
cachedCodeQlVersion = persisted.version;
return cachedCodeQlVersion;
}

View File

@@ -12,6 +12,7 @@ import {
runTool,
} from "./actions-util";
import * as api from "./api-client";
import * as outputCache from "./cli/output-cache";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
@@ -502,7 +503,7 @@ async function getCodeQLForCmd(
return cmd;
},
async getVersion() {
let result = util.getCachedCodeQlVersion(cmd);
let result = outputCache.getCachedCodeQlVersion(cmd);
if (result === undefined) {
result = await runCliJson<VersionInfo>(
cmd,
@@ -511,7 +512,7 @@ async function getCodeQLForCmd(
noStreamStdout: true,
},
);
util.cacheCodeQlVersion(cmd, result);
outputCache.cacheCodeQlVersion(cmd, result);
}
return result;
},

View File

@@ -14,6 +14,7 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { getCachedCodeQlVersion } from "./cli/output-cache";
import type { Config } from "./config/action-config";
import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
@@ -30,7 +31,6 @@ import { registryBaseSchema } from "./start-proxy/types";
import {
ConfigurationError,
getRequiredEnvParam,
getCachedCodeQlVersion,
isInTestMode,
GITHUB_DOTCOM_URL,
DiskUsage,

View File

@@ -18,6 +18,7 @@ import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client";
import { GitHubApiDetails } from "./api-client";
import { CachingKind } from "./caching-utils";
import { resetCachedCodeQlVersion } from "./cli/output-cache";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
@@ -39,7 +40,6 @@ import {
GitHubVariant,
GitHubVersion,
HTTPError,
resetCachedCodeQlVersion,
Result,
Success,
} from "./util";

View File

@@ -10,7 +10,7 @@ import * as sinon from "sinon";
import * as api from "./api-client";
import { EnvVar } from "./environment";
import { getRunnerLogger } from "./logging";
import { getTestEnv, setupTests } from "./testing-utils";
import { setupTests } from "./testing-utils";
import * as util from "./util";
setupTests(test);
@@ -532,96 +532,3 @@ test("Failure.orElse returns the default value for a failure result", (t) => {
const result = new util.Failure(new Error("test error"));
t.is(result.orElse("default value"), "default value");
});
test.serial(
"getCachedCodeQlVersion reuses a version persisted by an earlier step",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(
cacheFile,
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0" },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql", env), {
version: "2.20.0",
});
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(
cacheFile,
JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(util.getCachedCodeQlVersion("/path/to/codeql", env), undefined);
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a malformed persisted value",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
fs.writeFileSync(cacheFile, "not valid json", "utf8");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(util.getCachedCodeQlVersion("/path/to/codeql", env), undefined);
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const cacheFile = path.join(tmpDir, "version.json");
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const testValues = [
{ cmd: "/path/to/codeql" },
{ cmd: "/path/to/codeql", version: {} },
{ cmd: "/path/to/codeql", version: { version: 2 } },
{ version: { version: "2.20.0" } },
{
cmd: "/path/to/codeql",
version: { version: "2.20.0", overlayVersion: "1" },
},
{
cmd: "/path/to/codeql",
version: { version: "2.20.0", features: "nope" },
},
].map((v) => JSON.stringify(v));
for (const value of testValues) {
fs.writeFileSync(cacheFile, value, "utf8");
t.is(
util.getCachedCodeQlVersion("/path/to/codeql", env),
undefined,
value,
);
}
});
},
);
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(util.getCachedCodeQlVersion("/path/to/codeql", env), undefined);
});
});

View File

@@ -9,12 +9,11 @@ import getFolderSize from "get-folder-size";
import * as yaml from "js-yaml";
import * as semver from "semver";
import { getTemporaryDirectory } from "./actions-util";
import * as apiCompatibility from "./api-compatibility.json";
import type { CodeQL, VersionInfo } from "./codeql";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import { Env, EnvVar, getEnv, getRequiredEnvParam } from "./environment";
import { EnvVar, getRequiredEnvParam } from "./environment";
import * as json from "./json";
import { Language } from "./languages";
import { Logger } from "./logging";
@@ -599,16 +598,6 @@ export function asHTTPError(arg: any): HTTPError | undefined {
return undefined;
}
let cachedCodeQlVersion: undefined | VersionInfo = undefined;
/**
* Resets the in-process cache of the CodeQL CLI version. Only for use in tests,
* which exercise multiple "steps" within a single process.
*/
export function resetCachedCodeQlVersion(): void {
cachedCodeQlVersion = undefined;
}
/** The persisted version together with the CLI path it was obtained from. */
interface PersistedVersionInfo {
cmd: string;
@@ -629,7 +618,7 @@ function isVersionInfo(x: unknown): x is VersionInfo {
);
}
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
export function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
const candidate = x as Partial<PersistedVersionInfo> | null;
return (
typeof candidate === "object" &&
@@ -639,79 +628,6 @@ function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
);
}
/**
* Returns the file path to the `codeql version` output cache.
* @param env The environment variables to use.
*/
function getPathToCodeQLVersionCacheFile(env: Env): string {
return path.join(getTemporaryDirectory(env), "version.json");
}
/**
* Caches the CodeQL CLI version both in-memory and on disk.
* @param cmd The path to the CodeQL CLI.
* @param version The version information to cache.
* @param env The environment variables to use.
*/
export function cacheCodeQlVersion(
cmd: string,
version: VersionInfo,
env: Env = getEnv(),
): void {
if (cachedCodeQlVersion !== undefined) {
throw new Error("cacheCodeQlVersion() should be called only once");
}
cachedCodeQlVersion = version;
// Persist the version so that subsequent Actions steps, which run in separate
// processes, can reuse it rather than invoking `codeql version` again. We
// record the CLI path so that a different step using a different CodeQL bundle
// doesn't pick up a stale version.
fs.writeFileSync(
getPathToCodeQLVersionCacheFile(env),
JSON.stringify({ cmd, version }),
"utf8",
);
}
/**
* Returns the cached CodeQL CLI version, if any.
* @param cmd The path to the CodeQL CLI.
* @param env The environment variables to use.
*/
export function getCachedCodeQlVersion(
cmd?: string,
env: Env = getEnv(),
): undefined | VersionInfo {
if (cachedCodeQlVersion !== undefined) {
return cachedCodeQlVersion;
}
// 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 version` instead.
let serialized: string;
try {
serialized = fs.readFileSync(getPathToCodeQLVersionCacheFile(env), "utf8");
} catch {
return undefined;
}
let persisted: unknown;
try {
persisted = JSON.parse(serialized);
} catch {
return undefined;
}
if (
!isPersistedVersionInfo(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.
cachedCodeQlVersion = persisted.version;
return cachedCodeQlVersion;
}
export async function codeQlVersionAtLeast(
codeql: CodeQL,
requiredVersion: string,