Files
codeql-action/src/setup-codeql.ts
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1155 lines
37 KiB
TypeScript
Raw Normal View History

2023-01-09 14:53:52 +00:00
import * as fs from "fs";
import { OutgoingHttpHeaders } from "http";
import * as path from "path";
import * as core from "@actions/core";
2023-01-09 14:53:52 +00:00
import * as toolcache from "@actions/tool-cache";
import { default as deepEqual } from "fast-deep-equal";
import * as semver from "semver";
import { v4 as uuidV4 } from "uuid";
2023-01-09 14:53:52 +00:00
import {
isAnalyzingPullRequest,
isDynamicWorkflow,
isGitHubHostedRunner,
isRunningLocalAction,
} from "./actions-util";
2023-01-09 14:53:52 +00:00
import * as api from "./api-client";
import * as defaults from "./defaults.json";
2026-05-06 18:30:24 +01:00
import {
addNoLanguageDiagnostic,
makeDiagnostic,
makeTelemetryDiagnostic,
} from "./diagnostics";
import { EnvVar } from "./environment";
import {
CODEQL_VERSION_ZSTD_BUNDLE,
CodeQLDefaultVersionInfo,
CodeQLVersionInfo,
2025-10-10 13:49:06 +01:00
Feature,
FeatureEnablement,
} from "./feature-flags";
2025-08-08 11:26:14 +01:00
import { Logger } from "./logging";
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
import * as tar from "./tar";
import {
deleteToolcacheBundles,
downloadAndExtract,
getToolcacheDirectory,
ToolcacheCleanupResult,
ToolsDownloadStatusReport,
2024-12-02 18:30:02 +00:00
writeToolcacheMarkerFile,
} from "./tools-download";
2023-01-09 14:53:52 +00:00
import * as util from "./util";
2025-08-08 11:26:14 +01:00
import { isGoodVersion } from "./util";
2023-01-09 14:53:52 +00:00
export enum ToolsSource {
Unknown = "UNKNOWN",
Local = "LOCAL",
Toolcache = "TOOLCACHE",
Download = "DOWNLOAD",
}
2025-11-18 18:10:09 +00:00
const CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
const CODEQL_NIGHTLIES_REPOSITORY_OWNER = "dsp-testing";
const CODEQL_NIGHTLIES_REPOSITORY_NAME = "codeql-cli-nightlies";
2023-01-09 14:53:52 +00:00
2024-05-08 15:52:42 +01:00
const CODEQL_BUNDLE_VERSION_ALIAS: string[] = ["linked", "latest"];
const CODEQL_NIGHTLY_TOOLS_INPUTS = ["nightly", "nightly-latest"];
const CODEQL_TOOLCACHE_INPUT = "toolcache";
2024-05-08 15:52:42 +01:00
function getCodeQLBundleExtension(
compressionMethod: tar.CompressionMethod,
): string {
switch (compressionMethod) {
case "gzip":
return ".tar.gz";
case "zstd":
return ".tar.zst";
default:
util.assertNever(compressionMethod);
}
}
2026-02-15 17:13:52 +00:00
export function getCodeQLBundleName(
compressionMethod: tar.CompressionMethod,
): string {
const extension = getCodeQLBundleExtension(compressionMethod);
2023-01-09 14:53:52 +00:00
let platform: string;
if (process.platform === "win32") {
platform = "win64";
} else if (process.platform === "linux") {
2026-08-04 15:14:29 +02:00
platform = process.arch === "arm64" ? "linux-arm64" : "linux64";
2023-01-09 14:53:52 +00:00
} else if (process.platform === "darwin") {
platform = "osx64";
} else {
return `codeql-bundle${extension}`;
2023-01-09 14:53:52 +00:00
}
return `codeql-bundle-${platform}${extension}`;
2023-01-09 14:53:52 +00:00
}
export function getCodeQLActionRepository(logger: Logger): string {
if (isRunningLocalAction()) {
// This handles the case where the Action does not come from an Action repository,
// e.g. our integration tests which use the Action code from the current checkout.
// In these cases, the GITHUB_ACTION_REPOSITORY environment variable is not set.
logger.info(
"The CodeQL Action is checked out locally. Using the default CodeQL Action repository.",
);
return CODEQL_DEFAULT_ACTION_REPOSITORY;
}
return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY");
}
async function getCodeQLBundleDownloadURL(
2023-01-10 13:16:22 +00:00
tagName: string,
2023-01-09 14:53:52 +00:00
apiDetails: api.GitHubApiDetails,
compressionMethod: tar.CompressionMethod,
2023-01-09 14:53:52 +00:00
logger: Logger,
): Promise<string> {
const codeQLActionRepository = getCodeQLActionRepository(logger);
const potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
[util.GITHUB_DOTCOM_URL, CODEQL_DEFAULT_ACTION_REPOSITORY],
];
// We now filter out any duplicates.
// Duplicates will happen either because the GitHub instance is GitHub.com, or because the Action is not a fork.
const uniqueDownloadSources = potentialDownloadSources.filter(
(source, index, self) => {
return !self.slice(0, index).some((other) => deepEqual(source, other));
},
);
const codeQLBundleName = getCodeQLBundleName(compressionMethod);
2023-01-09 14:53:52 +00:00
for (const downloadSource of uniqueDownloadSources) {
const [apiURL, repository] = downloadSource;
// If we've reached the final case, short-circuit the API check since we know the bundle exists and is public.
if (
apiURL === util.GITHUB_DOTCOM_URL &&
repository === CODEQL_DEFAULT_ACTION_REPOSITORY
) {
break;
}
const [repositoryOwner, repositoryName] = repository.split("/");
try {
2023-07-13 11:17:33 +01:00
const release = await api.getApiClient().rest.repos.getReleaseByTag({
2023-01-09 14:53:52 +00:00
owner: repositoryOwner,
repo: repositoryName,
2023-01-10 13:16:22 +00:00
tag: tagName,
2023-01-09 14:53:52 +00:00
});
for (const asset of release.data.assets) {
if (asset.name === codeQLBundleName) {
logger.info(
`Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`,
2023-01-09 14:53:52 +00:00
);
return asset.url;
}
}
} catch (e) {
logger.info(
`Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`,
2023-01-09 14:53:52 +00:00
);
}
}
2023-01-10 13:16:22 +00:00
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
}
function tryGetBundleVersionFromTagName(
tagName: string,
logger: Logger,
): string | undefined {
2023-01-26 11:49:51 +00:00
const match = tagName.match(/^codeql-bundle-(.*)$/);
if (match === null || match.length < 2) {
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
return undefined;
2023-01-26 11:49:51 +00:00
}
return match[1];
}
2024-10-28 19:59:13 +00:00
export function tryGetTagNameFromUrl(
url: string,
logger: Logger,
): string | undefined {
const matches = [...url.matchAll(/\/(codeql-bundle-[^/]*)\//g)];
if (matches.length === 0) {
logger.debug(`Could not determine tag name for URL ${url}.`);
return undefined;
2023-01-10 13:16:22 +00:00
}
// Example: https://github.com/org/codeql-bundle-testing/releases/download/codeql-bundle-v2.19.0/codeql-bundle-linux64.tar.zst
// We require a trailing forward slash to be part of the match, so the last match gives us the tag
// name. An alternative approach would be to also match against `/releases/`, but this approach
// assumes less about the structure of the URL.
const match = matches[matches.length - 1];
2025-10-22 16:55:06 +01:00
if (match?.length !== 2) {
logger.debug(
`Could not determine tag name for URL ${url}. Matched ${JSON.stringify(
match,
)}.`,
);
return undefined;
}
return match[1];
}
2023-01-10 13:16:22 +00:00
export function convertToSemVer(version: string, logger: Logger): string {
if (!semver.valid(version)) {
logger.debug(
`Bundle version ${version} is not in SemVer format. Will treat it as pre-release 0.0.0-${version}.`,
);
version = `0.0.0-${version}`;
}
const s = semver.clean(version);
if (!s) {
throw new Error(`Bundle version ${version} is not in SemVer format.`);
}
return s;
2023-01-09 14:53:52 +00:00
}
2026-02-15 17:13:52 +00:00
export type CodeQLToolsSource =
| {
codeqlTarPath: string;
compressionMethod: tar.CompressionMethod;
sourceType: "local";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: "local";
}
2023-01-09 14:53:52 +00:00
| {
codeqlFolder: string;
sourceType: "toolcache";
/** Human-readable description of the source of the tools for telemetry purposes. */
2023-01-09 14:53:52 +00:00
toolsVersion: string;
}
| {
/** Bundle version of the tools, if known. */
bundleVersion?: string;
/** CLI version of the tools, if known. */
cliVersion?: string;
compressionMethod: tar.CompressionMethod;
2023-01-09 14:53:52 +00:00
codeqlURL: string;
sourceType: "download";
/** Human-readable description of the source of the tools for telemetry purposes. */
2023-01-09 14:53:52 +00:00
toolsVersion: string;
};
2023-01-10 13:16:22 +00:00
/**
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
*/
async function findOverridingToolsInCache(
humanReadableVersion: string,
2023-01-10 13:16:22 +00:00
logger: Logger,
): Promise<CodeQLToolsSource | undefined> {
const candidates = toolcache
.findAllVersions("CodeQL")
.filter(isGoodVersion)
.map((version) => ({
folder: toolcache.find("CodeQL", version),
version,
}))
.filter(({ folder }) => fs.existsSync(path.join(folder, "pinned-version")));
if (candidates.length === 1) {
const candidate = candidates[0];
logger.debug(
`CodeQL tools version ${candidate.version} in toolcache overriding version ${humanReadableVersion}.`,
2023-01-10 13:16:22 +00:00
);
return {
codeqlFolder: candidate.folder,
sourceType: "toolcache",
toolsVersion: candidate.version,
};
} else if (candidates.length === 0) {
logger.debug(
"Did not find any candidate pinned versions of the CodeQL tools in the toolcache.",
);
} else {
logger.debug(
"Could not use CodeQL tools from the toolcache since more than one candidate pinned " +
"version was found in the toolcache.",
);
}
return undefined;
}
2026-05-06 18:30:24 +01:00
/**
* Returns the sorted set of enabled versions that have cached overlay-base databases for the
* given languages, or an empty list if neither the `OverlayAnalysisMatchCodeqlVersion` nor the
* `OverlayAnalysisMatchCodeqlVersionDryRun` feature flag is enabled. When only the dry-run flag
* is enabled, this performs the lookup and emits a telemetry diagnostic with the version that
* would have been chosen, but still returns an empty list so the caller falls back.
*/
export async function getEnabledVersionsWithOverlayBaseDatabases(
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
features: FeatureEnablement,
logger: Logger,
): Promise<CodeQLVersionInfo[]> {
if (rawLanguages === undefined || rawLanguages.length === 0) {
return [];
}
2026-05-06 18:30:24 +01:00
const isEnabled = await features.getValue(
Feature.OverlayAnalysisMatchCodeqlVersion,
);
const isDryRun =
!isEnabled &&
(await features.getValue(Feature.OverlayAnalysisMatchCodeqlVersionDryRun));
if (!isEnabled && !isDryRun) {
return [];
}
let cachedVersions: string[] | undefined;
try {
cachedVersions = await getCodeQlVersionsForOverlayBaseDatabases(
rawLanguages,
logger,
);
} catch (e) {
logger.warning(
2026-05-08 18:05:35 +01:00
"Could not list overlay-base databases in the Actions cache while choosing a default " +
`CodeQL CLI version, falling back to the highest enabled version. Details: ${util.getErrorMessage(e)}`,
);
return [];
}
2026-05-06 18:30:24 +01:00
if (cachedVersions === undefined || cachedVersions.length === 0) {
return [];
}
const cachedVersionsSet = new Set(cachedVersions);
2026-05-06 18:30:24 +01:00
const overlayVersions = defaultCliVersion.enabledVersions.filter((v) =>
cachedVersionsSet.has(v.cliVersion),
);
2026-05-06 18:30:24 +01:00
if (overlayVersions.length === 0) {
return [];
}
const isCachedVersionDifferent =
overlayVersions[0].cliVersion !==
defaultCliVersion.enabledVersions[0].cliVersion;
if (isCachedVersionDifferent) {
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/overlay-aware-default-codeql-version",
"Overlay-aware default CodeQL version selection",
{
cachedVersions,
enabledVersions: defaultCliVersion.enabledVersions.map(
(v) => v.cliVersion,
),
isDryRun,
overlayAwareVersion: overlayVersions[0].cliVersion,
},
),
);
}
if (isDryRun) {
logger.debug(
`Overlay-aware default CodeQL version selection is running in dry-run mode. Would have used version ${overlayVersions[0].cliVersion}.`,
);
return [];
}
return overlayVersions;
}
/**
* Resolves the newest enabled default CLI version that has a cached overlay-base database for the
2026-05-07 11:00:54 +01:00
* relevant languages, if running a Code Scanning analysis for a pull request and one exists.
* Otherwise, falls back to the newest enabled default CLI version.
*/
async function resolveDefaultCliVersion(
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion: boolean,
features: FeatureEnablement,
logger: Logger,
): Promise<CodeQLVersionInfo> {
2026-05-07 11:00:54 +01:00
if (!useOverlayAwareDefaultCliVersion || !isAnalyzingPullRequest()) {
return defaultCliVersion.enabledVersions[0];
}
const overlayVersions = await getEnabledVersionsWithOverlayBaseDatabases(
defaultCliVersion,
rawLanguages,
features,
logger,
);
if (overlayVersions.length > 0) {
logger.info(
`Using CodeQL version ${overlayVersions[0].cliVersion} since this is the ` +
`highest enabled version that has a cached overlay-base database.`,
);
return overlayVersions[0];
}
return defaultCliVersion.enabledVersions[0];
}
2026-02-15 16:21:03 +00:00
/**
* Determines where the CodeQL CLI we want to use comes from. This can be from a local file,
* the Actions toolcache, or a download.
*
* @param toolsInput The argument provided for the `tools` input, if any.
* @param defaultCliVersion The default CLI version that's linked to the CodeQL Action.
* @param rawLanguages Raw set of languages.
2026-05-07 11:00:54 +01:00
* @param useOverlayAwareDefaultCliVersion Whether to select an overlay-aware default CLI version.
2026-02-15 16:21:03 +00:00
* @param apiDetails Information about the GitHub API.
* @param variant The GitHub variant we are running on.
* @param tarSupportsZstd Whether zstd is supported by `tar`.
* @param features Information about enabled features.
* @param logger The logger to use.
*
2026-02-16 09:07:02 +00:00
* @returns Information about where the CodeQL CLI we want to use comes from.
2026-02-15 16:21:03 +00:00
*/
2023-01-09 14:53:52 +00:00
export async function getCodeQLSource(
toolsInput: string | undefined,
2023-01-10 13:16:22 +00:00
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion: boolean,
2023-01-09 14:53:52 +00:00
apiDetails: api.GitHubApiDetails,
variant: util.GitHubVariant,
2024-09-23 21:59:03 +01:00
tarSupportsZstd: boolean,
2025-10-10 13:49:06 +01:00
features: FeatureEnablement,
2023-01-09 14:53:52 +00:00
logger: Logger,
): Promise<CodeQLToolsSource> {
2026-02-15 16:21:03 +00:00
// If there is an explicit `tools` input, it's not one of the reserved values, and it doesn't appear
// to point to a URL, then we assume it is a local path and use the CLI from there.
// TODO: This appears to misclassify filenames that happen to start with `http` as URLs.
2024-05-08 15:52:42 +01:00
if (
toolsInput &&
!isReservedToolsValue(toolsInput) &&
2024-05-08 15:52:42 +01:00
!toolsInput.startsWith("http")
) {
logger.info(`Using CodeQL CLI from local path ${toolsInput}`);
const compressionMethod = tar.inferCompressionMethod(toolsInput);
if (compressionMethod === undefined) {
throw new util.ConfigurationError(
`Could not infer compression method from path ${toolsInput}. Please specify a path ` +
"ending in '.tar.gz' or '.tar.zst'.",
);
}
2023-01-09 14:53:52 +00:00
return {
codeqlTarPath: toolsInput,
compressionMethod,
2023-01-09 14:53:52 +00:00
sourceType: "local",
toolsVersion: "local",
};
}
/** CLI version number, for example 2.12.6. */
let cliVersion: string | undefined;
/** Tag name of the CodeQL bundle, for example `codeql-bundle-20230120`. */
let tagName: string | undefined;
2023-01-10 13:16:22 +00:00
/**
* URL of the CodeQL bundle.
*
* This does not always include a tag name.
2023-01-10 13:16:22 +00:00
*/
let url: string | undefined;
2023-01-09 14:53:52 +00:00
2026-02-16 08:54:19 +00:00
// We allow forcing the nightly CLI via the FF for `dynamic` events (or in test mode) where the
// `tools` input cannot be adjusted to explicitly request it.
const canForceNightlyWithFF = isDynamicWorkflow() || util.isInTestMode();
2026-02-15 17:22:20 +00:00
const forceNightlyValueFF = await features.getValue(Feature.ForceNightly);
2026-02-16 08:54:19 +00:00
const forceNightly = forceNightlyValueFF && canForceNightlyWithFF;
2026-02-15 17:22:20 +00:00
2026-02-16 08:54:19 +00:00
// For advanced workflows, a value from `CODEQL_NIGHTLY_TOOLS_INPUTS` can be specified explicitly
// for the `tools` input in the workflow file.
const nightlyRequestedByToolsInput =
toolsInput !== undefined &&
CODEQL_NIGHTLY_TOOLS_INPUTS.includes(toolsInput);
if (forceNightly || nightlyRequestedByToolsInput) {
2026-02-15 17:22:20 +00:00
if (forceNightly) {
logger.info(
`Using the latest CodeQL CLI nightly, as forced by the ${Feature.ForceNightly} feature flag.`,
);
addNoLanguageDiagnostic(
undefined,
makeDiagnostic(
"codeql-action/forced-nightly-cli",
"A nightly release of CodeQL was used",
{
markdownMessage:
"GitHub configured this analysis to use a nightly release of CodeQL to allow you to preview changes from an upcoming release.\n\n" +
"Nightly releases do not undergo the same validation as regular releases and may lead to analysis instability.\n\n" +
"If use of a nightly CodeQL release for this analysis is unexpected, please contact GitHub support.",
visibility: {
cliSummaryTable: true,
statusPage: true,
telemetry: true,
},
2026-02-16 17:12:12 +00:00
severity: "note",
},
),
);
2026-02-15 17:22:20 +00:00
} else {
logger.info(
`Using the latest CodeQL CLI nightly, as requested by 'tools: ${toolsInput}'.`,
);
}
toolsInput = await getNightlyToolsUrl(logger);
}
2025-09-23 14:53:29 +02:00
/**
* Whether the tools shipped with the Action, i.e. those in `defaults.json`, have been forced.
*
* We use the special value of 'linked' to prioritize the version in `defaults.json` over the
* version specified by the feature flags on Dotcom and over any pinned cached version on
* Enterprise Server.
*
* Previously we have been using 'latest' to force the shipped tools, but this was not clear
* enough for the users, so it has been changed to `linked`. We're keeping around `latest` for
* backwards compatibility.
*/
const forceShippedTools =
toolsInput && CODEQL_BUNDLE_VERSION_ALIAS.includes(toolsInput);
2023-02-06 11:57:48 +00:00
if (forceShippedTools) {
cliVersion = defaults.cliVersion;
tagName = defaults.bundleVersion;
2025-09-23 14:53:29 +02:00
logger.info(
`'tools: ${toolsInput}' was requested, so using CodeQL version ${cliVersion}, the version shipped with the Action.`,
);
if (toolsInput === "latest") {
logger.warning(
"`tools: latest` has been renamed to `tools: linked`, but the old name is still supported. No action is required.",
);
}
} else if (
toolsInput !== undefined &&
toolsInput === CODEQL_TOOLCACHE_INPUT
) {
2025-10-06 13:16:03 +01:00
let latestToolcacheVersion: string | undefined;
2025-10-06 13:16:03 +01:00
// We only allow `toolsInput === "toolcache"` for `dynamic` events. In general, using `toolsInput === "toolcache"`
// can lead to alert wobble and so it shouldn't be used for an analysis where results are intended to be uploaded.
// We also allow this in test mode.
2026-07-24 16:28:21 +01:00
const allowToolcacheValue = isDynamicWorkflow() || util.isInTestMode();
2025-10-06 13:16:03 +01:00
if (allowToolcacheValue) {
// If `toolsInput === "toolcache"`, try to find the latest version of the CLI that's available in the toolcache
// and use that. We perform this check here since we can set `cliVersion` directly and don't want to default to
// the linked version.
logger.info(
2025-10-06 13:16:03 +01:00
`Attempting to use the latest CodeQL CLI version in the toolcache, as requested by 'tools: ${toolsInput}'.`,
);
2025-10-06 13:16:03 +01:00
latestToolcacheVersion = getLatestToolcacheVersion(logger);
if (latestToolcacheVersion) {
cliVersion = latestToolcacheVersion;
}
}
if (latestToolcacheVersion === undefined) {
if (allowToolcacheValue) {
logger.info(
`Found no CodeQL CLI in the toolcache, ignoring 'tools: ${toolsInput}'...`,
);
} else {
2026-07-24 16:28:21 +01:00
logger.warning(
`Ignoring 'tools: ${toolsInput}' because the workflow was not triggered dynamically.`,
);
2025-10-06 13:16:03 +01:00
}
const version = await resolveDefaultCliVersion(
defaultCliVersion,
rawLanguages,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion,
features,
logger,
);
cliVersion = version.cliVersion;
tagName = version.tagName;
}
} else if (toolsInput !== undefined) {
// If a tools URL was provided, then use that.
tagName = tryGetTagNameFromUrl(toolsInput, logger);
url = toolsInput;
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)) {
cliVersion = convertToSemVer(bundleVersion, logger);
}
}
} else {
const version = await resolveDefaultCliVersion(
defaultCliVersion,
rawLanguages,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion,
features,
logger,
);
cliVersion = version.cliVersion;
tagName = version.tagName;
}
const bundleVersion =
tagName && tryGetBundleVersionFromTagName(tagName, logger);
const humanReadableVersion =
cliVersion ??
(bundleVersion && convertToSemVer(bundleVersion, logger)) ??
tagName ??
url ??
"unknown";
logger.debug(
"Attempting to obtain CodeQL tools. " +
`CLI version: ${cliVersion ?? "unknown"}, ` +
`bundle tag name: ${tagName ?? "unknown"}, ` +
`URL: ${url ?? "unspecified"}.`,
);
2023-01-10 13:16:22 +00:00
let codeqlFolder: string | undefined;
if (cliVersion) {
// If we find the specified CLI version, we always use that.
codeqlFolder = toolcache.find("CodeQL", cliVersion);
// Fall back to matching `x.y.z-<tagName>`.
if (!codeqlFolder) {
logger.debug(
"Didn't find a version of the CodeQL tools in the toolcache with a version number " +
`exactly matching ${cliVersion}.`,
);
const allVersions = toolcache.findAllVersions("CodeQL");
logger.debug(
`Found the following versions of the CodeQL tools in the toolcache: ${JSON.stringify(
allVersions,
)}.`,
);
// If there is exactly one version of the CodeQL tools in the toolcache, and that version is
// the form `x.y.z-<tagName>`, then use it.
const candidateVersions = allVersions.filter((version) =>
version.startsWith(`${cliVersion}-`),
);
if (candidateVersions.length === 1) {
logger.debug(
`Exactly one version of the CodeQL tools starting with ${cliVersion} found in the ` +
"toolcache, using that.",
);
codeqlFolder = toolcache.find("CodeQL", candidateVersions[0]);
} else if (candidateVersions.length === 0) {
logger.debug(
`Didn't find any versions of the CodeQL tools starting with ${cliVersion} ` +
`in the toolcache. Trying next fallback method.`,
);
} else {
logger.warning(
`Found ${candidateVersions.length} versions of the CodeQL tools starting with ` +
`${cliVersion} in the toolcache, but at most one was expected.`,
);
logger.debug("Trying next fallback method.");
}
}
}
// Fall back to matching `0.0.0-<bundleVersion>`.
if (!codeqlFolder && tagName) {
2023-07-07 15:32:20 +01:00
const fallbackVersion = await tryGetFallbackToolcacheVersion(
cliVersion,
tagName,
logger,
);
if (fallbackVersion) {
codeqlFolder = toolcache.find("CodeQL", fallbackVersion);
} else {
logger.debug(
"Could not determine a fallback toolcache version number for CodeQL tools version " +
2023-07-07 15:32:20 +01:00
`${humanReadableVersion}.`,
);
}
}
if (codeqlFolder) {
logger.info(
`Found CodeQL tools version ${humanReadableVersion} in the toolcache.`,
2023-01-26 11:49:51 +00:00
);
} else {
logger.info(
`Did not find CodeQL tools version ${humanReadableVersion} in the toolcache.`,
2023-01-10 13:16:22 +00:00
);
}
2023-01-09 14:53:52 +00:00
if (codeqlFolder) {
if (cliVersion) {
logger.info(
`Using CodeQL CLI version ${cliVersion} from toolcache at ${codeqlFolder}`,
);
} else {
logger.info(`Using CodeQL CLI from toolcache at ${codeqlFolder}`);
}
2023-01-09 14:53:52 +00:00
return {
codeqlFolder,
sourceType: "toolcache",
toolsVersion: cliVersion ?? humanReadableVersion,
2023-01-09 14:53:52 +00:00
};
}
2023-01-10 13:16:22 +00:00
// If we don't find the requested version on Enterprise, we may allow a
2023-01-09 14:53:52 +00:00
// different version to save download time if the version hasn't been
// specified explicitly (in which case we always honor it).
2023-02-06 11:57:48 +00:00
if (
variant === util.GitHubVariant.GHES &&
2023-02-06 11:57:48 +00:00
!forceShippedTools &&
!toolsInput
) {
2023-01-10 13:16:22 +00:00
const result = await findOverridingToolsInCache(
humanReadableVersion,
2023-01-10 13:16:22 +00:00
logger,
);
if (result !== undefined) {
return result;
2023-01-09 14:53:52 +00:00
}
}
let compressionMethod: tar.CompressionMethod;
if (!url) {
compressionMethod =
cliVersion !== undefined &&
(await useZstdBundle(cliVersion, tarSupportsZstd))
? "zstd"
: "gzip";
url = await getCodeQLBundleDownloadURL(
tagName!,
apiDetails,
compressionMethod,
logger,
);
} else {
const method = tar.inferCompressionMethod(url);
if (method === undefined) {
throw new util.ConfigurationError(
`Could not infer compression method from URL ${url}. Please specify a URL ` +
"ending in '.tar.gz' or '.tar.zst'.",
);
}
compressionMethod = method;
}
if (cliVersion) {
logger.info(`Using CodeQL CLI version ${cliVersion} sourced from ${url} .`);
} else {
logger.info(`Using CodeQL CLI sourced from ${url} .`);
}
2023-01-09 14:53:52 +00:00
return {
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
cliVersion,
codeqlURL: url,
compressionMethod,
2023-01-09 14:53:52 +00:00
sourceType: "download",
toolsVersion: cliVersion ?? humanReadableVersion,
2023-01-09 14:53:52 +00:00
};
}
/**
* Gets a fallback version number to use when looking for CodeQL in the toolcache if we didn't find
* the `x.y.z` version. This is to support old versions of the toolcache.
*/
2025-11-18 18:10:09 +00:00
async function tryGetFallbackToolcacheVersion(
cliVersion: string | undefined,
tagName: string,
logger: Logger,
): Promise<string | undefined> {
const bundleVersion = tryGetBundleVersionFromTagName(tagName, logger);
if (!bundleVersion) {
return undefined;
}
const fallbackVersion = convertToSemVer(bundleVersion, logger);
logger.debug(
`Computed a fallback toolcache version number of ${fallbackVersion} for CodeQL version ` +
`${cliVersion ?? tagName}.`,
);
return fallbackVersion;
}
// 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 (
2023-01-09 14:53:52 +00:00
codeqlURL: string,
compressionMethod: tar.CompressionMethod,
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
2023-01-09 14:53:52 +00:00
apiDetails: api.GitHubApiDetails,
2024-10-02 15:32:34 +01:00
tarVersion: tar.TarVersion | undefined,
2023-01-09 14:53:52 +00:00
tempDir: string,
features: FeatureEnablement,
2023-01-09 14:53:52 +00:00
logger: Logger,
): Promise<{
codeqlFolder: string;
statusReport: ToolsDownloadStatusReport;
toolsVersion: string;
}> {
2023-01-09 14:53:52 +00:00
const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers: OutgoingHttpHeaders = {
accept: "application/octet-stream",
};
let authorization: string | undefined = undefined;
2025-09-24 15:50:19 +01:00
// We don't want to send an authorization header if there's already a token provided in the URL.
2023-01-09 14:53:52 +00:00
if (searchParams.has("token")) {
logger.debug("CodeQL tools URL contains an authorization token.");
} else {
authorization = api.getAuthorizationHeaderFor(
logger,
apiDetails,
codeqlURL,
);
2023-01-09 14:53:52 +00:00
}
const toolcacheInfo = getToolcacheDestinationInfo(
maybeBundleVersion,
maybeCliVersion,
logger,
);
2025-08-08 11:26:14 +01:00
const extractedBundlePath =
toolcacheInfo?.path ?? getTempExtractionDir(tempDir);
await tryDeleteToolcacheBundles(toolcacheInfo?.version, features, logger);
2025-08-08 11:26:14 +01:00
const statusReport = await downloadAndExtract(
2023-01-09 14:53:52 +00:00
codeqlURL,
compressionMethod,
extractedBundlePath,
authorization,
{ "User-Agent": "CodeQL Action", ...headers },
tarVersion,
logger,
2023-01-09 14:53:52 +00:00
);
2023-08-01 17:49:21 +01:00
if (!toolcacheInfo) {
logger.debug(
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
`URL ${codeqlURL}.`,
);
return {
2023-08-01 19:21:17 +01:00
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? "unknown",
};
}
2025-08-08 11:26:14 +01:00
writeToolcacheMarkerFile(toolcacheInfo.path, logger);
2023-08-01 18:50:45 +01:00
return {
2025-08-08 11:26:14 +01:00
codeqlFolder: extractedBundlePath,
statusReport,
toolsVersion: maybeCliVersion ?? toolcacheInfo.version,
};
};
2023-01-09 14:53:52 +00:00
function getToolcacheDestinationInfo(
maybeBundleVersion: string | undefined,
maybeCliVersion: string | undefined,
logger: Logger,
): { path: string; version: string } | undefined {
if (maybeBundleVersion) {
const version = getCanonicalToolcacheVersion(
maybeCliVersion,
maybeBundleVersion,
logger,
);
return {
path: getToolcacheDirectory(version),
version,
};
}
return undefined;
}
/**
* Reclaims disk space by deleting the CodeQL tools from the toolcache, if enabled.
*
* On GitHub-hosted runners the toolcache shares a filesystem with the workspace, so tools left in
* the toolcache take up space that the analysis could use instead.
*
* @param destinationVersion The toolcache version number that the tools will be stored under, or
* `undefined` if they will not be stored in the toolcache.
*/
async function tryDeleteToolcacheBundles(
destinationVersion: string | undefined,
features: FeatureEnablement,
logger: Logger,
): Promise<void> {
// A step that has already obtained the CodeQL tools may hand out a path into the toolcache that a
// later step runs, so only the first step to obtain them can know that nothing else relies on it.
if (util.getOptionalEnvVar(EnvVar.HAS_OBTAINED_CODEQL_TOOLS) !== undefined) {
logger.debug(
"Not deleting the CodeQL tools from the toolcache since a previous step in this job has " +
"already obtained them.",
);
return;
}
// If we are not going to add the tools to the toolcache, we are extracting them somewhere else
// and emptying the toolcache would not buy us the space we need.
if (
destinationVersion === undefined ||
!isGitHubHostedRunner() ||
!(await features.getValue(Feature.CleanupToolcacheBundles))
) {
return;
}
let result: ToolcacheCleanupResult = { deletedVersions: [], failed: true };
try {
result = await deleteToolcacheBundles(logger);
} catch (e) {
logger.info(
`Unable to reclaim disk space from the toolcache: ${util.getErrorMessage(e)}`,
);
}
addNoLanguageDiagnostic(
undefined,
makeTelemetryDiagnostic(
"codeql-action/toolcache-bundle-cleanup",
"Toolcache CodeQL bundle cleanup",
{ ...result },
),
);
}
2023-01-09 14:53:52 +00:00
export function getCodeQLURLVersion(url: string): string {
const match = url.match(/\/codeql-bundle-(.*)\//);
if (match === null || match.length < 2) {
2024-02-08 09:20:03 -08:00
throw new util.ConfigurationError(
2023-01-09 14:53:52 +00:00
`Malformed tools url: ${url}. Version could not be inferred`,
);
}
return match[1];
}
/**
* Returns the toolcache version number to use to store the bundle with the associated CLI version
* and bundle version.
*
* This is the canonical version number, since toolcaches populated by different versions of the
* CodeQL Action or different runner image creation scripts may store the bundle using a different
* version number. Functions like `getCodeQLSource` that fetch the bundle from rather than save the
* bundle to the toolcache should handle these different version numbers.
*/
function getCanonicalToolcacheVersion(
cliVersion: string | undefined,
bundleVersion: string,
logger: Logger,
): string {
// If the CLI version is a pre-release or contains build metadata, then cache the
// bundle as `0.0.0-<bundleVersion>` to avoid the bundle being interpreted as containing a stable
// CLI release. In principle, it should be enough to just check that the CLI version isn't a
// pre-release, but the version numbers of CodeQL nightlies have the format `x.y.z+<timestamp>`,
// and we don't want these nightlies to override stable CLI versions in the toolcache.
if (!cliVersion?.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)) {
return convertToSemVer(bundleVersion, logger);
}
// Bundles are now semantically versioned and can be looked up based on just the CLI version
// number, so we can version them in the toolcache using just the CLI version number.
return cliVersion;
}
2025-11-19 14:59:16 +00:00
interface SetupCodeQLResult {
codeqlFolder: string;
toolsDownloadStatusReport?: ToolsDownloadStatusReport;
toolsSource: ToolsSource;
toolsVersion: string;
}
2023-01-10 13:16:22 +00:00
/**
* Obtains the CodeQL bundle, installs it in the toolcache if appropriate, and extracts it.
2023-01-10 13:16:22 +00:00
*
* @returns the path to the extracted bundle, and the version of the tools
2023-01-10 13:16:22 +00:00
*/
export async function setupCodeQLBundle(
2023-01-10 13:16:22 +00:00
toolsInput: string | undefined,
apiDetails: api.GitHubApiDetails,
tempDir: string,
variant: util.GitHubVariant,
defaultCliVersion: CodeQLDefaultVersionInfo,
rawLanguages: string[] | undefined,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion: boolean,
2025-10-10 13:49:06 +01:00
features: FeatureEnablement,
2023-01-10 13:16:22 +00:00
logger: Logger,
2025-11-19 14:59:16 +00:00
): Promise<SetupCodeQLResult> {
if (!(await util.isBinaryAccessible("tar", logger))) {
throw new util.ConfigurationError(
"Could not find tar in PATH, so unable to extract CodeQL bundle.",
);
}
2024-11-01 15:27:58 +00:00
const zstdAvailability = await tar.isZstdAvailable(logger);
2023-01-10 13:16:22 +00:00
const source = await getCodeQLSource(
toolsInput,
defaultCliVersion,
rawLanguages,
2026-05-07 11:00:54 +01:00
useOverlayAwareDefaultCliVersion,
2023-01-10 13:16:22 +00:00
apiDetails,
variant,
2024-11-01 15:27:58 +00:00
zstdAvailability.available,
2025-10-10 13:49:06 +01:00
features,
2023-01-10 13:16:22 +00:00
logger,
);
2023-01-09 14:53:52 +00:00
2023-01-10 13:16:22 +00:00
let codeqlFolder: string;
let toolsVersion = source.toolsVersion;
let toolsDownloadStatusReport: ToolsDownloadStatusReport | undefined;
let toolsSource: ToolsSource;
2023-01-10 13:16:22 +00:00
switch (source.sourceType) {
case "local": {
2024-10-02 15:32:34 +01:00
codeqlFolder = await tar.extract(
source.codeqlTarPath,
getTempExtractionDir(tempDir),
source.compressionMethod,
2024-10-02 15:32:34 +01:00
zstdAvailability.version,
2024-10-02 15:45:55 +01:00
logger,
2024-10-02 15:32:34 +01:00
);
toolsSource = ToolsSource.Local;
2023-01-10 13:16:22 +00:00
break;
}
2023-01-10 13:16:22 +00:00
case "toolcache":
codeqlFolder = source.codeqlFolder;
logger.debug(`CodeQL found in cache ${codeqlFolder}`);
toolsSource = ToolsSource.Toolcache;
2023-01-10 13:16:22 +00:00
break;
case "download": {
const result = await downloadCodeQL(
2023-01-10 13:16:22 +00:00
source.codeqlURL,
source.compressionMethod,
source.bundleVersion,
source.cliVersion,
2023-01-10 13:16:22 +00:00
apiDetails,
2024-10-02 15:32:34 +01:00
zstdAvailability.version,
2023-01-10 13:16:22 +00:00
tempDir,
features,
2023-01-10 13:16:22 +00:00
logger,
);
toolsVersion = result.toolsVersion;
codeqlFolder = result.codeqlFolder;
toolsDownloadStatusReport = result.statusReport;
toolsSource = ToolsSource.Download;
2023-01-10 13:16:22 +00:00
break;
}
2023-01-10 13:16:22 +00:00
default:
util.assertNever(source);
2023-01-09 14:53:52 +00:00
}
// Record that this job now has a copy of the CodeQL tools, so that a later step doesn't delete
// the toolcache out from under the path we are about to return.
core.exportVariable(EnvVar.HAS_OBTAINED_CODEQL_TOOLS, "true");
return {
codeqlFolder,
toolsDownloadStatusReport,
toolsSource,
toolsVersion,
};
2023-01-09 14:53:52 +00:00
}
2024-09-23 19:35:35 +01:00
async function useZstdBundle(
cliVersion: string,
2024-09-23 21:59:03 +01:00
tarSupportsZstd: boolean,
2024-09-23 19:35:35 +01:00
): Promise<boolean> {
return (
2024-10-10 19:24:32 +01:00
// In testing, gzip performs better than zstd on Windows.
process.platform !== "win32" &&
2024-09-23 21:59:03 +01:00
tarSupportsZstd &&
semver.gte(cliVersion, CODEQL_VERSION_ZSTD_BUNDLE)
2024-09-23 19:35:35 +01:00
);
}
function getTempExtractionDir(tempDir: string) {
return path.join(tempDir, uuidV4());
}
/**
* Get the URL of the latest nightly CodeQL bundle.
*/
async function getNightlyToolsUrl(logger: Logger) {
const zstdAvailability = await tar.isZstdAvailable(logger);
// The nightly is guaranteed to have a zstd bundle
const compressionMethod = (await useZstdBundle(
CODEQL_VERSION_ZSTD_BUNDLE,
zstdAvailability.available,
))
? "zstd"
: "gzip";
try {
// Since nightlies are prereleases, we can't just download the latest release
// on the repository. So instead we need to find the latest pre-release
// version and construct the download URL from that.
const release = await api.getApiClient().rest.repos.listReleases({
owner: CODEQL_NIGHTLIES_REPOSITORY_OWNER,
repo: CODEQL_NIGHTLIES_REPOSITORY_NAME,
per_page: 1,
page: 1,
prerelease: true,
});
const latestRelease = release.data[0];
if (!latestRelease) {
throw new Error("Could not find the latest nightly release.");
}
return `https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${getCodeQLBundleName(compressionMethod)}`;
} catch (e) {
throw new Error(
`Failed to retrieve the latest nightly release: ${util.wrapError(e)}`,
);
}
}
2025-10-03 14:15:55 +01:00
/**
* Gets the latest version of the CodeQL CLI that is available in the toolcache, or `undefined`
* if no CodeQL CLI is available in the toolcache.
*
* @param logger The logger to use.
* @returns The latest version of the CodeQL CLI that is available in the toolcache, or `undefined` if there is none.
*/
export function getLatestToolcacheVersion(logger: Logger): string | undefined {
const allVersions = toolcache
.findAllVersions("CodeQL")
2025-10-06 12:58:00 +01:00
.sort((a, b) => semver.compare(b, a));
2025-10-03 14:15:55 +01:00
logger.debug(
`Found the following versions of the CodeQL tools in the toolcache: ${JSON.stringify(
allVersions,
)}.`,
);
if (allVersions.length > 0) {
const latestToolcacheVersion = allVersions[0];
logger.info(
`CLI version ${latestToolcacheVersion} is the latest version in the toolcache.`,
);
return latestToolcacheVersion;
}
return undefined;
}
function isReservedToolsValue(tools: string): boolean {
2025-09-22 14:01:09 +02:00
return (
CODEQL_BUNDLE_VERSION_ALIAS.includes(tools) ||
CODEQL_NIGHTLY_TOOLS_INPUTS.includes(tools) ||
tools === CODEQL_TOOLCACHE_INPUT
2025-09-22 14:01:09 +02:00
);
}