Merge pull request #4096 from github/mario-campos/use-json-module

Use `json` module for JSON validation in `output-cache`
This commit is contained in:
Mario Campos
2026-09-01 15:17:48 +00:00
committed by GitHub
6 changed files with 121 additions and 87 deletions

57
lib/entry-points.js generated
View File

@@ -146828,12 +146828,33 @@ function wrapApiConfigurationError(e) {
// src/cli/output-cache.ts
var fs3 = __toESM(require("fs"));
var import_path = __toESM(require("path"));
// src/cli/types.ts
var versionInfoBaseSchema = {
version: string,
features: optional(object({})),
/**
* 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: optional(number)
};
// src/cli/output-cache.ts
var outputCacheSchema = {
cmd: string,
entries: object({})
};
var COMMAND_CACHE_FILENAME = "codeql-action-command-cache.json";
var cachedCodeQlVersion = void 0;
function getCommandCacheFilePath(env) {
return import_path.default.join(getTemporaryDirectory(env), COMMAND_CACHE_FILENAME);
}
function cacheCodeQlVersion(env, cmd, version) {
function cacheCodeQlVersion(cacheFilePath, cmd, version) {
if (cachedCodeQlVersion !== void 0) {
throw new Error("cacheCodeQlVersion() should be called only once");
}
@@ -146842,23 +146863,17 @@ function cacheCodeQlVersion(env, cmd, version) {
cmd,
entries: { version }
};
fs3.writeFileSync(
getCommandCacheFilePath(env),
JSON.stringify(outputCache),
"utf8"
);
fs3.writeFileSync(cacheFilePath, JSON.stringify(outputCache), "utf8");
}
function getCachedCodeQlVersion(logger, env, cmd) {
function getCachedCodeQlVersion(logger, cacheFilePath, cmd) {
if (cachedCodeQlVersion !== void 0) {
return cachedCodeQlVersion;
}
let serialized;
try {
serialized = fs3.readFileSync(getCommandCacheFilePath(env), "utf8");
serialized = fs3.readFileSync(cacheFilePath, "utf8");
} catch (e) {
logger.debug(
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`
);
logger.debug(`Cannot read CLI-cache file ${cacheFilePath}: ${e}`);
return void 0;
}
let persisted;
@@ -146875,12 +146890,10 @@ function getCachedCodeQlVersion(logger, env, cmd) {
return cachedCodeQlVersion;
}
function isVersionInfo(x) {
const candidate = x;
return typeof candidate === "object" && candidate !== null && typeof candidate.version === "string" && (candidate.features === void 0 || typeof candidate.features === "object" && candidate.features !== null) && (candidate.overlayVersion === void 0 || typeof candidate.overlayVersion === "number");
return isObject(x) && validateSchema(versionInfoBaseSchema, x);
}
function isOutputCache(x) {
const candidate = x;
return typeof candidate === "object" && candidate !== null && typeof candidate.cmd === "string" && candidate.entries !== void 0 && isVersionInfo(candidate.entries.version);
return isObject(x) && validateSchema(outputCacheSchema, x) && isObject(x.entries) && isVersionInfo(x.entries.version);
}
// src/config/pack-registries.ts
@@ -147424,7 +147437,10 @@ async function createStatusReportBase(actionName, status, actionStartedAt, confi
core7.exportVariable("CODEQL_WORKFLOW_STARTED_AT" /* WORKFLOW_STARTED_AT */, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion(logger, getEnv());
const codeQlCliVersion = getCachedCodeQlVersion(
logger,
getCommandCacheFilePath(getEnv())
);
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
const testingEnvironment = getTestingEnvironment();
if (testingEnvironment) {
@@ -152512,7 +152528,12 @@ async function getCodeQLForCmd(logger, cmd, checkVersion) {
return cmd;
},
async getVersion() {
let result = getCachedCodeQlVersion(logger, getEnv(), cmd);
const cacheFilePath = getCommandCacheFilePath(getEnv());
let result = getCachedCodeQlVersion(
logger,
cacheFilePath,
cmd
);
if (result === void 0) {
result = await runCliJson(
cmd,
@@ -152521,7 +152542,7 @@ async function getCodeQLForCmd(logger, cmd, checkVersion) {
noStreamStdout: true
}
);
cacheCodeQlVersion(getEnv(), cmd, result);
cacheCodeQlVersion(cacheFilePath, cmd, result);
}
return result;
},

View File

@@ -3,12 +3,11 @@ import path from "path";
import test from "ava";
import { EnvVar } from "../environment";
import { getRunnerLogger } from "../logging";
import { getTestEnv, setupTests } from "../testing-utils";
import { setupTests } from "../testing-utils";
import * as util from "../util";
import * as outputCache from "./output-cache";
import { getCachedCodeQlVersion } from "./output-cache";
setupTests(test);
@@ -18,18 +17,18 @@ 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");
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(
cacheFile,
cacheFilePath,
JSON.stringify({
cmd: "/path/to/codeql",
entries: { version: { version: "2.20.0" } },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.deepEqual(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
{
version: "2.20.0",
},
@@ -42,18 +41,17 @@ 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");
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(
cacheFile,
cacheFilePath,
JSON.stringify({
cmd: "/path/to/other-codeql",
version: { version: "2.20.0" },
entries: { version: { version: "2.20.0" } },
}),
"utf8",
);
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});
@@ -64,11 +62,10 @@ 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 });
const cacheFilePath = path.join(tmpDir, "cache.json");
fs.writeFileSync(cacheFilePath, "not valid json", "utf8");
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});
@@ -79,9 +76,7 @@ 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 cacheFilePath = path.join(tmpDir, "cache.json");
const testValues = [
{ cmd: "/path/to/codeql" },
{ entries: { version: { version: "2.20.0" } } },
@@ -104,9 +99,9 @@ test.serial(
].map((v) => JSON.stringify(v));
for (const value of testValues) {
fs.writeFileSync(cacheFile, value, "utf8");
fs.writeFileSync(cacheFilePath, value, "utf8");
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
value,
);
@@ -117,10 +112,10 @@ test.serial(
test.serial("getCachedCodeQlVersion ignores non-existent file", async (t) => {
await util.withTmpDir(async (tmpDir: string) => {
const env = getTestEnv({ [EnvVar.TEMP]: tmpDir });
const cacheFilePath = path.join(tmpDir, "cache.json");
t.notThrows(() => {
t.is(
outputCache.getCachedCodeQlVersion(logger, env, "/path/to/codeql"),
getCachedCodeQlVersion(logger, cacheFilePath, "/path/to/codeql"),
undefined,
);
});

View File

@@ -3,9 +3,10 @@ import path from "path";
import { getTemporaryDirectory } from "../actions-util";
import { Env } from "../environment";
import * as json from "../json";
import { Logger } from "../logging";
import type { VersionInfo } from "./types";
import { VersionInfo, versionInfoBaseSchema } from "./types";
/**
* The keys of the command cache. Each key corresponds to a command whose output we cache.
@@ -13,12 +14,19 @@ import type { VersionInfo } from "./types";
export type CommandCacheKey = string;
/**
* The type of the command cache that is persisted to disk.
* The JSON schema of the command cache that is persisted to disk.
*/
export interface OutputCache {
cmd: string;
entries: Record<CommandCacheKey, unknown>;
}
const outputCacheSchema = {
cmd: json.string,
entries: json.object({}),
} as const satisfies json.Schema;
/**
* The type that describes the command cache that is persisted to disk.
*/
export type OutputCache = json.FromSchema<typeof outputCacheSchema> & {
entries: { version: VersionInfo };
};
/**
* The name of the temporary file that backs the on-disk cache of
@@ -43,18 +51,18 @@ export function resetCachedCodeQlVersion(): void {
* Returns the path to the temporary file that backs the
* on-disk cache of CLI responses between workflow steps.
*/
function getCommandCacheFilePath(env: Env): string {
export 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 env The environment variables to use.
* @param cacheFilePath The path to the cache file.
* @param cmd The path to the CodeQL CLI.
* @param version The version information to cache.
*/
export function cacheCodeQlVersion(
env: Env,
cacheFilePath: string,
cmd: string,
version: VersionInfo,
): void {
@@ -70,22 +78,18 @@ export function cacheCodeQlVersion(
// 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(outputCache),
"utf8",
);
fs.writeFileSync(cacheFilePath, JSON.stringify(outputCache), "utf8");
}
/**
* Returns the cached CodeQL CLI version, if any.
* @param logger The logger to use for logging messages.
* @param env The environment variables to use.
* @param cacheFilePath The path to the cache file.
* @param cmd The path to the CodeQL CLI.
*/
export function getCachedCodeQlVersion(
logger: Logger,
env: Env,
cacheFilePath: string,
cmd?: string,
): undefined | VersionInfo {
if (cachedCodeQlVersion !== undefined) {
@@ -96,11 +100,9 @@ export function getCachedCodeQlVersion(
// invokes `codeql version` instead.
let serialized: string;
try {
serialized = fs.readFileSync(getCommandCacheFilePath(env), "utf8");
serialized = fs.readFileSync(cacheFilePath, "utf8");
} catch (e) {
logger.debug(
`Cannot read CLI-cache file ${getCommandCacheFilePath(env)}: ${e}`,
);
logger.debug(`Cannot read CLI-cache file ${cacheFilePath}: ${e}`);
return undefined;
}
let persisted: unknown;
@@ -127,17 +129,7 @@ export function getCachedCodeQlVersion(
* @param x The value to test
*/
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")
);
return json.isObject(x) && json.validateSchema(versionInfoBaseSchema, x);
}
/**
@@ -145,12 +137,10 @@ function isVersionInfo(x: unknown): x is VersionInfo {
* @param x The value to test
*/
function isOutputCache(x: unknown): x is OutputCache {
const candidate = x as Partial<OutputCache> | null;
return (
typeof candidate === "object" &&
candidate !== null &&
typeof candidate.cmd === "string" &&
candidate.entries !== undefined &&
isVersionInfo(candidate.entries.version)
json.isObject(x) &&
json.validateSchema(outputCacheSchema, x) &&
json.isObject<{ version: unknown }>(x.entries) &&
isVersionInfo(x.entries.version)
);
}

View File

@@ -1,6 +1,11 @@
export interface VersionInfo {
version: string;
features?: { [name: string]: boolean };
import * as json from "../json";
/**
* The JSON schema of the expected output of the `codeql version` command.
*/
export const versionInfoBaseSchema = {
version: json.string,
features: json.optional(json.object({})),
/**
* The overlay version helps deal with backward incompatible changes for
* overlay analysis. When a precompiled query pack reports the same overlay
@@ -9,5 +14,17 @@ export interface VersionInfo {
* or if either the pack or the CLI does not report an overlay version,
* we need to revert to non-overlay analysis.
*/
overlayVersion?: number;
}
overlayVersion: json.optional(json.number),
} as const satisfies json.Schema;
/**
* The base type that describes the expected output of the `codeql version` command.
*/
export type VersionInfoBase = json.FromSchema<typeof versionInfoBaseSchema>;
/**
* The full type that describes the expected output of the `codeql version` command.
*/
export type VersionInfo = Omit<VersionInfoBase, "features"> & {
features?: { [name: string]: boolean };
};

View File

@@ -510,7 +510,12 @@ async function getCodeQLForCmd(
return cmd;
},
async getVersion() {
let result = outputCache.getCachedCodeQlVersion(logger, getEnv(), cmd);
const cacheFilePath = outputCache.getCommandCacheFilePath(getEnv());
let result = outputCache.getCachedCodeQlVersion(
logger,
cacheFilePath,
cmd,
);
if (result === undefined) {
result = await runCliJson<VersionInfo>(
cmd,
@@ -519,7 +524,7 @@ async function getCodeQLForCmd(
noStreamStdout: true,
},
);
outputCache.cacheCodeQlVersion(getEnv(), cmd, result);
outputCache.cacheCodeQlVersion(cacheFilePath, cmd, result);
}
return result;
},

View File

@@ -14,7 +14,10 @@ import {
isSelfHostedRunner,
} from "./actions-util";
import { getAnalysisKey, getApiClient } from "./api-client";
import { getCachedCodeQlVersion } from "./cli/output-cache";
import {
getCachedCodeQlVersion,
getCommandCacheFilePath,
} from "./cli/output-cache";
import type { Config } from "./config/action-config";
import type { ComputedInput, InputName } from "./config/inputs";
import { parseRegistriesWithoutCredentials } from "./config/pack-registries";
@@ -376,7 +379,10 @@ export async function createStatusReportBase(
core.exportVariable(EnvVar.WORKFLOW_STARTED_AT, workflowStartedAt);
}
const runnerOs = getRequiredEnvParam("RUNNER_OS");
const codeQlCliVersion = getCachedCodeQlVersion(logger, getEnv());
const codeQlCliVersion = getCachedCodeQlVersion(
logger,
getCommandCacheFilePath(getEnv()),
);
const actionRef = process.env["GITHUB_ACTION_REF"] || "";
const testingEnvironment = getTestingEnvironment();
// re-export the testing environment variable so that it is available to subsequent steps,