mirror of
https://github.com/github/codeql-action.git
synced 2026-08-06 05:07:52 -05:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a35a8ac9ef | |||
| ecf4e52a5e | |||
| 0daa052859 | |||
| ec96b5271e | |||
| 8f1212fe5c | |||
| d4b99ad098 | |||
| 5110948a58 | |||
| 252ad66b0d | |||
| 7f4f27e7e6 | |||
| bba0d6c67c | |||
| da149812f1 | |||
| 0d698171d3 | |||
| c2f43799c5 | |||
| 41965b0e7b | |||
| b3c7aa7372 | |||
| 649bababe7 | |||
| 895ffe7eb5 | |||
| 805b59092e | |||
| d1bd5ec7b0 | |||
| 94b12602ac | |||
| 126166c212 | |||
| dd7ff30205 | |||
| 420c97eadd | |||
| defcf1bff6 | |||
| c8e32e423d | |||
| 553eef0d3f | |||
| b18df17ee7 | |||
| a602287f73 | |||
| dc8e1e9aa0 | |||
| 889ae42672 | |||
| 587fcb33c7 | |||
| 445107e23a | |||
| 6010f85d81 | |||
| 311292c28f |
Generated
+2038
-1970
File diff suppressed because it is too large
Load Diff
@@ -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 },
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user