codeql-action/lib/setup-codeql.js

562 строки
26 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupCodeQLBundle = exports.getCodeQLURLVersion = exports.downloadCodeQL = exports.tryGetFallbackToolcacheVersion = exports.getCodeQLSource = exports.convertToSemVer = exports.tryGetBundleVersionFromUrl = exports.tryFindCliVersionDotcomOnly = exports.findCodeQLBundleTagDotcomOnly = exports.getCodeQLActionRepository = exports.CODEQL_DEFAULT_ACTION_REPOSITORY = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const perf_hooks_1 = require("perf_hooks");
const toolcache = __importStar(require("@actions/tool-cache"));
const fast_deep_equal_1 = __importDefault(require("fast-deep-equal"));
const semver = __importStar(require("semver"));
const uuid_1 = require("uuid");
const actions_util_1 = require("./actions-util");
const api = __importStar(require("./api-client"));
// Note: defaults.json is referenced from the CodeQL Action sync tool and the Actions runner image
// creation scripts. Ensure that any changes to the format of this file are compatible with both of
// these dependents.
const defaults = __importStar(require("./defaults.json"));
const init_1 = require("./init");
const util = __importStar(require("./util"));
const util_1 = require("./util");
exports.CODEQL_DEFAULT_ACTION_REPOSITORY = "github/codeql-action";
function getCodeQLBundleName() {
let platform;
if (process.platform === "win32") {
platform = "win64";
}
else if (process.platform === "linux") {
platform = "linux64";
}
else if (process.platform === "darwin") {
platform = "osx64";
}
else {
return "codeql-bundle.tar.gz";
}
return `codeql-bundle-${platform}.tar.gz`;
}
function getCodeQLActionRepository(logger) {
if ((0, actions_util_1.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 exports.CODEQL_DEFAULT_ACTION_REPOSITORY;
}
return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY");
}
exports.getCodeQLActionRepository = getCodeQLActionRepository;
/**
* Gets the tag name and, if known, the CodeQL CLI version for each CodeQL bundle release.
*
* CodeQL bundles are currently tagged in the form `codeql-bundle-yyyymmdd`, so it is not possible
* to directly find the CodeQL bundle release for a particular CLI version or find the CodeQL CLI
* version for a particular CodeQL bundle.
*
* To get around this, we add a `cli-version-x.y.z.txt` asset to each bundle release that specifies
* the CLI version for that bundle release. We can then use the GitHub Releases for the CodeQL
* Action as a source of truth.
*
* In the medium term, we should migrate to a tagging scheme that allows us to directly find the
* CodeQL bundle release for a particular CLI version, for example `codeql-bundle-vx.y.z`.
*/
async function getCodeQLBundleReleasesDotcomOnly(logger) {
logger.debug(`Fetching CodeQL CLI version and CodeQL bundle tag name information for releases of the CodeQL tools.`);
const apiClient = api.getApiClient();
const codeQLActionRepository = getCodeQLActionRepository(logger);
const releases = await apiClient.paginate(apiClient.repos.listReleases, {
owner: codeQLActionRepository.split("/")[0],
repo: codeQLActionRepository.split("/")[1],
});
logger.debug(`Found ${releases.length} releases.`);
return releases.map((release) => ({
cliVersion: tryGetCodeQLCliVersionForRelease(release, logger),
tagName: release.tag_name,
}));
}
function tryGetCodeQLCliVersionForRelease(release, logger) {
const cliVersionsFromMarkerFiles = release.assets
.map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1])
.filter((v) => v)
.map((v) => v);
if (cliVersionsFromMarkerFiles.length > 1) {
logger.warning(`Ignoring release ${release.tag_name} with multiple CLI version marker files.`);
return undefined;
}
else if (cliVersionsFromMarkerFiles.length === 0) {
logger.debug(`Failed to find the CodeQL CLI version for release ${release.tag_name}.`);
return undefined;
}
return cliVersionsFromMarkerFiles[0];
}
async function findCodeQLBundleTagDotcomOnly(cliVersion, logger) {
const filtered = (await getCodeQLBundleReleasesDotcomOnly(logger)).filter((release) => release.cliVersion === cliVersion);
if (filtered.length === 0) {
throw new Error(`Failed to find a release of the CodeQL tools that contains CodeQL CLI ${cliVersion}.`);
}
else if (filtered.length > 1) {
throw new Error(`Found multiple releases of the CodeQL tools that contain CodeQL CLI ${cliVersion}. ` +
`Only one such release should exist.`);
}
return filtered[0].tagName;
}
exports.findCodeQLBundleTagDotcomOnly = findCodeQLBundleTagDotcomOnly;
async function tryFindCliVersionDotcomOnly(tagName, logger) {
try {
logger.debug(`Fetching the GitHub Release for the CodeQL bundle tagged ${tagName}.`);
const apiClient = api.getApiClient();
const codeQLActionRepository = getCodeQLActionRepository(logger);
const release = await apiClient.repos.getReleaseByTag({
owner: codeQLActionRepository.split("/")[0],
repo: codeQLActionRepository.split("/")[1],
tag: tagName,
});
return tryGetCodeQLCliVersionForRelease(release.data, logger);
}
catch (e) {
logger.debug(`Failed to find the CLI version for the CodeQL bundle tagged ${tagName}. ${(0, util_1.wrapError)(e).message}`);
return undefined;
}
}
exports.tryFindCliVersionDotcomOnly = tryFindCliVersionDotcomOnly;
async function getCodeQLBundleDownloadURL(tagName, apiDetails, variant, logger) {
const codeQLActionRepository = getCodeQLActionRepository(logger);
const potentialDownloadSources = [
// This GitHub instance, and this Action.
[apiDetails.url, codeQLActionRepository],
// This GitHub instance, and the canonical Action.
[apiDetails.url, exports.CODEQL_DEFAULT_ACTION_REPOSITORY],
// GitHub.com, and the canonical Action.
[util.GITHUB_DOTCOM_URL, exports.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) => (0, fast_deep_equal_1.default)(source, other));
});
const codeQLBundleName = getCodeQLBundleName();
if (variant === util.GitHubVariant.GHAE) {
try {
const release = await api
.getApiClient()
.request("GET /enterprise/code-scanning/codeql-bundle/find/{tag}", {
tag: tagName,
});
const assetID = release.data.assets[codeQLBundleName];
if (assetID !== undefined) {
const download = await api
.getApiClient()
.request("GET /enterprise/code-scanning/codeql-bundle/download/{asset_id}", { asset_id: assetID });
const downloadURL = download.data.url;
logger.info(`Found CodeQL bundle at GitHub AE endpoint with URL ${downloadURL}.`);
return downloadURL;
}
else {
logger.info(`Attempted to fetch bundle from GitHub AE endpoint but the bundle ${codeQLBundleName} was not found in the assets ${JSON.stringify(release.data.assets)}.`);
}
}
catch (e) {
logger.info(`Attempted to fetch bundle from GitHub AE endpoint but got error ${e}.`);
}
}
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 === exports.CODEQL_DEFAULT_ACTION_REPOSITORY) {
break;
}
const [repositoryOwner, repositoryName] = repository.split("/");
try {
const release = await api.getApiClient().repos.getReleaseByTag({
owner: repositoryOwner,
repo: repositoryName,
tag: tagName,
});
for (const asset of release.data.assets) {
if (asset.name === codeQLBundleName) {
logger.info(`Found CodeQL bundle in ${downloadSource[1]} on ${downloadSource[0]} with URL ${asset.url}.`);
return asset.url;
}
}
}
catch (e) {
logger.info(`Looked for CodeQL bundle in ${downloadSource[1]} on ${downloadSource[0]} but got error ${e}.`);
}
}
return `https://github.com/${exports.CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
}
function tryGetBundleVersionFromTagName(tagName, logger) {
const match = tagName.match(/^codeql-bundle-(.*)$/);
if (match === null || match.length < 2) {
logger.debug(`Could not determine bundle version from tag ${tagName}.`);
return undefined;
}
return match[1];
}
function tryGetTagNameFromUrl(url, logger) {
const match = url.match(/\/(codeql-bundle-.*)\//);
if (match === null || match.length < 2) {
logger.debug(`Could not determine tag name for URL ${url}.`);
return undefined;
}
return match[1];
}
function tryGetBundleVersionFromUrl(url, logger) {
const tagName = tryGetTagNameFromUrl(url, logger);
if (tagName === undefined) {
return undefined;
}
return tryGetBundleVersionFromTagName(tagName, logger);
}
exports.tryGetBundleVersionFromUrl = tryGetBundleVersionFromUrl;
function convertToSemVer(version, logger) {
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;
}
exports.convertToSemVer = convertToSemVer;
/**
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
*/
async function findOverridingToolsInCache(humanReadableVersion, logger) {
const candidates = toolcache
.findAllVersions("CodeQL")
.filter(util_1.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}.`);
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;
}
async function getCodeQLSource(toolsInput, defaultCliVersion, apiDetails, variant, logger) {
if (toolsInput && toolsInput !== "latest" && !toolsInput.startsWith("http")) {
return {
codeqlTarPath: toolsInput,
sourceType: "local",
toolsVersion: "local",
};
}
/**
* Whether the tools shipped with the Action, i.e. those in `defaults.json`, have been forced.
*
* We use the special value of 'latest' 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.
*/
const forceShippedTools = toolsInput === "latest";
if (forceShippedTools) {
logger.info("Overriding the version of the CodeQL tools by the version shipped with the Action since " +
`"tools: latest" was requested.`);
}
/** CLI version number, for example 2.12.1. */
let cliVersion;
/** Tag name of the CodeQL bundle, for example `codeql-bundle-20230120`. */
let tagName;
/**
* URL of the CodeQL bundle.
*
* This does not always include a tag name.
*/
let url;
if (forceShippedTools) {
cliVersion = defaults.cliVersion;
tagName = defaults.bundleVersion;
}
else if (toolsInput !== undefined) {
// If a tools URL was provided, then use that.
tagName = tryGetTagNameFromUrl(toolsInput, logger);
url = toolsInput;
}
else {
// Otherwise, use the default CLI version passed in.
cliVersion = defaultCliVersion.cliVersion;
tagName = defaultCliVersion["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"}.`);
let codeqlFolder;
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 && (cliVersion || tagName)) {
if (cliVersion || tagName) {
const fallbackVersion = await tryGetFallbackToolcacheVersion(cliVersion, tagName, variant, logger);
if (fallbackVersion) {
codeqlFolder = toolcache.find("CodeQL", fallbackVersion);
}
else {
logger.debug("Could not determine a fallback toolcache version number for CodeQL tools version " +
`${humanReadableVersion}.`);
}
}
else {
logger.debug("Both the CLI version and the bundle version are unknown, so we will not be able to find " +
"the requested version of the CodeQL tools in the toolcache.");
}
}
if (codeqlFolder) {
logger.info(`Found CodeQL tools version ${humanReadableVersion} in the toolcache.`);
}
else {
logger.info(`Did not find CodeQL tools version ${humanReadableVersion} in the toolcache.`);
}
if (codeqlFolder) {
return {
codeqlFolder,
sourceType: "toolcache",
toolsVersion: cliVersion ?? humanReadableVersion,
};
}
// If we don't find the requested version on Enterprise, we may allow a
// different version to save download time if the version hasn't been
// specified explicitly (in which case we always honor it).
if (variant !== util.GitHubVariant.DOTCOM &&
!forceShippedTools &&
!toolsInput) {
const result = await findOverridingToolsInCache(humanReadableVersion, logger);
if (result !== undefined) {
return result;
}
}
if (!url) {
if (!tagName && cliVersion && variant === util.GitHubVariant.DOTCOM) {
tagName = await findCodeQLBundleTagDotcomOnly(cliVersion, logger);
}
else if (!tagName) {
throw new Error(`Could not obtain the requested version (${humanReadableVersion}) of the CodeQL tools ` +
"since we could not compute the tag name.");
}
url = await getCodeQLBundleDownloadURL(tagName, apiDetails, variant, logger);
}
return {
bundleVersion: tagName && tryGetBundleVersionFromTagName(tagName, logger),
cliVersion,
codeqlURL: url,
sourceType: "download",
toolsVersion: cliVersion ?? humanReadableVersion,
};
}
exports.getCodeQLSource = getCodeQLSource;
/**
* 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.
*/
async function tryGetFallbackToolcacheVersion(cliVersion, tagName, variant, logger) {
//
// If we are on Dotcom, we will make an HTTP request to the Releases API here
// to find the tag name for the requested version.
if (cliVersion && !tagName && variant === util.GitHubVariant.DOTCOM) {
tagName = await findCodeQLBundleTagDotcomOnly(cliVersion, logger);
}
if (!tagName) {
return 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;
}
exports.tryGetFallbackToolcacheVersion = tryGetFallbackToolcacheVersion;
async function downloadCodeQL(codeqlURL, maybeBundleVersion, maybeCliVersion, apiDetails, variant, tempDir, logger) {
const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers = {
accept: "application/octet-stream",
};
// We only want to provide an authorization header if we are downloading
// from the same GitHub instance the Action is running on.
// This avoids leaking Enterprise tokens to dotcom.
// We also don't want to send an authorization header if there's already a token provided in the URL.
let authorization = undefined;
if (searchParams.has("token")) {
logger.debug("CodeQL tools URL contains an authorization token.");
}
else if (codeqlURL.startsWith(`${apiDetails.url}/`)) {
logger.debug("Providing an authorization token to download CodeQL tools.");
authorization = `token ${apiDetails.auth}`;
}
else {
logger.debug("Downloading CodeQL tools without an authorization token.");
}
logger.info(`Downloading CodeQL tools from ${codeqlURL}. This may take a while.`);
const dest = path.join(tempDir, (0, uuid_1.v4)());
const finalHeaders = Object.assign({ "User-Agent": "CodeQL Action" }, headers);
const toolsDownloadStart = perf_hooks_1.performance.now();
const codeqlPath = await toolcache.downloadTool(codeqlURL, dest, authorization, finalHeaders);
const toolsDownloadDurationMs = Math.round(perf_hooks_1.performance.now() - toolsDownloadStart);
logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`);
const codeqlExtracted = await toolcache.extractTar(codeqlPath);
const bundleVersion = maybeBundleVersion ?? tryGetBundleVersionFromUrl(codeqlURL, logger);
if (bundleVersion === undefined) {
logger.debug("Could not cache CodeQL tools because we could not determine the bundle version from the " +
`URL ${codeqlURL}.`);
return {
toolsVersion: maybeCliVersion ?? "unknown",
codeqlFolder: codeqlExtracted,
toolsDownloadDurationMs,
};
}
// Try to compute the CLI version for this bundle
if (maybeCliVersion === undefined &&
variant === util.GitHubVariant.DOTCOM &&
codeqlURL.includes(`/${exports.CODEQL_DEFAULT_ACTION_REPOSITORY}/`)) {
maybeCliVersion = await tryFindCliVersionDotcomOnly(`codeql-bundle-${bundleVersion}`, logger);
}
// Include both the CLI version and the bundle version in the toolcache version number. That way
// if the user requests the same URL again, we can get it from the cache without having to call
// any of the Releases API.
//
// Special case: 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.
const toolcacheVersion = maybeCliVersion?.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)
? `${maybeCliVersion}-${bundleVersion}`
: convertToSemVer(bundleVersion, logger);
return {
toolsVersion: maybeCliVersion ?? toolcacheVersion,
codeqlFolder: await toolcache.cacheDir(codeqlExtracted, "CodeQL", toolcacheVersion),
toolsDownloadDurationMs,
};
}
exports.downloadCodeQL = downloadCodeQL;
function getCodeQLURLVersion(url) {
const match = url.match(/\/codeql-bundle-(.*)\//);
if (match === null || match.length < 2) {
throw new Error(`Malformed tools url: ${url}. Version could not be inferred`);
}
return match[1];
}
exports.getCodeQLURLVersion = getCodeQLURLVersion;
/**
* Obtains the CodeQL bundle, installs it in the toolcache if appropriate, and extracts it.
*
* @param toolsInput
* @param apiDetails
* @param tempDir
* @param variant
* @param defaultCliVersion
* @param logger
* @param checkVersion Whether to check that CodeQL CLI meets the minimum
* version requirement. Must be set to true outside tests.
* @returns the path to the extracted bundle, and the version of the tools
*/
async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, defaultCliVersion, logger) {
const source = await getCodeQLSource(toolsInput, defaultCliVersion, apiDetails, variant, logger);
let codeqlFolder;
let toolsVersion = source.toolsVersion;
let toolsDownloadDurationMs;
let toolsSource;
switch (source.sourceType) {
case "local":
codeqlFolder = await toolcache.extractTar(source.codeqlTarPath);
toolsSource = init_1.ToolsSource.Local;
break;
case "toolcache":
codeqlFolder = source.codeqlFolder;
logger.debug(`CodeQL found in cache ${codeqlFolder}`);
toolsSource = init_1.ToolsSource.Toolcache;
break;
case "download": {
const result = await downloadCodeQL(source.codeqlURL, source.bundleVersion, source.cliVersion, apiDetails, variant, tempDir, logger);
toolsVersion = result.toolsVersion;
codeqlFolder = result.codeqlFolder;
toolsDownloadDurationMs = result.toolsDownloadDurationMs;
toolsSource = init_1.ToolsSource.Download;
break;
}
default:
util.assertNever(source);
}
return { codeqlFolder, toolsDownloadDurationMs, toolsSource, toolsVersion };
}
exports.setupCodeQLBundle = setupCodeQLBundle;
//# sourceMappingURL=setup-codeql.js.map