Merge pull request #4145 from github/henrymercer/bundle-resolution-pr

Separate CodeQL bundle resolution from installation
This commit is contained in:
Henry Mercer
2026-09-17 14:31:25 +00:00
committed by GitHub
4 changed files with 455 additions and 189 deletions

91
lib/entry-points.js generated
View File

@@ -151967,7 +151967,7 @@ async function getCodeQLBundleDownloadURL(tagName, apiDetails, compressionMethod
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
}
function tryGetBundleVersionFromTagName(tagName, logger) {
const match2 = tagName.match(/^codeql-bundle-(.*)$/);
const match2 = tagName.match(/^codeql-bundle-(.+)$/);
if (match2 === null || match2.length < 2) {
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
return void 0;
@@ -152207,7 +152207,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO
url2 = toolsInput;
if (tagName) {
const bundleVersion3 = tryGetBundleVersionFromTagName(tagName, logger);
if (bundleVersion3 && semver9.valid(bundleVersion3)) {
if (bundleVersion3 !== void 0 && semver9.valid(bundleVersion3)) {
cliVersion2 = convertToSemVer(bundleVersion3, logger);
}
}
@@ -152222,8 +152222,9 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO
cliVersion2 = version.cliVersion;
tagName = version.tagName;
}
const bundleVersion2 = tagName && tryGetBundleVersionFromTagName(tagName, logger);
const humanReadableVersion = cliVersion2 ?? (bundleVersion2 && convertToSemVer(bundleVersion2, logger)) ?? tagName ?? url2 ?? "unknown";
const bundleVersion2 = tagName !== void 0 ? tryGetBundleVersionFromTagName(tagName, logger) : void 0;
const resolvedVersion = cliVersion2 ?? (bundleVersion2 !== void 0 ? convertToSemVer(bundleVersion2, logger) : void 0);
const humanReadableVersion = resolvedVersion ?? tagName ?? url2 ?? "unknown";
logger.debug(
`Attempting to obtain CodeQL tools. CLI version: ${cliVersion2 ?? "unknown"}, bundle tag name: ${tagName ?? "unknown"}, URL: ${url2 ?? "unspecified"}.`
);
@@ -152330,17 +152331,17 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO
logger.info(`Using CodeQL CLI sourced from ${url2} .`);
}
return {
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
bundle: { kind: "combined", url: url2 },
bundleVersion: bundleVersion2,
cliVersion: cliVersion2,
codeqlURL: url2,
compressionMethod,
sourceType: "download",
toolsVersion: cliVersion2 ?? humanReadableVersion
toolsVersion: resolvedVersion ?? "unknown"
};
}
async function tryGetFallbackToolcacheVersion(cliVersion2, tagName, logger) {
const bundleVersion2 = tryGetBundleVersionFromTagName(tagName, logger);
if (!bundleVersion2) {
if (bundleVersion2 === void 0) {
return void 0;
}
const fallbackVersion = convertToSemVer(bundleVersion2, logger);
@@ -152349,7 +152350,9 @@ async function tryGetFallbackToolcacheVersion(cliVersion2, tagName, logger) {
);
return fallbackVersion;
}
var downloadCodeQL = async function(codeqlURL, compressionMethod, maybeBundleVersion, maybeCliVersion, apiDetails, tarVersion, tempDir, features, logger) {
var downloadCodeQL = async function(source, apiDetails, tarVersion, tempDir, logger) {
const { bundle, compressionMethod } = source;
const codeqlURL = bundle.url;
const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers = {
@@ -152365,13 +152368,8 @@ var downloadCodeQL = async function(codeqlURL, compressionMethod, maybeBundleVer
codeqlURL
);
}
const toolcacheInfo = getToolcacheDestinationInfo(
maybeBundleVersion,
maybeCliVersion,
logger
);
const extractedBundlePath = toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
await tryDeleteToolcacheBundles({ env: getEnv(), features, logger });
const toolcacheDestination = getToolcacheDestination(source, logger);
const extractedBundlePath = toolcacheDestination ?? getTempExtractionDir(tempDir);
const statusReport = await downloadAndExtract(
codeqlURL,
compressionMethod,
@@ -152381,36 +152379,29 @@ var downloadCodeQL = async function(codeqlURL, compressionMethod, maybeBundleVer
tarVersion,
logger
);
if (!toolcacheInfo) {
if (toolcacheDestination) {
writeToolcacheMarkerFile(toolcacheDestination, logger);
} else {
logger.debug(
`Could not cache CodeQL tools because we could not determine the bundle version from the URL ${codeqlURL}.`
);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? "unknown"
};
}
writeToolcacheMarkerFile(toolcacheInfo.path, logger);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? toolcacheInfo.version
statusReport
};
};
function getToolcacheDestinationInfo(maybeBundleVersion, maybeCliVersion, logger) {
if (maybeBundleVersion) {
const version = getCanonicalToolcacheVersion(
maybeCliVersion,
maybeBundleVersion,
logger
);
return {
path: getToolcacheDirectory(version),
version
};
function getToolcacheDestination(source, logger) {
if (!source.bundleVersion) {
return void 0;
}
return void 0;
return getToolcacheDirectory(
getCanonicalToolcacheVersion(
source.cliVersion,
source.bundleVersion,
logger
)
);
}
async function tryDeleteToolcacheBundles({
env,
@@ -152461,7 +152452,6 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau
logger
);
let codeqlFolder;
let toolsVersion = source.toolsVersion;
let toolsDownloadStatusReport;
let toolsSource;
switch (source.sourceType) {
@@ -152482,18 +152472,13 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau
toolsSource = "TOOLCACHE" /* Toolcache */;
break;
case "download": {
const result = await downloadCodeQL(
source.codeqlURL,
source.compressionMethod,
source.bundleVersion,
source.cliVersion,
const result = await downloadCodeQLBundle(
{ env: getEnv(), features, logger },
source,
apiDetails,
zstdAvailability.version,
tempDir,
features,
logger
tempDir
);
toolsVersion = result.toolsVersion;
codeqlFolder = result.codeqlFolder;
toolsDownloadStatusReport = result.statusReport;
toolsSource = "DOWNLOAD" /* Download */;
@@ -152507,9 +152492,19 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defau
codeqlFolder,
toolsDownloadStatusReport,
toolsSource,
toolsVersion
toolsVersion: source.toolsVersion
};
}
async function downloadCodeQLBundle(action, source, apiDetails, tarVersion, tempDir) {
await tryDeleteToolcacheBundles(action);
return await downloadCodeQL(
source,
apiDetails,
tarVersion,
tempDir,
action.logger
);
}
async function useZstdBundle(cliVersion2, tarSupportsZstd) {
return (
// In testing, gzip performs better than zstd on Windows.

View File

@@ -90,7 +90,7 @@ async function installIntoToolcache({
tmpDir: string;
}) {
const url = mockBundleDownloadApi({ apiDetails, isPinned, tagName });
await codeql.setupCodeQL(
return await codeql.setupCodeQL(
cliVersion !== undefined ? undefined : url,
apiDetails,
tmpDir,
@@ -259,6 +259,65 @@ test.serial(
},
);
for (const { cliVersion, tagName, expectedToolcacheVersion } of [
{
cliVersion: "2.21.0",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "2.21.0",
},
{
cliVersion: "2.21.0-rc.1",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "0.0.0-20240101",
},
{
cliVersion: "2.21.0+20240101",
tagName: "codeql-bundle-20240101",
expectedToolcacheVersion: "0.0.0-20240101",
},
{
cliVersion: "2.21.0",
tagName: "custom-release",
expectedToolcacheVersion: undefined,
},
]) {
test.serial(
`preserves CLI version ${cliVersion} when installing ${tagName}`,
async (t) => {
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = await installIntoToolcache({
cliVersion,
isPinned: false,
tagName,
tmpDir,
});
t.is(result.toolsVersion, cliVersion);
t.is(result.toolsSource, ToolsSource.Download);
t.true(
Number.isInteger(result.toolsDownloadStatusReport?.totalDurationMs),
);
t.deepEqual(
toolcache.findAllVersions("CodeQL"),
expectedToolcacheVersion === undefined
? []
: [expectedToolcacheVersion],
);
if (expectedToolcacheVersion !== undefined) {
const cachedFolder = toolcache.find(
"CodeQL",
expectedToolcacheVersion,
);
t.truthy(cachedFolder);
t.true(fs.existsSync(`${cachedFolder}.complete`));
}
});
},
);
}
const EXPLICITLY_REQUESTED_BUNDLE_TEST_CASES = [
{
tagName: "codeql-bundle-2.17.6",

View File

@@ -10,7 +10,7 @@ import * as sinon from "sinon";
import * as actionsUtil from "./actions-util";
import * as api from "./api-client";
import * as diagnostics from "./diagnostics";
import { ActionsEnvVars, EnvVar, ReadOnlyEnv } from "./environment";
import { ActionsEnvVars, Env, EnvVar, ReadOnlyEnv } from "./environment";
import { Feature } from "./feature-flags";
import { getRunnerLogger } from "./logging";
import { getCacheRestoreKeyPrefix } from "./overlay/caching";
@@ -25,6 +25,8 @@ import {
createFeatures,
createTestConfig,
getRecordingLogger,
getTestEnv,
initAllState,
makeMacro,
mockBundleDownloadApi,
setupActionsVars,
@@ -34,6 +36,7 @@ import * as toolsDownload from "./tools-download";
import {
getErrorMessage,
GitHubVariant,
HTTPError,
initializeEnvironment,
withTmpDir,
} from "./util";
@@ -44,6 +47,15 @@ test.beforeEach(() => {
initializeEnvironment("1.2.3");
});
function stubDownloadAndExtract() {
return sinon
.stub(toolsDownload, "downloadAndExtract")
.callsFake(async (_url, _compressionMethod, dest) => {
fs.mkdirSync(dest, { recursive: true });
return { downloadDurationMs: 200, totalDurationMs: 300 };
});
}
test.serial("parse codeql bundle url version", (t) => {
t.deepEqual(
setupCodeql.getCodeQLURLVersion(
@@ -102,9 +114,9 @@ test.serial(
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const tagName = "codeql-bundle-v1.2.3";
mockBundleDownloadApi({ tagName });
const url = mockBundleDownloadApi({ tagName });
const source = await setupCodeql.getCodeQLSource(
`https://github.com/github/codeql-action/releases/download/${tagName}/codeql-bundle-linux64.tar.gz`,
url,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
false, // useOverlayAwareDefaultCliVersion
@@ -115,8 +127,14 @@ test.serial(
getRunnerLogger(true),
);
t.is(source.sourceType, "download");
t.is(source["cliVersion"], "1.2.3");
t.deepEqual(source, {
bundle: { kind: "combined", url },
bundleVersion: "v1.2.3",
cliVersion: "1.2.3",
compressionMethod: "gzip",
sourceType: "download",
toolsVersion: "1.2.3",
} satisfies setupCodeql.CodeQLDownloadSource);
});
},
);
@@ -198,7 +216,8 @@ for (const {
t.is(source.sourceType, "download");
if (source.sourceType === "download") {
t.is(source.compressionMethod, expectedCompressionMethod);
t.true(source.codeqlURL.endsWith(`/${expectedBundleName}`));
t.is(source.bundle.kind, "combined");
t.true(source.bundle.url.endsWith(`/${expectedBundleName}`));
}
});
},
@@ -252,23 +271,14 @@ test.serial(
const logger = getRecordingLogger(loggedMessages);
const features = createFeatures([]);
// Stub the downloadCodeQL function to prevent downloading artefacts
// during testing from being called.
sinon.stub(setupCodeql, "downloadCodeQL").resolves({
codeqlFolder: "codeql",
statusReport: {
downloadDurationMs: 200,
totalDurationMs: 300,
},
toolsVersion: LINKED_CLI_VERSION.cliVersion,
});
const extractStub = stubDownloadAndExtract();
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = await setupCodeql.setupCodeQLBundle(
"linked",
SAMPLE_DOTCOM_API_DETAILS,
"tmp/codeql_action_test/",
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
@@ -280,6 +290,15 @@ test.serial(
// Basic sanity check that the version we got back is indeed
// the linked (default) CLI version.
t.is(result.toolsVersion, LINKED_CLI_VERSION.cliVersion);
t.true(extractStub.calledOnce);
t.is(
result.codeqlFolder,
toolcache.find("CodeQL", LINKED_CLI_VERSION.cliVersion),
);
t.deepEqual(result.toolsDownloadStatusReport, {
downloadDurationMs: 200,
totalDurationMs: 300,
});
// Ensure message logging CodeQL CLI version was present in user logs.
const expected_message: string = `Using CodeQL CLI version ${LINKED_CLI_VERSION.cliVersion}`;
@@ -305,23 +324,14 @@ test.serial(
"https://github.com/github/codeql-action/releases/download/codeql-bundle-v2.16.0/codeql-bundle-linux64.tar.gz";
const expectedVersion = "2.16.0";
// Stub the downloadCodeQL function to prevent downloading artefacts
// during testing from being called.
sinon.stub(setupCodeql, "downloadCodeQL").resolves({
codeqlFolder: "codeql",
statusReport: {
downloadDurationMs: 200,
totalDurationMs: 300,
},
toolsVersion: expectedVersion,
});
const extractStub = stubDownloadAndExtract();
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = await setupCodeql.setupCodeQLBundle(
bundleUrl,
SAMPLE_DOTCOM_API_DETAILS,
"tmp/codeql_action_test/",
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
@@ -333,6 +343,13 @@ test.serial(
// Basic sanity check that the version we got back is indeed the version that the
// bundle contains..
t.is(result.toolsVersion, expectedVersion);
t.true(extractStub.calledOnce);
t.is(extractStub.firstCall.args[0], bundleUrl);
t.is(result.codeqlFolder, toolcache.find("CodeQL", expectedVersion));
t.deepEqual(result.toolsDownloadStatusReport, {
downloadDurationMs: 200,
totalDurationMs: 300,
});
// Ensure message logging CodeQL CLI version was present in user logs.
const expected_message: string = `Using CodeQL CLI version 2.16.0 sourced from ${bundleUrl} .`;
@@ -348,11 +365,12 @@ test.serial(
);
test.serial(
"getCodeQLSource correctly returns nightly CLI version when tools == nightly",
"getCodeQLSource and setupCodeQLBundle preserve the nightly version when tools == nightly",
async (t) => {
const loggedMessages: LoggedMessage[] = [];
const logger = getRecordingLogger(loggedMessages);
const features = createFeatures([]);
const extractStub = stubDownloadAndExtract();
const expectedDate = "30260213";
const expectedTag = `codeql-bundle-${expectedDate}`;
@@ -390,14 +408,34 @@ test.serial(
const expectedVersion = `0.0.0-${expectedDate}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`;
t.deepEqual(source, {
bundle: { kind: "combined", url: expectedURL },
bundleVersion: expectedDate,
cliVersion: undefined,
codeqlURL: expectedURL,
compressionMethod: "zstd",
sourceType: "download",
toolsVersion: expectedVersion,
} satisfies setupCodeql.CodeQLToolsSource);
const result = await setupCodeql.setupCodeQLBundle(
"nightly",
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
["javascript"],
false, // useOverlayAwareDefaultCliVersion
features,
logger,
);
t.true(extractStub.calledOnce);
t.is(extractStub.firstCall.args[0], expectedURL);
t.is(result.toolsVersion, source.toolsVersion);
t.is(result.toolsSource, setupCodeql.ToolsSource.Download);
t.is(result.codeqlFolder, toolcache.find("CodeQL", expectedVersion));
t.true(fs.existsSync(`${result.codeqlFolder}.complete`));
t.deepEqual(toolcache.findAllVersions("CodeQL"), [expectedVersion]);
// Afterwards, ensure that we see the expected messages in the log.
checkExpectedLogMessages(t, loggedMessages, [
"Using the latest CodeQL CLI nightly, as requested by 'tools: nightly'.",
@@ -453,9 +491,9 @@ test.serial(
const expectedVersion = `0.0.0-${expectedDate}`;
const expectedURL = `https://github.com/dsp-testing/codeql-cli-nightlies/releases/download/${expectedTag}/${setupCodeql.getCodeQLBundleName("zstd")}`;
t.deepEqual(source, {
bundle: { kind: "combined", url: expectedURL },
bundleVersion: expectedDate,
cliVersion: undefined,
codeqlURL: expectedURL,
compressionMethod: "zstd",
sourceType: "download",
toolsVersion: expectedVersion,
@@ -472,6 +510,91 @@ test.serial(
},
);
for (const bundlePath of [
"codeql-bundle.tar.gz",
"codeql-bundle.tar.zst",
"codeql-bundle-/codeql-bundle.tar.gz",
]) {
test.serial(
`setupCodeQLBundle reports an unknown version for ${bundlePath}`,
async (t) => {
const extractStub = stubDownloadAndExtract();
const downloadSpy = sinon.spy(setupCodeql, "downloadCodeQL");
const url = `https://example.com/${bundlePath}`;
const messages: LoggedMessage[] = [];
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
const result = await setupCodeql.setupCodeQLBundle(
url,
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
false, // useOverlayAwareDefaultCliVersion
createFeatures([]),
getRecordingLogger(messages),
);
t.true(extractStub.calledOnce);
t.is(extractStub.firstCall.args[0], url);
t.is(downloadSpy.firstCall.args[0].bundleVersion, undefined);
t.is(downloadSpy.firstCall.args[0].toolsVersion, "unknown");
t.is(result.toolsVersion, "unknown");
t.is(result.toolsSource, setupCodeql.ToolsSource.Download);
t.is(path.dirname(result.codeqlFolder), tmpDir);
t.true(fs.existsSync(result.codeqlFolder));
t.false(fs.existsSync(`${result.codeqlFolder}.complete`));
t.deepEqual(toolcache.findAllVersions("CodeQL"), []);
checkExpectedLogMessages(t, messages, [
`Using CodeQL CLI sourced from ${url}`,
]);
});
},
);
}
test.serial(
"setupCodeQLBundle preserves local installation without cleaning the toolcache",
async (t) => {
const cleanupSpy = sinon.spy(toolsDownload, "deleteToolcacheBundles");
const downloadSpy = sinon.spy(setupCodeql, "downloadCodeQL");
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
const cachedDirectory = createToolcacheEntry(
tmpDir,
"CodeQL",
CLEANUP_STALE_VERSION,
);
const result = await setupCodeql.setupCodeQLBundle(
path.join(__dirname, "../src/testdata/codeql-bundle.tar.gz"),
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
false, // useOverlayAwareDefaultCliVersion
createFeatures([Feature.CleanupToolcacheBundles]),
getRunnerLogger(true),
);
t.is(result.toolsVersion, "local");
t.is(result.toolsSource, setupCodeql.ToolsSource.Local);
t.is(result.toolsDownloadStatusReport, undefined);
t.is(path.dirname(result.codeqlFolder), tmpDir);
t.true(fs.existsSync(result.codeqlFolder));
t.false(fs.existsSync(`${result.codeqlFolder}.complete`));
t.true(fs.existsSync(cachedDirectory));
t.true(cleanupSpy.notCalled);
t.true(downloadSpy.notCalled);
t.is(process.env[EnvVar.HAS_SET_UP_CODEQL], "true");
});
},
);
test.serial(
"getCodeQLSource correctly returns latest version from toolcache when tools == toolcache",
async (t) => {
@@ -974,30 +1097,35 @@ async function runDownloadCodeQL(
toolcacheRoot: string,
features: Feature[],
bundleVersion: string | undefined,
env = getTestEnv({
[ActionsEnvVars.RUNNER_TOOL_CACHE]: toolcacheRoot,
}),
): Promise<{
codeqlFolder: string;
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
}> {
sinon
.stub(toolsDownload, "downloadAndExtract")
.callsFake(async (_url, _compressionMethod, dest) => {
// The real implementation creates the destination directory, which matters here because the
// cleanup deletes it first and `writeToolcacheMarkerFile` writes into its parent afterwards.
fs.mkdirSync(dest, { recursive: true });
return { totalDurationMs: 1 };
});
stubDownloadAndExtract();
const addDiagnostic = sinon.stub(diagnostics, "addNoLanguageDiagnostic");
const { codeqlFolder } = await setupCodeql.downloadCodeQL(
"https://example.com/codeql-bundle.tar.gz",
"gzip",
bundleVersion,
CLEANUP_CLI_VERSION,
const { codeqlFolder } = await setupCodeql.downloadCodeQLBundle(
initAllState({
env,
features: createFeatures(features),
}),
{
bundle: {
kind: "combined",
url: "https://example.com/codeql-bundle.tar.gz",
},
compressionMethod: "gzip",
bundleVersion,
cliVersion: CLEANUP_CLI_VERSION,
sourceType: "download",
toolsVersion: CLEANUP_CLI_VERSION,
},
SAMPLE_DOTCOM_API_DETAILS,
undefined, // tarVersion
toolcacheRoot, // tempDir
createFeatures(features),
getRunnerLogger(true),
);
const diagnostic = addDiagnostic
@@ -1026,7 +1154,7 @@ async function testToolcacheCleanup(
}: {
features: Feature[];
runnerEnvironment: string | undefined;
setUp?: () => void;
setUp?: (env: Env) => void;
},
check: (context: {
cleanupDiagnostic: toolsDownload.ToolcacheCleanupResult | undefined;
@@ -1041,7 +1169,10 @@ async function testToolcacheCleanup(
} else {
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = runnerEnvironment;
}
setUp?.();
const env = getTestEnv({
[ActionsEnvVars.RUNNER_TOOL_CACHE]: tmpDir,
});
setUp?.(env);
// The extraction of the bundle would normally create this directory.
const destinationDirectory = createToolcacheEntry(
@@ -1060,6 +1191,7 @@ async function testToolcacheCleanup(
tmpDir,
features,
CLEANUP_BUNDLE_VERSION,
env,
);
t.true(
@@ -1072,7 +1204,7 @@ async function testToolcacheCleanup(
}
test.serial(
"downloadCodeQL does not clean up the toolcache when the feature flag is disabled",
"downloadCodeQLBundle does not clean up the toolcache when the feature flag is disabled",
async (t) => {
await testToolcacheCleanup(
t,
@@ -1087,7 +1219,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not clean up the toolcache when the runner is not GitHub-hosted",
"downloadCodeQLBundle does not clean up the toolcache when the runner is not GitHub-hosted",
async (t) => {
await testToolcacheCleanup(
t,
@@ -1105,7 +1237,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not clean up the toolcache when the runner environment is unknown",
"downloadCodeQLBundle does not clean up the toolcache when the runner environment is unknown",
async (t) => {
// A runner that doesn't report its environment must be treated as not GitHub-hosted, since its
// toolcache may well outlive the job.
@@ -1125,7 +1257,7 @@ test.serial(
);
test.serial(
"downloadCodeQL deletes other CodeQL bundles from the toolcache when enabled on a GitHub-hosted runner",
"downloadCodeQLBundle deletes other CodeQL bundles from the toolcache when enabled on a GitHub-hosted runner",
async (t) => {
await testToolcacheCleanup(
t,
@@ -1153,7 +1285,7 @@ test.serial(
);
test.serial(
"downloadCodeQL reports no deleted versions when the toolcache has no CodeQL bundles",
"downloadCodeQLBundle reports no deleted versions when the toolcache has no CodeQL bundles",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -1175,7 +1307,7 @@ test.serial(
);
test.serial(
"downloadCodeQL continues when deleting a CodeQL bundle from the toolcache fails",
"downloadCodeQLBundle continues when deleting a CodeQL bundle from the toolcache fails",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -1306,7 +1438,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not follow a symlinked CodeQL toolcache directory",
"downloadCodeQLBundle does not follow a symlinked CodeQL toolcache directory",
async (t) => {
await withTmpDir(async (tmpDir) => {
const toolcacheRoot = path.join(tmpDir, "toolcache");
@@ -1342,7 +1474,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not clean up the toolcache once a step has already set up CodeQL",
"downloadCodeQLBundle does not clean up the toolcache once a step has already set up CodeQL",
async (t) => {
// `.github/workflows/codeql.yml` sets up CodeQL twice and then runs both returned paths. If the
// second setup downloads, it must not delete the bundle the first one handed out.
@@ -1351,8 +1483,8 @@ test.serial(
{
features: [Feature.CleanupToolcacheBundles],
runnerEnvironment: "github-hosted",
setUp: () => {
process.env[EnvVar.HAS_SET_UP_CODEQL] = "true";
setUp: (env) => {
env.set(EnvVar.HAS_SET_UP_CODEQL, "true");
},
},
({ cleanupDiagnostic, destinationDirectory, staleDirectory }) => {
@@ -1364,6 +1496,33 @@ test.serial(
},
);
test.serial(
"downloadCodeQLBundle checks the supplied environment before cleaning the toolcache",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
const staleDirectory = createToolcacheEntry(
tmpDir,
"CodeQL",
CLEANUP_STALE_VERSION,
);
const { codeqlFolder, cleanupDiagnostic } = await runDownloadCodeQL(
tmpDir,
[Feature.CleanupToolcacheBundles],
CLEANUP_BUNDLE_VERSION,
getTestEnv({ [EnvVar.HAS_SET_UP_CODEQL]: "true" }),
);
t.true(fs.existsSync(staleDirectory));
t.true(fs.existsSync(`${codeqlFolder}.complete`));
t.is(cleanupDiagnostic, undefined);
});
},
);
test.serial(
"setupCodeQLBundle records that this job has set up CodeQL",
async (t) => {
@@ -1371,11 +1530,7 @@ test.serial(
setupActionsVars(tmpDir, tmpDir);
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
sinon.stub(setupCodeql, "downloadCodeQL").resolves({
codeqlFolder: "codeql",
statusReport: { totalDurationMs: 1 },
toolsVersion: LINKED_CLI_VERSION.cliVersion,
});
stubDownloadAndExtract();
await setupCodeql.setupCodeQLBundle(
"linked",
@@ -1399,7 +1554,50 @@ test.serial(
);
test.serial(
"downloadCodeQL cleans up the toolcache even when the download will not be cached",
"setupCodeQLBundle cleans up once and propagates a failed combined download",
async (t) => {
const error = new HTTPError("Not Found", 404);
const extractStub = stubDownloadAndExtract().rejects(error);
const cleanupSpy = sinon.spy(toolsDownload, "deleteToolcacheBundles");
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted";
delete process.env[EnvVar.HAS_SET_UP_CODEQL];
const staleDirectory = createToolcacheEntry(
tmpDir,
"CodeQL",
CLEANUP_STALE_VERSION,
);
await t.throwsAsync(
setupCodeql.setupCodeQLBundle(
"linked",
SAMPLE_DOTCOM_API_DETAILS,
tmpDir,
GitHubVariant.DOTCOM,
SAMPLE_DEFAULT_CLI_VERSION,
undefined, // rawLanguages
false, // useOverlayAwareDefaultCliVersion
createFeatures([Feature.CleanupToolcacheBundles]),
getRunnerLogger(true),
),
{ is: error },
);
t.true(cleanupSpy.calledOnce);
t.true(cleanupSpy.calledBefore(extractStub));
t.true(extractStub.calledOnce);
t.false(fs.existsSync(staleDirectory));
t.false(fs.existsSync(`${extractStub.firstCall.args[2]}.complete`));
t.deepEqual(toolcache.findAllVersions("CodeQL"), []);
t.is(process.env[EnvVar.HAS_SET_UP_CODEQL], undefined);
});
},
);
test.serial(
"downloadCodeQLBundle cleans up the toolcache even when the download will not be cached",
async (t) => {
// A `tools` URL we can't derive a bundle version from is extracted to a temporary directory
// rather than the toolcache, but the toolcache is on the same filesystem, so emptying it still
@@ -1434,7 +1632,7 @@ test.serial(
);
test.serial(
"downloadCodeQL reports a failure when the toolcache cannot be inspected",
"downloadCodeQLBundle reports a failure when the toolcache cannot be inspected",
async (t) => {
await withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
@@ -1466,7 +1664,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not clean up a toolcache on a different filesystem to the workspace",
"downloadCodeQLBundle does not clean up a toolcache on a different filesystem to the workspace",
async (t) => {
// Some runner images keep the toolcache on a different volume to the workspace, in which case
// deleting the tools frees up disk space that the analysis cannot use.
@@ -1516,7 +1714,7 @@ test.serial(
);
test.serial(
"downloadCodeQL does not delete through a symlinked version directory",
"downloadCodeQLBundle does not delete through a symlinked version directory",
async (t) => {
await withTmpDir(async (tmpDir) => {
const toolcacheRoot = path.join(tmpDir, "toolcache");

View File

@@ -164,7 +164,7 @@ function tryGetBundleVersionFromTagName(
tagName: string,
logger: Logger,
): string | undefined {
const match = tagName.match(/^codeql-bundle-(.*)$/);
const match = tagName.match(/^codeql-bundle-(.+)$/);
if (match === null || match.length < 2) {
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
return undefined;
@@ -215,6 +215,25 @@ export function convertToSemVer(version: string, logger: Logger): string {
return s;
}
/** Describes the contents and location of a downloadable CodeQL bundle. */
type CodeQLBundle = { kind: "combined"; url: string };
/** A resolved download, including its bundle identity and version. */
export interface CodeQLDownloadSource {
/** Distinguishes downloads from local archives and cached installations. */
sourceType: "download";
/** The bundle to download. */
bundle: CodeQLBundle;
/** The compression format of the bundle archive. */
compressionMethod: tar.CompressionMethod;
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** CLI version of the tools, if known. */
cliVersion?: string;
/** Resolved version for telemetry, independent of whether the bundle can be cached. */
toolsVersion: string;
}
export type CodeQLToolsSource =
| {
codeqlTarPath: string;
@@ -229,17 +248,7 @@ export type CodeQLToolsSource =
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string;
}
| {
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** CLI version of the tools, if known. */
cliVersion?: string;
compressionMethod: tar.CompressionMethod;
codeqlURL: string;
sourceType: "download";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string;
};
| CodeQLDownloadSource;
/**
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
@@ -583,7 +592,7 @@ export async function getCodeQLSource(
if (tagName) {
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
// If the bundle version is a semantic version, it is a CLI version number.
if (bundleVersion && semver.valid(bundleVersion)) {
if (bundleVersion !== undefined && semver.valid(bundleVersion)) {
cliVersion = convertToSemVer(bundleVersion, logger);
}
}
@@ -600,13 +609,15 @@ export async function getCodeQLSource(
}
const bundleVersion =
tagName && tryGetBundleVersionFromTagName(tagName, logger);
const humanReadableVersion =
tagName !== undefined
? tryGetBundleVersionFromTagName(tagName, logger)
: undefined;
const resolvedVersion =
cliVersion ??
(bundleVersion && convertToSemVer(bundleVersion, logger)) ??
tagName ??
url ??
"unknown";
(bundleVersion !== undefined
? convertToSemVer(bundleVersion, logger)
: undefined);
const humanReadableVersion = resolvedVersion ?? tagName ?? url ?? "unknown";
logger.debug(
"Attempting to obtain CodeQL tools. " +
@@ -750,12 +761,12 @@ export async function getCodeQLSource(
logger.info(`Using CodeQL CLI sourced from ${url} .`);
}
return {
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
bundle: { kind: "combined", url },
bundleVersion,
cliVersion,
codeqlURL: url,
compressionMethod,
sourceType: "download",
toolsVersion: cliVersion ?? humanReadableVersion,
toolsVersion: resolvedVersion ?? "unknown",
};
}
@@ -769,7 +780,7 @@ async function tryGetFallbackToolcacheVersion(
logger: Logger,
): Promise<string | undefined> {
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
if (!bundleVersion) {
if (bundleVersion === undefined) {
return undefined;
}
const fallbackVersion = convertToSemVer(bundleVersion, logger);
@@ -783,20 +794,17 @@ async function tryGetFallbackToolcacheVersion(
// Exported using `export const` for testing purposes. Specifically, we want to
// be able to stub this function and have other functions in this file use that stub.
export const downloadCodeQL = async function (
codeqlURL: string,
compressionMethod: tar.CompressionMethod,
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
features: FeatureEnablement,
logger: Logger,
): Promise<{
codeqlFolder: string;
statusReport: ToolsDownloadStatusReport;
toolsVersion: string;
}> {
const { bundle, compressionMethod } = source;
const codeqlURL = bundle.url;
const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers: OutgoingHttpHeaders = {
@@ -815,16 +823,9 @@ export const downloadCodeQL = async function (
);
}
const toolcacheInfo = getToolcacheDestinationInfo(
maybeBundleVersion,
maybeCliVersion,
logger,
);
const toolcacheDestination = getToolcacheDestination(source, logger);
const extractedBundlePath =
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
await tryDeleteToolcacheBundles({ env: getEnv(), features, logger });
toolcacheDestination ?? getTempExtractionDir(tempDir);
const statusReport = await downloadAndExtract(
codeqlURL,
@@ -836,46 +837,40 @@ export const downloadCodeQL = async function (
logger,
);
if (!toolcacheInfo) {
if (toolcacheDestination) {
writeToolcacheMarkerFile(toolcacheDestination, logger);
} else {
logger.debug(
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
`URL ${codeqlURL}.`,
);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? "unknown",
};
}
writeToolcacheMarkerFile(toolcacheInfo.path, logger);
return {
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? toolcacheInfo.version,
};
};
function getToolcacheDestinationInfo(
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
/**
* Returns the canonical toolcache directory for a resolved download, or `undefined` if its bundle
* version is unknown.
*/
function getToolcacheDestination(
source: CodeQLDownloadSource,
logger: Logger,
): { path: string; version: string } | undefined {
if (maybeBundleVersion) {
const version = getCanonicalToolcacheVersion(
maybeCliVersion,
maybeBundleVersion,
logger,
);
return {
path: getToolcacheDirectory(version),
version,
};
): string | undefined {
if (!source.bundleVersion) {
return undefined;
}
return undefined;
return getToolcacheDirectory(
getCanonicalToolcacheVersion(
source.cliVersion,
source.bundleVersion,
logger,
),
);
}
/**
@@ -1000,7 +995,6 @@ export async function setupCodeQLBundle(
);
let codeqlFolder: string;
let toolsVersion = source.toolsVersion;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsSource: ToolsSource;
switch (source.sourceType) {
@@ -1021,18 +1015,13 @@ export async function setupCodeQLBundle(
toolsSource = ToolsSource.Toolcache;
break;
case "download": {
const result = await downloadCodeQL(
source.codeqlURL,
source.compressionMethod,
source.bundleVersion,
source.cliVersion,
const result = await downloadCodeQLBundle(
{ env: getEnv(), features, logger },
source,
apiDetails,
zstdAvailability.version,
tempDir,
features,
logger,
);
toolsVersion = result.toolsVersion;
codeqlFolder = result.codeqlFolder;
toolsDownloadStatusReport = result.statusReport;
toolsSource = ToolsSource.Download;
@@ -1050,10 +1039,35 @@ export async function setupCodeQLBundle(
codeqlFolder,
toolsDownloadStatusReport,
toolsSource,
toolsVersion,
toolsVersion: source.toolsVersion,
};
}
/**
* Performs eligible toolcache cleanup once, then downloads and extracts the resolved bundle.
*
* @returns The extraction directory and download timings.
*/
export async function downloadCodeQLBundle(
action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>,
source: CodeQLDownloadSource,
apiDetails: api.GitHubApiDetails,
tarVersion: tar.TarVersion | undefined,
tempDir: string,
): Promise<{
codeqlFolder: string;
statusReport: ToolsDownloadStatusReport;
}> {
await tryDeleteToolcacheBundles(action);
return await downloadCodeQL(
source,
apiDetails,
tarVersion,
tempDir,
action.logger,
);
}
async function useZstdBundle(
cliVersion: string,
tarSupportsZstd: boolean,