Compare commits

...

34 Commits

Author SHA1 Message Date
Mario Campos a35a8ac9ef Update isVersionInfo validation function with check lambda 2026-07-13 23:45:44 -05:00
Mario Campos ecf4e52a5e Merge remote-tracking branch 'origin/main' into mario-campos/cache-cli-resolve-langs 2026-07-13 23:29:44 -05:00
Mario Campos 0daa052859 Validate CLI memory-backed cached objects against CLI path 2026-07-13 23:22:43 -05:00
Mario Campos ec96b5271e Delete uncached codeql.resolveLanguages() function call
This CLI command was cached, at one point, but that's been saved for another PR, so this change will too.
2026-07-13 22:37:29 -05:00
Mario Campos 8f1212fe5c Revert src/config-utils.ts
These hunks were related to another hunk elsewhere, that has since now been reverted in favor of a separate/distinct PR.
2026-07-13 22:30:38 -05:00
Mario Campos d4b99ad098 Merge branch 'main' into mario-campos/cache-cli-resolve-langs
# Conflicts:
#	lib/entry-points.js
#	src/codeql.ts
#	src/environment.ts
#	src/status-report.ts
2026-07-09 15:03:56 -05:00
Mario Campos 5110948a58 Revert runCliJson implementation
It's been forked off into #3981
2026-07-09 14:05:47 -05:00
Mario Campos 252ad66b0d Perform deep validation of VersionInfo.features 2026-07-01 15:38:56 -05:00
Mario Campos 7f4f27e7e6 Revert change to resolveExtractor
This is secondary work that will follow from the main purpose of this PR.
2026-07-01 09:41:56 -05:00
Mario Campos bba0d6c67c Revert JSON module changes
These have been spun off into #3980.
2026-06-30 22:25:11 -05:00
Mario Campos da149812f1 Switch CommandCacheKey back to an enum
The code reads better/simpler as an `enum`.
2026-06-30 16:10:41 -05:00
Mario Campos 0d698171d3 Move output type validators to output-cache.ts module
This move simplifies the consumer-side of the module. And, also, the validators should be an internal implementation detail—not the concern of those who wish to "get" a cached output.
2026-06-30 15:39:36 -05:00
Mario Campos c2f43799c5 Refactor CommandCacheKey as a string union 2026-06-29 23:25:50 -05:00
Mario Campos 41965b0e7b Move cache.ts to cli/output-cache.ts
This change should hopefully make the purpose of this module clear.
2026-06-29 22:16:21 -05:00
Mario Campos b3c7aa7372 Add warning log for invalid data in command cache retrieval 2026-06-19 18:04:52 -05:00
Mario Campos 649bababe7 Refactor cache storage to use cacheCommandOutput for consistency 2026-06-19 17:49:45 -05:00
Mario Campos 895ffe7eb5 Front-load caching and rear-load saving to disk
This implementation saves CPU and I/O by not trying to write to the file on every set-cache.
2026-06-19 17:45:48 -05:00
Mario Campos 805b59092e Remove duplicate key check in cacheCommandOutput and related tests
This key-check is unnecessary because this function is only ever called if the key does NOT exist in the cache already. In that case, why check again?
2026-06-19 16:29:52 -05:00
Mario Campos d1bd5ec7b0 Add logging for command cache file read/write failures 2026-06-19 16:07:35 -05:00
Mario Campos 94b12602ac Refactor cache file reading to check for existence before attempting to read 2026-06-19 15:41:47 -05:00
Mario Campos 126166c212 Rename CacheEntry to CommandCacheEntry for consistency 2026-06-19 15:07:56 -05:00
Mario Campos dd7ff30205 Replace string with CommandCacheKey for better type safety 2026-06-19 15:04:22 -05:00
Mario Campos 420c97eadd Improve documentation for cache file and command keys in CLI 2026-06-19 14:12:27 -05:00
Mario Campos defcf1bff6 Rename optional and undefinable functions for clarity; update related schemas to use optionalOrNull 2026-06-19 13:55:56 -05:00
Mario Campos c8e32e423d Refactor resolveLanguages() to cache output according to CLI feature support
The output of `resolveLanguages()` can vary based on whether the flag `--filter-to-languages-with-queries` is included, but not all versions of the CLI support that. This makes caching a single execution problematic, so I opted to cache it based on whether it's supported. If it's supported, it's used; otherwise, it's not.
2026-06-18 14:46:44 -05:00
Mario Campos 553eef0d3f Add error handling for undefined extractors in language resolution 2026-06-18 10:36:50 -05:00
Mario Campos b18df17ee7 Rebased onto main; fixups were needed 2026-06-18 10:25:23 -05:00
Mario Campos a602287f73 Refactor CLI caching with in-memory and file storage 2026-06-18 10:10:46 -05:00
Mario Campos dc8e1e9aa0 Refactor CLI JSON handling into a dedicated runCliJson function 2026-06-18 10:05:20 -05:00
Mario Campos 889ae42672 Refactor CLI executions into helper functions
This provides a separation of concerns between the memoization and the execution.
2026-06-18 10:02:31 -05:00
Mario Campos 587fcb33c7 Refactor isVersionInfo() to use json` module 2026-06-18 09:58:23 -05:00
Mario Campos 445107e23a Validate numbers, objects, and undefinables in the json module
This commit adds a `number` validator`, an `object` validator, an `isNumber` predicate, and `undefinable()` to test optional-but-not-null properties.
2026-06-18 09:58:23 -05:00
Mario Campos 6010f85d81 Reimplement resolveExtractor() as wrapper over resolveLanguages()
By wrapping `resolveLanguages()`, which is memoized, we can avoid executing `codeql resolve extractor` several times over the course of an analysis.
2026-06-18 09:58:23 -05:00
Mario Campos 311292c28f Cache the output of codeql resolve languages
Repeated calls to `resolveLanguages()` will only pay the performance penalty of executing `codeql resolve languages` once.
2026-06-18 09:58:20 -05:00
11 changed files with 2553 additions and 2162 deletions
+2038 -1970
View File
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
import * as fs from "fs";
import * as os from "os";
import path from "path";
import test from "ava";
import { setupTests } from "../testing-utils";
import {
cacheCommandOutput,
CommandCacheKey,
getCachedCommandOutput,
resetCachedCommandOutputs,
type VersionInfo,
} from "./output-cache";
setupTests(test);
const COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
/**
* Runs `body` with a temporary directory configured as the cache's backing
* store (`RUNNER_TEMP`). `CODEQL_ACTION_TEMP` is cleared so that
* `getTemporaryDirectory()` falls back to `RUNNER_TEMP`.
*
* `setupTests` snapshots and restores `process.env` around every test, so we
* don't restore the environment variables we set here ourselves.
*/
async function withCacheDir(
body: (cacheFilePath: string) => Promise<void> | void,
): Promise<void> {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cache-test-"));
process.env["RUNNER_TEMP"] = tmpDir;
delete process.env["CODEQL_ACTION_TEMP"];
resetCachedCommandOutputs();
try {
await body(path.join(tmpDir, COMMAND_CACHE_FILENAME));
} finally {
await fs.promises.rm(tmpDir, { force: true, recursive: true });
}
}
function writeCacheFile(
cacheFilePath: string,
contents: Record<string, unknown>,
): void {
fs.writeFileSync(cacheFilePath, JSON.stringify(contents));
}
test.serial(
"getCachedCommandOutput reuses an output persisted by an earlier step",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.20.0" },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
{ version: "2.20.0" },
);
});
},
);
test.serial(
"getCachedCommandOutput ignores an output persisted from a different CLI",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/other-codeql",
output: { version: "2.20.0" },
},
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput ignores a malformed cache file",
async (t) => {
await withCacheDir((cacheFilePath) => {
fs.writeFileSync(cacheFilePath, "not valid json");
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput returns undefined when there is no cache file",
async (t) => {
await withCacheDir(() => {
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput ignores an output that fails validation",
async (t) => {
await withCacheDir((cacheFilePath) => {
for (const output of [
{},
{ version: 2 },
{ version: "2.20.0", overlayVersion: "1" },
{ version: "2.20.0", features: "nope" },
]) {
resetCachedCommandOutputs();
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: { cmd: "/path/to/codeql", output },
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
JSON.stringify(output),
);
}
});
},
);
test.serial(
"getCachedCommandOutput ignores an entry missing the cmd field",
async (t) => {
await withCacheDir((cacheFilePath) => {
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: { output: { version: "2.20.0" } },
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial("cacheCommandOutput persists the output to the memo", async (t) => {
await withCacheDir(() => {
const output: VersionInfo = { version: "2.20.0" };
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
// Tier 1: the value is immediately available from the memo.
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
output,
);
});
});
test.serial(
"getCachedCommandOutput treats a memoized output from another CLI as a miss",
async (t) => {
await withCacheDir(() => {
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
version: "2.20.0",
});
t.is(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
undefined,
);
});
},
);
test.serial(
"getCachedCommandOutput prefers the in-memory memo over the file",
async (t) => {
await withCacheDir((cacheFilePath) => {
const output: VersionInfo = { version: "2.20.0", overlayVersion: 1 };
cacheCommandOutput(CommandCacheKey.Version, "/path/to/codeql", output);
// Overwrite the file with a different value; the memo (tier 1) should win.
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.21.0" },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
output,
);
});
},
);
test.serial(
"getCachedCommandOutput falls back to file when memoized output comes from another CLI",
async (t) => {
await withCacheDir((cacheFilePath) => {
cacheCommandOutput(CommandCacheKey.Version, "/path/to/other-codeql", {
version: "2.19.0",
});
writeCacheFile(cacheFilePath, {
[CommandCacheKey.Version]: {
cmd: "/path/to/codeql",
output: { version: "2.20.0", overlayVersion: 1 },
},
});
t.deepEqual(
getCachedCommandOutput(CommandCacheKey.Version, "/path/to/codeql"),
{ version: "2.20.0", overlayVersion: 1 },
);
});
},
);
+238
View File
@@ -0,0 +1,238 @@
import * as fs from "fs";
import * as path from "path";
import { getTemporaryDirectory } from "../actions-util";
import * as json from "../json";
import { getActionsLogger } from "../logging";
/**
* 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";
/** A key used to identify cached command output. */
export enum CommandCacheKey {
Version = "version",
ResolveLanguages = "resolve languages",
}
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
* analysis with that pack. Otherwise, if the overlay versions are different,
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
/** Returns true if `x` is a {@link VersionInfo}. */
export function isVersionInfo(x: unknown): x is VersionInfo {
const isBooleanRecord = (obj: unknown): obj is Record<string, boolean> =>
json.isObject(obj) &&
Object.values(obj).every((val) => typeof val === "boolean");
return (
json.isObject(x) &&
json.validateSchema(
{
version: json.string,
features: {
validate: isBooleanRecord,
check: (obj) => ({
unknownKeys: [],
invalidKeys: [],
valid: isBooleanRecord(obj),
}),
required: false,
},
overlayVersion: json.optional(json.number),
},
x,
)
);
}
export interface ResolveLanguagesOutput {
aliases?: {
[alias: string]: string;
};
extractors: {
[language: string]: Array<{
extractor_root: string;
extractor_options?: any;
}>;
};
}
/** Returns true if `x` is a {@link ResolveLanguagesOutput}. */
export function isResolveLanguagesOutput(
x: unknown,
): x is ResolveLanguagesOutput {
return (
json.isObject<ResolveLanguagesOutput>(x) &&
json.isObject(x.extractors) &&
Object.values(x.extractors).every(
(extractorList) =>
json.isArray(extractorList) &&
extractorList.every(
(extractor) =>
json.isObject<{ extractor_root: unknown }>(extractor) &&
json.isString(extractor.extractor_root),
),
) &&
(x.aliases === undefined ||
(json.isObject(x.aliases) &&
Object.values(x.aliases).every((alias) => json.isString(alias))))
);
}
export type CommandCacheKeyOutputMap = {
[CommandCacheKey.Version]: VersionInfo;
[CommandCacheKey.ResolveLanguages]: ResolveLanguagesOutput;
};
const commandCacheValidators: {
[K in CommandCacheKey]: (
output: unknown,
) => output is CommandCacheKeyOutputMap[K];
} = {
[CommandCacheKey.Version]: isVersionInfo,
[CommandCacheKey.ResolveLanguages]: isResolveLanguagesOutput,
};
interface StoredCommandCacheEntry {
cmd: string;
output: unknown;
}
/** A single cached command output together with the CLI path it came from. */
export interface CommandCacheEntry<K extends CommandCacheKey> {
/**
* 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: CommandCacheKeyOutputMap[K];
}
/**
* 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<
CommandCacheKey,
CommandCacheEntry<CommandCacheKey>
>();
const logger = getActionsLogger();
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, StoredCommandCacheEntry> {
if (!fs.existsSync(getCommandCacheFilePath())) {
return {};
}
try {
const contents = fs.readFileSync(getCommandCacheFilePath(), "utf8");
const parsed = json.parseString(contents);
if (json.isObject(parsed)) {
return parsed;
}
} catch (e) {
logger.warning(`Failed to read or parse command cache file: ${e}`);
}
return {};
}
/**
* Persists the in-memory cache to the temporary file. Best-effort: a failure to write
* just means a later step re-runs the CLI.
*/
export function writeCommandCacheFile(): void {
try {
fs.writeFileSync(
getCommandCacheFilePath(),
JSON.stringify(Object.fromEntries(inMemoryCache)),
);
} catch (e) {
logger.warning(`Failed to write command cache file: ${e}`);
}
}
/**
* Stores the output of a CLI command under `key` in a module-global object.
*/
export function cacheCommandOutput<K extends CommandCacheKey>(
key: K,
cmd: string,
output: CommandCacheKeyOutputMap[K],
): void {
const entry: CommandCacheEntry<K> = { cmd, output };
inMemoryCache.set(key, entry);
}
/**
* 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 its output satisfies the internal validator for `key`; 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<K extends CommandCacheKey>(
key: K,
cmd?: string,
): CommandCacheKeyOutputMap[K] | undefined {
// Tier 1: the in-memory variable.
const memoized = inMemoryCache.get(key);
if (memoized !== undefined) {
if (cmd === undefined || memoized.cmd === cmd) {
return memoized.output as CommandCacheKeyOutputMap[K];
}
// If the memoized entry doesn't match the requested CLI,
// treat it as a miss and fall back to tier 2, the file.
inMemoryCache.delete(key);
}
// Tier 2: the temporary file persisted by an earlier step, if any.
const entry = readCommandCacheFile()[key] as unknown;
if (
!json.isObject<StoredCommandCacheEntry>(entry) ||
!json.isString(entry.cmd) ||
(cmd !== undefined && entry.cmd !== cmd) ||
!commandCacheValidators[key](entry.output)
) {
logger.warning("Received invalid data from the command-cache file.");
return undefined;
}
// Memoize so subsequent lookups in this process hit tier 1.
const cachedEntry = entry as StoredCommandCacheEntry;
const output = cachedEntry.output as CommandCacheKeyOutputMap[K];
cacheCommandOutput(key, cachedEntry.cmd, output);
return output;
}
/**
* 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();
}
+36 -38
View File
@@ -12,6 +12,14 @@ import {
runTool,
} from "./actions-util";
import * as api from "./api-client";
import {
cacheCommandOutput,
CommandCacheKey,
getCachedCommandOutput,
type CommandCacheKeyOutputMap,
type ResolveLanguagesOutput,
type VersionInfo,
} from "./cli/output-cache";
import { CliError, wrapCliConfigurationError } from "./cli-errors";
import { appendExtraQueryExclusions, type Config } from "./config-utils";
import { DocUrl } from "./doc-url";
@@ -216,36 +224,10 @@ export interface CodeQL {
): Promise<void>;
}
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
* version as the CodeQL CLI, we can use the CodeQL CLI to perform overlay
* analysis with that pack. Otherwise, if the overlay versions are different,
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
export interface ResolveDatabaseOutput {
overlayBaseSpecifier?: string;
}
export interface ResolveLanguagesOutput {
aliases?: {
[alias: string]: string;
};
extractors: {
[language: string]: Array<{
extractor_root: string;
extractor_options?: any;
}>;
};
}
export interface ResolveBuildEnvironmentOutput {
configuration?: {
[language: string]: {
@@ -495,6 +477,29 @@ export async function getCodeQLForTesting(
return getCodeQLForCmd(cmd, false);
}
/**
* Returns the cached output for a `codeql` command, resolving through the three
* caching tiers in order: the in-memory memo, then the temporary file (both via
* {@link getCachedCommandOutput}), and finally the CLI itself by invoking `run`
* on a miss and persisting its result back into the first two tiers.
*
* @param key The cache key identifying the command's output.
* @param cmd The path to the CodeQL CLI the output is obtained from.
* @param run Invokes the CLI to compute the output when it isn't cached.
*/
async function getCachedOrRun<K extends CommandCacheKey>(
key: K,
cmd: string,
run: () => Promise<CommandCacheKeyOutputMap[K]>,
): Promise<CommandCacheKeyOutputMap[K]> {
let result = getCachedCommandOutput(key, cmd);
if (result === undefined) {
result = await run();
cacheCommandOutput(key, cmd, result);
}
return result;
}
/**
* Return a CodeQL object for CodeQL CLI access.
*
@@ -512,18 +517,11 @@ async function getCodeQLForCmd(
return cmd;
},
async getVersion() {
let result = util.getCachedCodeQlVersion(cmd);
if (result === undefined) {
result = await runCliJson<VersionInfo>(
cmd,
["version", "--format=json"],
{
noStreamStdout: true,
},
);
util.cacheCodeQlVersion(cmd, result);
}
return result;
return getCachedOrRun(CommandCacheKey.Version, cmd, () => {
return runCliJson<VersionInfo>(cmd, ["version", "--format=json"], {
noStreamStdout: true,
});
});
},
async printVersion() {
// Reuse the cached version information rather than invoking the CLI again.
-6
View File
@@ -29,12 +29,6 @@ export enum EnvVar {
*/
CODE_SCANNING_REF = "CODE_SCANNING_REF",
/**
* `PersistedVersionInfo` for the CodeQL CLI, so later Actions steps can reuse it instead of
* invoking `codeql version` again.
*/
CODEQL_VERSION_INFO = "CODEQL_ACTION_CLI_VERSION_INFO",
/** Whether the CodeQL Action has invoked the Go autobuilder. */
DID_AUTOBUILD_GOLANG = "CODEQL_ACTION_DID_AUTOBUILD_GOLANG",
+8
View File
@@ -24,6 +24,7 @@ import {
getTotalCacheSize,
shouldRestoreCache,
} from "./caching-utils";
import { writeCommandCacheFile } from "./cli/output-cache";
import { CodeQL } from "./codeql";
import { getConfigFileInput } from "./config/file";
import * as configUtils from "./config-utils";
@@ -326,6 +327,10 @@ async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
toolsSource = initCodeQLResult.toolsSource;
zstdAvailability = initCodeQLResult.zstdAvailability;
// Populate the in-memory command cache with CLI version.
// This result will be persisted to disk at the end of the init action.
await codeql.getVersion();
// Check the workflow for problems. If there are any problems, they are reported
// to the workflow log. No exceptions are thrown.
await checkWorkflow(logger, codeql);
@@ -769,6 +774,9 @@ async function run(actionState: ActionState<["Logger", "Env", "Actions"]>) {
core.setOutput("codeql-path", config.codeQLCmd);
core.setOutput("codeql-version", (await codeql.getVersion()).version);
// Persist the command cache to disk at the end of a successful init.
writeCommandCacheFile();
} catch (unwrappedError) {
const error = wrapError(unwrappedError);
core.setFailed(error.message);
+5 -2
View File
@@ -12,6 +12,7 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { CommandCacheKey, getCachedCommandOutput } from "./cli/output-cache";
import type { Config } from "./config/action-config";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
import type { DependencyCacheRestoreStatusReport } from "./dependency-caching";
@@ -25,7 +26,6 @@ import type { ToolsSource } from "./setup-codeql";
import {
ConfigurationError,
getRequiredEnvParam,
getCachedCodeQlVersion,
isInTestMode,
GITHUB_DOTCOM_URL,
DiskUsage,
@@ -295,7 +295,10 @@ export async function createStatusReportBase(
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion();
const codeQlCliVersion = getCachedCommandOutput(
CommandCacheKey.Version,
undefined,
);
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
const testingEnvironment = getTestingEnvironment();
// re-export the testing environment variable so that it is available to subsequent steps,
+6 -5
View File
@@ -18,6 +18,8 @@ import { AnalysisKind } from "./analyses";
import * as apiClient from "./api-client";
import { GitHubApiDetails } from "./api-client";
import { CachingKind } from "./caching-utils";
import type { VersionInfo } from "./cli/output-cache";
import { resetCachedCommandOutputs } from "./cli/output-cache";
import * as codeql from "./codeql";
import { Config } from "./config-utils";
import * as defaults from "./defaults.json";
@@ -38,7 +40,6 @@ import {
GitHubVariant,
GitHubVersion,
HTTPError,
resetCachedCodeQlVersion,
} from "./util";
export const SAMPLE_DOTCOM_API_DETAILS = {
@@ -108,9 +109,9 @@ export function setupTests(testFn: TestFn<any>) {
// unless the test explicitly sets one up.
codeql.setCodeQL({});
// Reset the in-process CodeQL version cache so that it doesn't leak between
// tests, which each represent a separate Actions step in production.
resetCachedCodeQlVersion();
// Reset the in-process CodeQL command-output cache so that it doesn't leak
// between tests, which each represent a separate Actions step in production.
resetCachedCommandOutputs();
// Replace stdout and stderr so we can record output during tests
t.context.testOutput = "";
@@ -782,7 +783,7 @@ export const makeVersionInfo = (
version: string,
features?: { [name: string]: boolean },
overlayVersion?: number,
): codeql.VersionInfo => ({
): VersionInfo => ({
version,
features,
overlayVersion,
+1 -1
View File
@@ -1,6 +1,6 @@
import * as semver from "semver";
import type { VersionInfo } from "./codeql";
import type { VersionInfo } from "./cli/output-cache";
export enum ToolsFeature {
BuiltinExtractorsSpecifyDefaultQueries = "builtinExtractorsSpecifyDefaultQueries",
-55
View File
@@ -532,58 +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",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0" },
});
t.deepEqual(util.getCachedCodeQlVersion("/path/to/codeql"), {
version: "2.20.0",
});
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted version from a different CLI",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
});
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
},
);
test.serial(
"getCachedCodeQlVersion ignores a malformed persisted value",
(t) => {
process.env[EnvVar.CODEQL_VERSION_INFO] = "not valid json";
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined);
},
);
test.serial(
"getCachedCodeQlVersion ignores a persisted value with the wrong structure",
(t) => {
for (const value of [
JSON.stringify({ cmd: "/path/to/codeql" }),
JSON.stringify({ cmd: "/path/to/codeql", version: {} }),
JSON.stringify({ cmd: "/path/to/codeql", version: { version: 2 } }),
JSON.stringify({ version: { version: "2.20.0" } }),
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0", overlayVersion: "1" },
}),
JSON.stringify({
cmd: "/path/to/codeql",
version: { version: "2.20.0", features: "nope" },
}),
]) {
process.env[EnvVar.CODEQL_VERSION_INFO] = value;
t.is(util.getCachedCodeQlVersion("/path/to/codeql"), undefined, value);
}
},
);
+1 -85
View File
@@ -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 } from "./codeql";
import type { Pack } from "./config/db-config";
import type { Config } from "./config-utils";
import { EnvVar, getRequiredEnvParam } from "./environment";
@@ -598,90 +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;
version: VersionInfo;
}
function isVersionInfo(x: unknown): x is VersionInfo {
const candidate = x as Partial<VersionInfo> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.version === "string" &&
(candidate.features === undefined ||
(typeof candidate.features === "object" &&
candidate.features !== null)) &&
(candidate.overlayVersion === undefined ||
typeof candidate.overlayVersion === "number")
);
}
function isPersistedVersionInfo(x: unknown): x is PersistedVersionInfo {
const candidate = x as Partial<PersistedVersionInfo> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.cmd === "string" &&
isVersionInfo(candidate.version)
);
}
export function cacheCodeQlVersion(cmd: string, version: VersionInfo): 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.
core.exportVariable(
EnvVar.CODEQL_VERSION_INFO,
JSON.stringify({ cmd, version }),
);
}
export function getCachedCodeQlVersion(cmd?: string): 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.
const serialized = process.env[EnvVar.CODEQL_VERSION_INFO];
if (!serialized) {
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,