diff --git a/lib/entry-points.js b/lib/entry-points.js index 56914e8848..ab6b83dab9 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146727,12 +146727,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"); } @@ -146741,23 +146762,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; @@ -146774,12 +146789,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 @@ -147323,7 +147336,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) { @@ -152343,7 +152359,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, @@ -152352,7 +152373,7 @@ async function getCodeQLForCmd(logger, cmd, checkVersion) { noStreamStdout: true } ); - cacheCodeQlVersion(getEnv(), cmd, result); + cacheCodeQlVersion(cacheFilePath, cmd, result); } return result; }, diff --git a/src/cli/output-cache.test.ts b/src/cli/output-cache.test.ts index d8d8629303..656cfd8201 100644 --- a/src/cli/output-cache.test.ts +++ b/src/cli/output-cache.test.ts @@ -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, ); }); diff --git a/src/cli/output-cache.ts b/src/cli/output-cache.ts index 8bf8c27abe..fb5deb1ad6 100644 --- a/src/cli/output-cache.ts +++ b/src/cli/output-cache.ts @@ -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; -} +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 & { + 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 | 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 | 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) ); } diff --git a/src/cli/types.ts b/src/cli/types.ts index ad48ff29b4..71d8d14c2f 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -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; + +/** + * The full type that describes the expected output of the `codeql version` command. + */ +export type VersionInfo = Omit & { + features?: { [name: string]: boolean }; +}; diff --git a/src/codeql.ts b/src/codeql.ts index 8f7e9e7445..bfa52d52d1 100644 --- a/src/codeql.ts +++ b/src/codeql.ts @@ -491,7 +491,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( cmd, @@ -500,7 +505,7 @@ async function getCodeQLForCmd( noStreamStdout: true, }, ); - outputCache.cacheCodeQlVersion(getEnv(), cmd, result); + outputCache.cacheCodeQlVersion(cacheFilePath, cmd, result); } return result; }, diff --git a/src/status-report.ts b/src/status-report.ts index e61b04f9dd..c5d15e1f16 100644 --- a/src/status-report.ts +++ b/src/status-report.ts @@ -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,