diff --git a/src/overlay-database-utils.test.ts b/src/overlay-database-utils.test.ts index 8a5ab96e8..ab92ce869 100644 --- a/src/overlay-database-utils.test.ts +++ b/src/overlay-database-utils.test.ts @@ -14,6 +14,7 @@ import { getCacheRestoreKeyPrefix, getCacheSaveKey, getCacheWorkflowKeyPrefix, + getCodeQLVersionFromOverlayBaseDatabase, OverlayDatabaseMode, writeBaseDatabaseOidsFile, writeOverlayChangesFile, @@ -315,3 +316,146 @@ test("overlay-base database cache keys remain stable", async (t) => { `Expected restore key prefix "${restoreKeyPrefix}" to start with workflow key prefix "${workflowKeyPrefix}"`, ); }); + +/** + * Helper function to generate a cache save key for testing. + * Sets up the necessary sinon stubs and returns the generated cache key. + */ +async function generateTestCacheKey(codeQlVersion: string): Promise { + const config = createTestConfig({ languages: ["python", "javascript"] }); + const commitOid = "abc123def456"; + + sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/"); + sinon.stub(gitUtils, "getCommitOid").resolves(commitOid); + + return await getCacheSaveKey(config, codeQlVersion, "checkout-path"); +} + +/** + * Helper function to stub getMostRecentActionsCacheEntry with a given key and creation date. + * Returns the stubbed function for cleanup if needed. + */ +function stubMostRecentActionsCacheEntry(key?: string, createdAt?: Date) { + const cacheItem = + key !== undefined || createdAt !== undefined + ? { + key, + created_at: createdAt?.toISOString(), + } + : undefined; + + return sinon + .stub(apiClient, "getMostRecentActionsCacheEntry") + .resolves(cacheItem); +} + +test("getCodeQLVersionFromOverlayBaseDatabase returns version when cache entry is valid", async (t) => { + const logger = getRunnerLogger(true); + const cacheKey = await generateTestCacheKey("2.23.0"); + + stubMostRecentActionsCacheEntry(cacheKey, new Date()); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is(result, "2.23.0", "Should return the extracted CodeQL version"); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when no cache entries found", async (t) => { + const logger = getRunnerLogger(true); + + sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/"); + stubMostRecentActionsCacheEntry(); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when no cache entries found", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when cache entry is too old", async (t) => { + const logger = getRunnerLogger(true); + const cacheKey = await generateTestCacheKey("2.23.0"); + + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 15); // 15 days ago (older than 14 day limit) + + stubMostRecentActionsCacheEntry(cacheKey, oldDate); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when cache entry is too old", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when cache key format is invalid", async (t) => { + const logger = getRunnerLogger(true); + + sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/"); + stubMostRecentActionsCacheEntry("invalid-key-format", new Date()); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when cache key format is invalid", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when CodeQL version is invalid semver", async (t) => { + const logger = getRunnerLogger(true); + const invalidCacheKey = await generateTestCacheKey("invalid.version"); + + stubMostRecentActionsCacheEntry(invalidCacheKey, new Date()); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when CodeQL version is invalid semver", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when CodeQL version is too old", async (t) => { + const logger = getRunnerLogger(true); + const cacheKey = await generateTestCacheKey("2.20.0"); // Older than minimum required version (2.22.4) + + stubMostRecentActionsCacheEntry(cacheKey, new Date()); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when CodeQL version is older than minimum required version", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when cache entry has no key", async (t) => { + const logger = getRunnerLogger(true); + + sinon.stub(apiClient, "getAutomationID").resolves("test-automation-id/"); + stubMostRecentActionsCacheEntry(undefined, new Date()); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when cache entry has no key", + ); +}); + +test("getCodeQLVersionFromOverlayBaseDatabase returns undefined when cache entry has no created_at", async (t) => { + const logger = getRunnerLogger(true); + const cacheKey = await generateTestCacheKey("2.23.0"); + + stubMostRecentActionsCacheEntry(cacheKey, undefined); + + const result = await getCodeQLVersionFromOverlayBaseDatabase(logger); + t.is( + result, + undefined, + "Should return undefined when cache entry has no created_at", + ); +}); diff --git a/src/overlay-database-utils.ts b/src/overlay-database-utils.ts index b4904e976..992caee16 100644 --- a/src/overlay-database-utils.ts +++ b/src/overlay-database-utils.ts @@ -3,9 +3,10 @@ import * as fs from "fs"; import * as path from "path"; import * as actionsCache from "@actions/cache"; +import * as semver from "semver"; import { getRequiredInput, getTemporaryDirectory } from "./actions-util"; -import { getAutomationID } from "./api-client"; +import { getAutomationID, getMostRecentActionsCacheEntry } from "./api-client"; import { type CodeQL } from "./codeql"; import { type Config } from "./config-utils"; import { getCommitOid, getFileOidsUnderPath } from "./git-utils"; @@ -441,6 +442,64 @@ export async function downloadOverlayBaseDatabaseFromCache( }; } +const IGNORE_DATABASES_OLDER_THAN_N_DAYS = 14; + +export async function getCodeQLVersionFromOverlayBaseDatabase( + logger: Logger, +): Promise { + const keyPrefix = await getCacheWorkflowKeyPrefix(); + const cacheItem = await getMostRecentActionsCacheEntry(keyPrefix); + + if (cacheItem?.created_at === undefined || cacheItem.key === undefined) { + logger.info("No overlay-base database cache entries found"); + return undefined; + } + + const cutoffTime = new Date(); + cutoffTime.setDate(cutoffTime.getDate() - IGNORE_DATABASES_OLDER_THAN_N_DAYS); + + const cacheCreationTime = new Date(cacheItem.created_at); + if (cacheCreationTime < cutoffTime) { + logger.info( + `Not considering overlay-base database cache entry ${cacheItem.key} ` + + `because it is too old (created at ${cacheItem.created_at})`, + ); + return undefined; + } + + const keyParts = cacheItem.key.split("-"); + if (keyParts.length < 9) { + logger.info( + `Overlay-base database cache entry ${cacheItem.key} has invalid key format`, + ); + return undefined; + } + const codeQlVersion = keyParts[keyParts.length - 2]; + + if (!semver.valid(codeQlVersion)) { + logger.info( + `Overlay-base database cache entry ${cacheItem.key} has invalid ` + + `CodeQL version ${codeQlVersion}`, + ); + return undefined; + } + + if (semver.lt(codeQlVersion, CODEQL_OVERLAY_MINIMUM_VERSION)) { + logger.info( + `Overlay-base database cache entry ${cacheItem.key} has ` + + `CodeQL version ${codeQlVersion}, which is older than the ` + + `minimum required version ${CODEQL_OVERLAY_MINIMUM_VERSION}`, + ); + return undefined; + } + + logger.info( + `Found overlay-base database cache entry ${cacheItem.key} ` + + `created at ${cacheItem.created_at} with CodeQL version ${codeQlVersion}`, + ); + return codeQlVersion; +} + /** * Computes the cache key for saving the overlay-base database to the GitHub * Actions cache.