Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add getCodeQLVersionFromOverlayBaseDatabase()
  • Loading branch information
cklin committed Sep 26, 2025
commit 1b32ed334b084dfa0ec295fb67c71a40d89b3deb
144 changes: 144 additions & 0 deletions src/overlay-database-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getCacheRestoreKeyPrefix,
getCacheSaveKey,
getCacheWorkflowKeyPrefix,
getCodeQLVersionFromOverlayBaseDatabase,
OverlayDatabaseMode,
writeBaseDatabaseOidsFile,
writeOverlayChangesFile,
Expand Down Expand Up @@ -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<string> {
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",
);
});
61 changes: 60 additions & 1 deletion src/overlay-database-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -441,6 +442,64 @@ export async function downloadOverlayBaseDatabaseFromCache(
};
}

const IGNORE_DATABASES_OLDER_THAN_N_DAYS = 14;

export async function getCodeQLVersionFromOverlayBaseDatabase(
logger: Logger,
): Promise<string | undefined> {
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) {
Copy link

Copilot AI Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 9 for minimum key parts should be defined as a named constant to improve code maintainability and make the cache key format expectations clearer.

Copilot uses AI. Check for mistakes.
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.
Expand Down