Files
codeql-action/lib/setup-codeql.js
Arthur Baars 2fed02cbe2 Supply authorization parameter to toolcache.downloadTool()
Previously we supplied the authorization information via the 'headers'
parameter. This works fine, except in some cases when the request is
retried.
2023-02-03 14:56:00 +01:00

510 lines
24 KiB
JavaScript
Generated

"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.getCodeQLSource = exports.convertToSemVer = exports.getBundleVersionFromUrl = 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}. ${e instanceof Error ? e.message : e}`);
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 getBundleVersionFromTagName(tagName) {
const match = tagName.match(/^codeql-bundle-(.*)$/);
if (match === null || match.length < 2) {
throw new Error(`Malformed bundle tag name: ${tagName}. Bundle version could not be inferred`);
}
return match[1];
}
function getBundleVersionFromUrl(url) {
const match = url.match(/\/(codeql-bundle-.*)\//);
if (match === null || match.length < 2) {
throw new Error(`Malformed tools url: ${url}. Bundle version could not be inferred`);
}
return getBundleVersionFromTagName(match[1]);
}
exports.getBundleVersionFromUrl = getBundleVersionFromUrl;
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;
async function getOrFindBundleTagName(version, logger) {
if (version.variant === util.GitHubVariant.DOTCOM) {
return await findCodeQLBundleTagDotcomOnly(version.cliVersion, logger);
}
else {
return version.tagName;
}
}
/**
* Look for a version of the CodeQL tools in the cache which could override the requested CLI version.
*/
async function findOverridingToolsInCache(requestedCliVersion, 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 ${requestedCliVersion}.`);
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, bypassToolcache, defaultCliVersion, apiDetails, variant, logger) {
if (toolsInput && toolsInput !== "latest" && !toolsInput.startsWith("http")) {
return {
codeqlTarPath: toolsInput,
sourceType: "local",
toolsVersion: "local",
};
}
const forceLatestReason =
// We use the special value of 'latest' to prioritize the version in the
// defaults over any pinned cached version.
toolsInput === "latest"
? '"tools: latest" was requested'
: // If the user hasn't requested a particular CodeQL version, then bypass
// the toolcache when the appropriate feature is enabled. This
// allows us to quickly rollback a broken bundle that has made its way
// into the toolcache.
toolsInput === undefined && bypassToolcache
? "a specific version of the CodeQL tools was not requested and the bypass toolcache feature is enabled"
: undefined;
const forceLatest = forceLatestReason !== undefined;
if (forceLatest) {
logger.debug(`Forcing the latest version of the CodeQL tools since ${forceLatestReason}.`);
}
/**
* The requested version is:
*
* 1. The one in `defaults.json`, if forceLatest is true.
* 2. The version specified by the tools input URL, if one was provided.
* 3. The default CLI version, otherwise.
* We include a `variant` property to let us verify using the type system that
* `tagName` is only undefined when the variant is Dotcom. This lets us ensure
* that we can always compute `tagName`, either by using the existing tag name
* on enterprise instances, or calling `findCodeQLBundleTagDotcomOnly` on
* Dotcom.
*/
const requestedVersion = forceLatest
? // case 1
{
cliVersion: defaults.cliVersion,
syntheticCliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion,
variant,
}
: toolsInput !== undefined
? // case 2
{
syntheticCliVersion: convertToSemVer(getBundleVersionFromUrl(toolsInput), logger),
tagName: `codeql-bundle-${getBundleVersionFromUrl(toolsInput)}`,
url: toolsInput,
variant,
}
: // case 3
{
...defaultCliVersion,
syntheticCliVersion: defaultCliVersion.cliVersion,
};
// If we find the specified version, we always use that.
let codeqlFolder = toolcache.find("CodeQL", requestedVersion.syntheticCliVersion);
let tagName = requestedVersion["tagName"];
if (!codeqlFolder) {
logger.debug("Didn't find a version of the CodeQL tools in the toolcache with a version number " +
`exactly matching ${requestedVersion.syntheticCliVersion}.`);
if (requestedVersion.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(`${requestedVersion.cliVersion}-`));
if (candidateVersions.length === 1) {
logger.debug("Exactly one candidate version found, using that.");
codeqlFolder = toolcache.find("CodeQL", candidateVersions[0]);
}
else {
logger.debug("Did not find exactly one version of the CodeQL tools starting with the requested version.");
}
}
}
if (!codeqlFolder && requestedVersion.cliVersion) {
// Fall back to accepting a `0.0.0-<bundleVersion>` version if we didn't find the
// `x.y.z` version. This is to support old versions of the toolcache.
//
// 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.
tagName =
tagName || (await getOrFindBundleTagName(requestedVersion, logger));
const fallbackVersion = convertToSemVer(getBundleVersionFromTagName(tagName), logger);
logger.debug(`Computed a fallback toolcache version number of ${fallbackVersion} for CodeQL tools version ` +
`${requestedVersion.cliVersion}.`);
codeqlFolder = toolcache.find("CodeQL", fallbackVersion);
}
if (codeqlFolder) {
return {
codeqlFolder,
sourceType: "toolcache",
toolsVersion: requestedVersion.syntheticCliVersion,
};
}
logger.debug(`Did not find CodeQL tools version ${requestedVersion.syntheticCliVersion} in the toolcache.`);
// 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 && !forceLatest && !toolsInput) {
const result = await findOverridingToolsInCache(requestedVersion.syntheticCliVersion, logger);
if (result !== undefined) {
return result;
}
}
return {
cliVersion: requestedVersion.cliVersion || undefined,
codeqlURL: requestedVersion["url"] ||
(await getCodeQLBundleDownloadURL(tagName ||
// The check on `requestedVersion.tagName` is redundant but lets us
// use the property that if we don't know `requestedVersion.tagName`,
// then we must know `requestedVersion.cliVersion`. This property is
// required by the type of `getOrFindBundleTagName`.
(requestedVersion.tagName !== undefined
? requestedVersion.tagName
: await getOrFindBundleTagName(requestedVersion, logger)), apiDetails, variant, logger)),
sourceType: "download",
toolsVersion: requestedVersion.syntheticCliVersion,
};
}
exports.getCodeQLSource = getCodeQLSource;
async function downloadCodeQL(codeqlURL, 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 = getBundleVersionFromUrl(codeqlURL);
// Try to compute the CLI version for this bundle
const cliVersion = maybeCliVersion ||
(variant === util.GitHubVariant.DOTCOM &&
(await tryFindCliVersionDotcomOnly(`codeql-bundle-${bundleVersion}`, logger))) ||
undefined;
// 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 = cliVersion && cliVersion.match(/^[0-9]+\.[0-9]+\.[0-9]+$/)
? `${cliVersion}-${bundleVersion}`
: convertToSemVer(bundleVersion, logger);
return {
toolsVersion: cliVersion || 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 bypassToolcache
* @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, bypassToolcache, defaultCliVersion, logger) {
const source = await getCodeQLSource(toolsInput, bypassToolcache, 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.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