mirror of
https://github.com/github/codeql-action.git
synced 2026-10-30 01:27:22 +00:00
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
import * as fs from "fs";
|
|
import * as path from "path";
|
|
|
|
import { getTemporaryDirectory } from "./actions-util";
|
|
import * as json from "./json";
|
|
|
|
/**
|
|
* The name of the temporary file that backs the on-disk cache of
|
|
* CLI responses between workflow steps.
|
|
*/
|
|
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
|
|
|
|
/** The keys for cached CLI commands. */
|
|
export enum CommandCacheKey {
|
|
Version = "version",
|
|
ResolveLanguages = "resolveLanguages",
|
|
}
|
|
|
|
/** A single cached command output together with the CLI path it came from. */
|
|
interface CacheEntry {
|
|
/**
|
|
* The path to the CodeQL CLI that produced `output`. Persisted so that a
|
|
* different step using a different CodeQL bundle doesn't pick up a stale
|
|
* value.
|
|
*/
|
|
cmd: string;
|
|
output: unknown;
|
|
}
|
|
|
|
/**
|
|
* Tier 1: the in-process memo. Consulted first on every lookup and populated
|
|
* whenever a value is read from the file (tier 2) or computed via the CLI
|
|
* (tier 3).
|
|
*/
|
|
const inMemoryCache = new Map<string, CacheEntry>();
|
|
|
|
function getCommandCacheFilePath(): string {
|
|
return path.join(getTemporaryDirectory(), COMMAND_CACHE_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Reads and parses the temporary cache file. Best-effort: a missing, malformed,
|
|
* or otherwise unreadable file is treated as an empty cache.
|
|
*/
|
|
function readCommandCacheFile(): Record<string, CacheEntry> {
|
|
let contents: string;
|
|
try {
|
|
contents = fs.readFileSync(getCommandCacheFilePath(), "utf8");
|
|
} catch {
|
|
return {};
|
|
}
|
|
try {
|
|
const parsed = json.parseString(contents);
|
|
if (json.isObject(parsed)) {
|
|
return parsed;
|
|
}
|
|
} catch {
|
|
// Fall through and treat a malformed file as empty.
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Persists the cache to the temporary file. Best-effort: a failure to write
|
|
* just means a later step re-runs the CLI.
|
|
*/
|
|
function writeCommandCacheFile(data: Record<string, CacheEntry>): void {
|
|
try {
|
|
fs.writeFileSync(getCommandCacheFilePath(), JSON.stringify(data));
|
|
} catch {
|
|
// Best-effort; ignore write failures.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stores the output of a command under `key`, writing it to both the in-memory
|
|
* memo (tier 1) and the temporary file (tier 2).
|
|
*
|
|
* Should only be called once per key within a single process; doing otherwise
|
|
* indicates a logic error, since a value that has already been cached should be
|
|
* served from the memo rather than recomputed.
|
|
*/
|
|
export function cacheCommandOutput(
|
|
key: string,
|
|
cmd: string,
|
|
output: unknown,
|
|
): void {
|
|
if (inMemoryCache.has(key)) {
|
|
throw new Error(
|
|
`cacheCommandOutput() should be called only once per key, but was called more than once for '${key}'.`,
|
|
);
|
|
}
|
|
const entry: CacheEntry = { cmd, output };
|
|
inMemoryCache.set(key, entry);
|
|
|
|
const data = readCommandCacheFile();
|
|
data[key] = entry;
|
|
writeCommandCacheFile(data);
|
|
}
|
|
|
|
/**
|
|
* Returns the cached output for `key`, or `undefined` if it isn't cached.
|
|
*
|
|
* Resolves tier 1 (in-memory memo) first, then tier 2 (temporary file). A value
|
|
* loaded from the file is ignored unless its `cmd` matches the optional `cmd`
|
|
* argument, and it satisfies the optional `validate` type guard; valid values
|
|
* are memoized into tier 1 before being returned.
|
|
*
|
|
* A return value of `undefined` signals the caller to fall back to tier 3 (the
|
|
* CLI).
|
|
*/
|
|
export function getCachedCommandOutput<T>(
|
|
key: string,
|
|
cmd?: string,
|
|
validate?: (output: unknown) => output is T,
|
|
): T | undefined {
|
|
// Tier 1: the in-memory variable.
|
|
const memoized = inMemoryCache.get(key);
|
|
if (memoized !== undefined) {
|
|
return memoized.output as T;
|
|
}
|
|
|
|
// Tier 2: the temporary file persisted by an earlier step, if any.
|
|
const entry = readCommandCacheFile()[key] as unknown;
|
|
if (
|
|
!json.isObject<CacheEntry>(entry) ||
|
|
!json.isString(entry.cmd) ||
|
|
(cmd !== undefined && entry.cmd !== cmd) ||
|
|
(validate !== undefined && !validate(entry.output))
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
// Memoize so subsequent lookups in this process hit tier 1.
|
|
inMemoryCache.set(key, { cmd: entry.cmd, output: entry.output });
|
|
return entry.output as T;
|
|
}
|
|
|
|
/**
|
|
* Clears the in-process memo (tier 1). Only for use in tests, which exercise
|
|
* multiple "steps" within a single process.
|
|
*/
|
|
export function resetCachedCommandOutputs(): void {
|
|
inMemoryCache.clear();
|
|
}
|