mirror of
https://github.com/github/codeql-action.git
synced 2026-10-30 09:37:15 +00:00
Use per-language CodeQL bundles
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -30,8 +30,13 @@ import {
|
||||
Feature,
|
||||
FeatureEnablement,
|
||||
} from "./feature-flags";
|
||||
import { BuiltInLanguage } from "./languages";
|
||||
import { Logger } from "./logging";
|
||||
import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching";
|
||||
import {
|
||||
getPerLanguageBundleLanguage,
|
||||
tryGetBundleLanguageFromUrl,
|
||||
} from "./per-language-bundles";
|
||||
import * as tar from "./tar";
|
||||
import {
|
||||
deleteToolcacheBundles,
|
||||
@@ -72,21 +77,40 @@ function getCodeQLBundleExtension(
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the platform component of the CodeQL bundle name for the current platform. */
|
||||
export function getBundlePlatform(): string | undefined {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "win64";
|
||||
case "linux":
|
||||
return process.arch === "arm64" ? "linux-arm64" : "linux64";
|
||||
case "darwin":
|
||||
return "osx64";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the CodeQL bundle asset to download.
|
||||
*
|
||||
* @param compressionMethod The compression method of the bundle.
|
||||
* @param language If provided, the name of the bundle that contains only this language, rather than
|
||||
* the name of the combined bundle that contains every language.
|
||||
*/
|
||||
export function getCodeQLBundleName(
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
language?: BuiltInLanguage,
|
||||
): string {
|
||||
const extension = getCodeQLBundleExtension(compressionMethod);
|
||||
const platform = getBundlePlatform();
|
||||
|
||||
let platform: string;
|
||||
if (process.platform === "win32") {
|
||||
platform = "win64";
|
||||
} else if (process.platform === "linux") {
|
||||
platform = process.arch === "arm64" ? "linux-arm64" : "linux64";
|
||||
} else if (process.platform === "darwin") {
|
||||
platform = "osx64";
|
||||
} else {
|
||||
if (platform === undefined) {
|
||||
return `codeql-bundle${extension}`;
|
||||
}
|
||||
if (language !== undefined) {
|
||||
return `codeql-bundle-${language}-${platform}${extension}`;
|
||||
}
|
||||
return `codeql-bundle-${platform}${extension}`;
|
||||
}
|
||||
|
||||
@@ -107,7 +131,7 @@ export function getCodeQLActionRepository(logger: Logger): string {
|
||||
async function getCodeQLBundleDownloadURL(
|
||||
tagName: string,
|
||||
apiDetails: api.GitHubApiDetails,
|
||||
compressionMethod: tar.CompressionMethod,
|
||||
codeQLBundleName: string,
|
||||
logger: Logger,
|
||||
): Promise<string> {
|
||||
const codeQLActionRepository = getCodeQLActionRepository(logger);
|
||||
@@ -126,7 +150,6 @@ async function getCodeQLBundleDownloadURL(
|
||||
return !self.slice(0, index).some((other) => deepEqual(source, other));
|
||||
},
|
||||
);
|
||||
const codeQLBundleName = getCodeQLBundleName(compressionMethod);
|
||||
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.
|
||||
@@ -216,7 +239,15 @@ export function convertToSemVer(version: string, logger: Logger): string {
|
||||
}
|
||||
|
||||
/** Describes the contents and location of a downloadable CodeQL bundle. */
|
||||
type CodeQLBundle = { kind: "combined"; url: string };
|
||||
type CodeQLBundle =
|
||||
| { kind: "combined"; url: string }
|
||||
| {
|
||||
kind: "per-language";
|
||||
url: string;
|
||||
language: BuiltInLanguage;
|
||||
/** Only set when the Action selected the bundle, allowing a same-version fallback. */
|
||||
combinedBundleURL?: string;
|
||||
};
|
||||
|
||||
/** A resolved download, including its bundle identity and version. */
|
||||
export interface CodeQLDownloadSource {
|
||||
@@ -467,6 +498,7 @@ export async function getCodeQLSource(
|
||||
* This does not always include a tag name.
|
||||
*/
|
||||
let url: string | undefined;
|
||||
let bundle: CodeQLBundle | undefined;
|
||||
|
||||
// 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.
|
||||
@@ -475,7 +507,8 @@ export async function getCodeQLSource(
|
||||
const forceNightly = forceNightlyValueFF && canForceNightlyWithFF;
|
||||
|
||||
// For advanced workflows, a value from `CODEQL_NIGHTLY_TOOLS_INPUTS` can be specified explicitly
|
||||
// for the `tools` input in the workflow file.
|
||||
// for the `tools` input. This is the computed input, so it may come from the repository property
|
||||
// rather than the workflow file.
|
||||
const nightlyRequestedByToolsInput =
|
||||
toolsInput !== undefined &&
|
||||
CODEQL_NIGHTLY_TOOLS_INPUTS.includes(toolsInput);
|
||||
@@ -509,7 +542,8 @@ export async function getCodeQLSource(
|
||||
`Using the latest CodeQL CLI nightly, as requested by 'tools: ${toolsInput}'.`,
|
||||
);
|
||||
}
|
||||
toolsInput = await getNightlyToolsUrl(logger);
|
||||
bundle = await getNightlyBundle(rawLanguages, variant, features, logger);
|
||||
toolsInput = bundle.url;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -738,12 +772,42 @@ export async function getCodeQLSource(
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
url = await getCodeQLBundleDownloadURL(
|
||||
tagName!,
|
||||
apiDetails,
|
||||
compressionMethod,
|
||||
const perLanguageBundleLanguage = await getPerLanguageBundleLanguage(
|
||||
{
|
||||
rawLanguages,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
platform: getBundlePlatform(),
|
||||
variant,
|
||||
},
|
||||
features,
|
||||
logger,
|
||||
);
|
||||
|
||||
const resolveBundleURL = (language?: BuiltInLanguage) =>
|
||||
getCodeQLBundleDownloadURL(
|
||||
tagName!,
|
||||
apiDetails,
|
||||
getCodeQLBundleName(compressionMethod, language),
|
||||
logger,
|
||||
);
|
||||
|
||||
if (perLanguageBundleLanguage !== undefined) {
|
||||
logger.info(
|
||||
`Downloading the ${perLanguageBundleLanguage} CodeQL bundle, since ${perLanguageBundleLanguage} ` +
|
||||
"is the only language being analyzed.",
|
||||
);
|
||||
url = await resolveBundleURL(perLanguageBundleLanguage);
|
||||
bundle = {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language: perLanguageBundleLanguage,
|
||||
combinedBundleURL: await resolveBundleURL(),
|
||||
};
|
||||
} else {
|
||||
url = await resolveBundleURL();
|
||||
bundle = { kind: "combined", url };
|
||||
}
|
||||
} else {
|
||||
const method = tar.inferCompressionMethod(url);
|
||||
if (method === undefined) {
|
||||
@@ -753,6 +817,20 @@ export async function getCodeQLSource(
|
||||
);
|
||||
}
|
||||
compressionMethod = method;
|
||||
|
||||
if (bundle === undefined) {
|
||||
// Explicit per-language URLs must also stay out of the toolcache, but have no fallback.
|
||||
const language = tryGetBundleLanguageFromUrl(url);
|
||||
bundle =
|
||||
language === undefined
|
||||
? { kind: "combined", url }
|
||||
: { kind: "per-language", url, language };
|
||||
}
|
||||
if (bundle.kind === "per-language") {
|
||||
logger.info(
|
||||
`${url} appears to be a CodeQL bundle that contains only ${bundle.language}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (cliVersion) {
|
||||
@@ -761,7 +839,7 @@ export async function getCodeQLSource(
|
||||
logger.info(`Using CodeQL CLI sourced from ${url} .`);
|
||||
}
|
||||
return {
|
||||
bundle: { kind: "combined", url },
|
||||
bundle,
|
||||
bundleVersion,
|
||||
cliVersion,
|
||||
compressionMethod,
|
||||
@@ -841,8 +919,11 @@ export const downloadCodeQL = async function (
|
||||
writeToolcacheMarkerFile(toolcacheDestination, logger);
|
||||
} else {
|
||||
logger.debug(
|
||||
"Could not cache CodeQL tools because we could not determine the bundle version from the " +
|
||||
`URL ${codeqlURL}.`,
|
||||
bundle.kind === "per-language"
|
||||
? "Not caching the CodeQL tools because they came from a bundle that contains only a " +
|
||||
"single language."
|
||||
: "Could not cache CodeQL tools because we could not determine the bundle version from the " +
|
||||
`URL ${codeqlURL}.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -860,7 +941,8 @@ function getToolcacheDestination(
|
||||
source: CodeQLDownloadSource,
|
||||
logger: Logger,
|
||||
): string | undefined {
|
||||
if (!source.bundleVersion) {
|
||||
// Per-language bundles must not be stored in the toolcache.
|
||||
if (source.bundle.kind !== "combined" || !source.bundleVersion) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1046,6 +1128,9 @@ export async function setupCodeQLBundle(
|
||||
/**
|
||||
* Performs eligible toolcache cleanup once, then downloads and extracts the resolved bundle.
|
||||
*
|
||||
* If `source` refers to a bundle for a single language and that bundle turns out not to exist, this
|
||||
* falls back to downloading the combined bundle.
|
||||
*
|
||||
* @returns The extraction directory and download timings.
|
||||
*/
|
||||
export async function downloadCodeQLBundle(
|
||||
@@ -1058,14 +1143,60 @@ export async function downloadCodeQLBundle(
|
||||
codeqlFolder: string;
|
||||
statusReport: ToolsDownloadStatusReport;
|
||||
}> {
|
||||
const { bundle } = source;
|
||||
const { logger } = action;
|
||||
|
||||
await tryDeleteToolcacheBundles(action);
|
||||
return await downloadCodeQL(
|
||||
source,
|
||||
apiDetails,
|
||||
tarVersion,
|
||||
tempDir,
|
||||
action.logger,
|
||||
);
|
||||
|
||||
try {
|
||||
const result = await downloadCodeQL(
|
||||
source,
|
||||
apiDetails,
|
||||
tarVersion,
|
||||
tempDir,
|
||||
logger,
|
||||
);
|
||||
return bundle.kind === "combined"
|
||||
? result
|
||||
: {
|
||||
...result,
|
||||
statusReport: {
|
||||
...result.statusReport,
|
||||
bundleLanguage: bundle.language,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
if (
|
||||
bundle.kind !== "per-language" ||
|
||||
bundle.combinedBundleURL === undefined ||
|
||||
util.asHTTPError(e)?.status !== 404
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
logger.warning(
|
||||
`No ${bundle.language} CodeQL bundle was found at ${bundle.url}, so ` +
|
||||
"falling back to the bundle that contains all languages. This analysis will still " +
|
||||
"produce correct results, but will take longer to set up.",
|
||||
);
|
||||
|
||||
const result = await downloadCodeQL(
|
||||
{
|
||||
...source,
|
||||
bundle: { kind: "combined", url: bundle.combinedBundleURL },
|
||||
},
|
||||
apiDetails,
|
||||
tarVersion,
|
||||
tempDir,
|
||||
logger,
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
statusReport: {
|
||||
...result.statusReport,
|
||||
perLanguageBundleFallback: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function useZstdBundle(
|
||||
@@ -1084,10 +1215,13 @@ function getTempExtractionDir(tempDir: string) {
|
||||
return path.join(tempDir, uuidV4());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL of the latest nightly CodeQL bundle.
|
||||
*/
|
||||
async function getNightlyToolsUrl(logger: Logger) {
|
||||
/** Selects a bundle from the latest nightly, with a same-release fallback when applicable. */
|
||||
async function getNightlyBundle(
|
||||
rawLanguages: string[] | undefined,
|
||||
variant: util.GitHubVariant,
|
||||
features: FeatureEnablement,
|
||||
logger: Logger,
|
||||
): Promise<CodeQLBundle> {
|
||||
const zstdAvailability = await tar.isZstdAvailable(logger);
|
||||
// The nightly is guaranteed to have a zstd bundle
|
||||
const compressionMethod = (await useZstdBundle(
|
||||
@@ -1097,6 +1231,19 @@ async function getNightlyToolsUrl(logger: Logger) {
|
||||
? "zstd"
|
||||
: "gzip";
|
||||
|
||||
const language = await getPerLanguageBundleLanguage(
|
||||
{
|
||||
rawLanguages,
|
||||
cliVersion: undefined,
|
||||
compressionMethod,
|
||||
platform: getBundlePlatform(),
|
||||
variant,
|
||||
isNightly: true,
|
||||
},
|
||||
features,
|
||||
logger,
|
||||
);
|
||||
|
||||
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
|
||||
@@ -1112,7 +1259,17 @@ async function getNightlyToolsUrl(logger: Logger) {
|
||||
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)}`;
|
||||
const assetUrl = (name: string) =>
|
||||
`https://github.com/${CODEQL_NIGHTLIES_REPOSITORY_OWNER}/${CODEQL_NIGHTLIES_REPOSITORY_NAME}/releases/download/${latestRelease.tag_name}/${name}`;
|
||||
const url = assetUrl(getCodeQLBundleName(compressionMethod, language));
|
||||
return language === undefined
|
||||
? { kind: "combined", url }
|
||||
: {
|
||||
kind: "per-language",
|
||||
url,
|
||||
language,
|
||||
combinedBundleURL: assetUrl(getCodeQLBundleName(compressionMethod)),
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to retrieve the latest nightly release: ${util.wrapError(e)}`,
|
||||
|
||||
Reference in New Issue
Block a user