diff --git a/lib/analyze-action.js b/lib/analyze-action.js index 6d7e6e19c..a95d9b12f 100644 --- a/lib/analyze-action.js +++ b/lib/analyze-action.js @@ -88183,9 +88183,10 @@ async function getTotalCacheSize(paths, logger, quiet = false) { function shouldStoreCache(kind) { return kind === "full" /* Full */ || kind === "store" /* Store */; } +var cacheKeyHashLength = 16; function createCacheKeyHash(components) { const componentsJson = JSON.stringify(components); - return crypto.createHash("sha256").update(componentsJson).digest("hex").substring(0, 16); + return crypto.createHash("sha256").update(componentsJson).digest("hex").substring(0, cacheKeyHashLength); } // src/config/db-config.ts @@ -91204,6 +91205,29 @@ async function cacheKey2(codeql, features, language, patterns) { const hash2 = await glob.hashFiles(patterns.join("\n")); return `${await cachePrefix2(codeql, features, language)}${hash2}`; } +async function getFeaturePrefix(codeql, features, language) { + const enabledFeatures = []; + const addFeatureIfEnabled = async (feature) => { + if (await features.getValue(feature, codeql)) { + enabledFeatures.push(feature); + } + }; + if (language === "java" /* java */) { + const minimizeJavaJars = await features.getValue( + "java_minimize_dependency_jars" /* JavaMinimizeDependencyJars */, + codeql + ); + if (minimizeJavaJars) { + return "minify-"; + } + } else if (language === "csharp" /* csharp */) { + await addFeatureIfEnabled("csharp_new_cache_key" /* CsharpNewCacheKey */); + } + if (enabledFeatures.length > 0) { + return `${createCacheKeyHash(enabledFeatures)}-`; + } + return ""; +} async function cachePrefix2(codeql, features, language) { const runnerOs = getRequiredEnvParam("RUNNER_OS"); const customPrefix = process.env["CODEQL_ACTION_DEPENDENCY_CACHE_PREFIX" /* DEPENDENCY_CACHING_PREFIX */]; @@ -91211,14 +91235,8 @@ async function cachePrefix2(codeql, features, language) { if (customPrefix !== void 0 && customPrefix.length > 0) { prefix = `${prefix}-${customPrefix}`; } - const minimizeJavaJars = await features.getValue( - "java_minimize_dependency_jars" /* JavaMinimizeDependencyJars */, - codeql - ); - if (language === "java" /* java */ && minimizeJavaJars) { - prefix = `minify-${prefix}`; - } - return `${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; + const featurePrefix = await getFeaturePrefix(codeql, features, language); + return `${featurePrefix}${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; } // src/diagnostics.ts diff --git a/lib/init-action.js b/lib/init-action.js index 380c1f2ab..0bb4376ea 100644 --- a/lib/init-action.js +++ b/lib/init-action.js @@ -85266,9 +85266,10 @@ function getCachingKind(input) { return "none" /* None */; } } +var cacheKeyHashLength = 16; function createCacheKeyHash(components) { const componentsJson = JSON.stringify(components); - return crypto.createHash("sha256").update(componentsJson).digest("hex").substring(0, 16); + return crypto.createHash("sha256").update(componentsJson).digest("hex").substring(0, cacheKeyHashLength); } function getDependencyCachingEnabled() { const dependencyCaching = getOptionalInput("dependency-caching") || process.env["CODEQL_ACTION_DEPENDENCY_CACHING" /* DEPENDENCY_CACHING */]; @@ -87379,6 +87380,29 @@ async function cacheKey2(codeql, features, language, patterns) { const hash = await glob.hashFiles(patterns.join("\n")); return `${await cachePrefix2(codeql, features, language)}${hash}`; } +async function getFeaturePrefix(codeql, features, language) { + const enabledFeatures = []; + const addFeatureIfEnabled = async (feature) => { + if (await features.getValue(feature, codeql)) { + enabledFeatures.push(feature); + } + }; + if (language === "java" /* java */) { + const minimizeJavaJars = await features.getValue( + "java_minimize_dependency_jars" /* JavaMinimizeDependencyJars */, + codeql + ); + if (minimizeJavaJars) { + return "minify-"; + } + } else if (language === "csharp" /* csharp */) { + await addFeatureIfEnabled("csharp_new_cache_key" /* CsharpNewCacheKey */); + } + if (enabledFeatures.length > 0) { + return `${createCacheKeyHash(enabledFeatures)}-`; + } + return ""; +} async function cachePrefix2(codeql, features, language) { const runnerOs = getRequiredEnvParam("RUNNER_OS"); const customPrefix = process.env["CODEQL_ACTION_DEPENDENCY_CACHE_PREFIX" /* DEPENDENCY_CACHING_PREFIX */]; @@ -87386,14 +87410,8 @@ async function cachePrefix2(codeql, features, language) { if (customPrefix !== void 0 && customPrefix.length > 0) { prefix = `${prefix}-${customPrefix}`; } - const minimizeJavaJars = await features.getValue( - "java_minimize_dependency_jars" /* JavaMinimizeDependencyJars */, - codeql - ); - if (language === "java" /* java */ && minimizeJavaJars) { - prefix = `minify-${prefix}`; - } - return `${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; + const featurePrefix = await getFeaturePrefix(codeql, features, language); + return `${featurePrefix}${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; } // src/diagnostics.ts diff --git a/src/caching-utils.ts b/src/caching-utils.ts index a980ce680..33dac7cfb 100644 --- a/src/caching-utils.ts +++ b/src/caching-utils.ts @@ -73,6 +73,9 @@ export function getCachingKind(input: string | undefined): CachingKind { } } +// The length to which `createCacheKeyHash` truncates hash strings. +export const cacheKeyHashLength = 16; + /** * Creates a SHA-256 hash of the cache key components to ensure uniqueness * while keeping the cache key length manageable. @@ -94,7 +97,7 @@ export function createCacheKeyHash(components: Record): string { .createHash("sha256") .update(componentsJson) .digest("hex") - .substring(0, 16); + .substring(0, cacheKeyHashLength); } /** Determines whether dependency caching is enabled. */ diff --git a/src/dependency-caching.test.ts b/src/dependency-caching.test.ts new file mode 100644 index 000000000..7cf174c03 --- /dev/null +++ b/src/dependency-caching.test.ts @@ -0,0 +1,68 @@ +import test from "ava"; +// import * as sinon from "sinon"; + +import { cacheKeyHashLength } from "./caching-utils"; +import { createStubCodeQL } from "./codeql"; +import { getFeaturePrefix } from "./dependency-caching"; +import { Feature } from "./feature-flags"; +import { KnownLanguage } from "./languages"; +import { setupTests, createFeatures } from "./testing-utils"; + +setupTests(test); + +test("getFeaturePrefix - returns empty string if no features are enabled", async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([]); + + for (const knownLanguage of Object.values(KnownLanguage)) { + const result = await getFeaturePrefix(codeql, features, knownLanguage); + t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`); + } +}); + +test("getFeaturePrefix - Java - returns 'minify-' if JavaMinimizeDependencyJars is enabled", async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.JavaMinimizeDependencyJars]); + + const result = await getFeaturePrefix(codeql, features, KnownLanguage.java); + t.deepEqual(result, "minify-"); +}); + +test("getFeaturePrefix - non-Java - returns '' if JavaMinimizeDependencyJars is enabled", async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.JavaMinimizeDependencyJars]); + + for (const knownLanguage of Object.values(KnownLanguage)) { + // Skip Java since we expect a result for it, which is tested in the previous test. + if (knownLanguage === KnownLanguage.java) { + continue; + } + const result = await getFeaturePrefix(codeql, features, knownLanguage); + t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`); + } +}); + +test("getFeaturePrefix - C# - returns prefix if CsharpNewCacheKey is enabled", async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpNewCacheKey]); + + const result = await getFeaturePrefix(codeql, features, KnownLanguage.csharp); + t.notDeepEqual(result, ""); + t.assert(result.endsWith("-")); + // Check the length of the prefix, which should correspond to `cacheKeyHashLength` + 1 for the trailing `-`. + t.is(result.length, cacheKeyHashLength + 1); +}); + +test("getFeaturePrefix - non-C# - returns '' if CsharpNewCacheKey is enabled", async (t) => { + const codeql = createStubCodeQL({}); + const features = createFeatures([Feature.CsharpNewCacheKey]); + + for (const knownLanguage of Object.values(KnownLanguage)) { + // Skip C# since we expect a result for it, which is tested in the previous test. + if (knownLanguage === KnownLanguage.csharp) { + continue; + } + const result = await getFeaturePrefix(codeql, features, knownLanguage); + t.deepEqual(result, "", `Expected no feature prefix for ${knownLanguage}`); + } +}); diff --git a/src/dependency-caching.ts b/src/dependency-caching.ts index bf2b0244b..6ee36a3bb 100644 --- a/src/dependency-caching.ts +++ b/src/dependency-caching.ts @@ -6,11 +6,11 @@ import * as glob from "@actions/glob"; import { getTemporaryDirectory } from "./actions-util"; import { listActionsCaches } from "./api-client"; -import { getTotalCacheSize } from "./caching-utils"; +import { createCacheKeyHash, getTotalCacheSize } from "./caching-utils"; import { CodeQL } from "./codeql"; import { Config } from "./config-utils"; import { EnvVar } from "./environment"; -import { Feature, Features } from "./feature-flags"; +import { Feature, FeatureEnablement, Features } from "./feature-flags"; import { KnownLanguage, Language } from "./languages"; import { Logger } from "./logging"; import { getErrorMessage, getRequiredEnvParam } from "./util"; @@ -442,6 +442,56 @@ async function cacheKey( return `${await cachePrefix(codeql, features, language)}${hash}`; } +/** + * If experimental features which the cache contents depend on are enabled for the current language, + * this function returns a prefix that uniquely identifies the set of enabled features. The purpose of + * this is to avoid restoring caches whose contents depended on experimental features, if those + * experimental features are later disabled. + * + * @param codeql The CodeQL instance. + * @param features Information about enabled features. + * @param language The language we are creating the key for. + * + * @returns A cache key prefix identifying the enabled, experimental features that the cache depends on. + */ +export async function getFeaturePrefix( + codeql: CodeQL, + features: FeatureEnablement, + language: Language, +): Promise { + const enabledFeatures: Feature[] = []; + + const addFeatureIfEnabled = async (feature: Feature) => { + if (await features.getValue(feature, codeql)) { + enabledFeatures.push(feature); + } + }; + + if (language === KnownLanguage.java) { + // To ensure a safe rollout of JAR minimization, we change the key when the feature is enabled. + const minimizeJavaJars = await features.getValue( + Feature.JavaMinimizeDependencyJars, + codeql, + ); + + // To maintain backwards compatibility with this, we return "minify-" instead of a hash. + if (minimizeJavaJars) { + return "minify-"; + } + } else if (language === KnownLanguage.csharp) { + await addFeatureIfEnabled(Feature.CsharpNewCacheKey); + } + + // If any features that affect the cache are enabled, return a feature prefix by + // computing a hash of the feature array. + if (enabledFeatures.length > 0) { + return `${createCacheKeyHash(enabledFeatures)}-`; + } + + // No feature prefix. + return ""; +} + /** * Constructs a prefix for the cache key, comprised of a CodeQL-specific prefix, a version number that * can be changed to invalidate old caches, the runner's operating system, and the specified language name. @@ -464,16 +514,12 @@ async function cachePrefix( prefix = `${prefix}-${customPrefix}`; } - // To ensure a safe rollout of JAR minimization, we change the key when the feature is enabled. - const minimizeJavaJars = await features.getValue( - Feature.JavaMinimizeDependencyJars, - codeql, - ); - if (language === KnownLanguage.java && minimizeJavaJars) { - prefix = `minify-${prefix}`; - } + // Calculate the feature prefix for the cache, if any. This is a hash that identifies + // experimental features that affect the cache contents. + const featurePrefix = await getFeaturePrefix(codeql, features, language); - return `${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; + // Assemble the cache key. + return `${featurePrefix}${prefix}-${CODEQL_DEPENDENCY_CACHE_VERSION}-${runnerOs}-${language}-`; } /** Represents information about our overall cache usage for CodeQL dependency caches. */