Cache explicitly requested bundles with their URL if possible

This commit is contained in:
Henry Mercer 2023-01-12 20:44:05 +00:00
parent c9b1be5115
commit c2e39e078f
9 changed files with 396 additions and 154 deletions

73
lib/codeql.test.js generated
View file

@ -92,7 +92,7 @@ ava_1.default.beforeEach(() => {
* @returns the download URL for the bundle. This can be passed to the tools parameter of * @returns the download URL for the bundle. This can be passed to the tools parameter of
* `codeql.setupCodeQL`. * `codeql.setupCodeQL`.
*/ */
async function mockDownloadApi({ apiDetails = sampleApiDetails, isPinned, tagName, }) { function mockDownloadApi({ apiDetails = sampleApiDetails, isPinned, tagName, }) {
var _a; var _a;
const platform = process.platform === "win32" const platform = process.platform === "win32"
? "win64" ? "win64"
@ -109,18 +109,43 @@ async function mockDownloadApi({ apiDetails = sampleApiDetails, isPinned, tagNam
return `${baseUrl}${relativeUrl}`; return `${baseUrl}${relativeUrl}`;
} }
async function installIntoToolcache({ apiDetails = sampleApiDetails, cliVersion, isPinned, tagName, tmpDir, }) { async function installIntoToolcache({ apiDetails = sampleApiDetails, cliVersion, isPinned, tagName, tmpDir, }) {
const url = await mockDownloadApi({ apiDetails, isPinned, tagName }); const url = mockDownloadApi({ apiDetails, isPinned, tagName });
await codeql.setupCodeQL(cliVersion !== undefined ? undefined : url, apiDetails, tmpDir, util.GitHubVariant.GHES, false, cliVersion !== undefined await codeql.setupCodeQL(cliVersion !== undefined ? undefined : url, apiDetails, tmpDir, util.GitHubVariant.GHES, false, cliVersion !== undefined
? { cliVersion, tagName, variant: util.GitHubVariant.GHES } ? { cliVersion, tagName, variant: util.GitHubVariant.GHES }
: SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false); : SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false);
} }
function mockReleaseApi({ apiDetails = sampleApiDetails, assetNames, tagName, }) {
return (0, nock_1.default)(apiDetails.apiURL)
.get(`/repos/github/codeql-action/releases/tags/${tagName}`)
.reply(200, {
assets: assetNames.map((name) => ({
name,
})),
tag_name: tagName,
});
}
function mockApiDetails(apiDetails) {
// This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to
// mock this directly. The difficulty is that `getApiDetails()` is called locally in
// `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(apiDetails.auth);
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
requiredEnvParamStub.withArgs("GITHUB_SERVER_URL").returns(apiDetails.url);
requiredEnvParamStub
.withArgs("GITHUB_API_URL")
.returns(apiDetails.apiURL || "");
}
(0, ava_1.default)("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => { (0, ava_1.default)("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => {
await util.withTmpDir(async (tmpDir) => { await util.withTmpDir(async (tmpDir) => {
(0, testing_utils_1.setupActionsVars)(tmpDir, tmpDir); (0, testing_utils_1.setupActionsVars)(tmpDir, tmpDir);
const versions = ["20200601", "20200610"]; const versions = ["20200601", "20200610"];
for (let i = 0; i < versions.length; i++) { for (let i = 0; i < versions.length; i++) {
const version = versions[i]; const version = versions[i];
const url = await mockDownloadApi({ const url = mockDownloadApi({
tagName: `codeql-bundle-${version}`, tagName: `codeql-bundle-${version}`,
isPinned: false, isPinned: false,
}); });
@ -139,7 +164,7 @@ async function installIntoToolcache({ apiDetails = sampleApiDetails, cliVersion,
isPinned: true, isPinned: true,
tmpDir, tmpDir,
}); });
const url = await mockDownloadApi({ const url = mockDownloadApi({
tagName: "codeql-bundle-20200610", tagName: "codeql-bundle-20200610",
}); });
const result = await codeql.setupCodeQL(url, sampleApiDetails, tmpDir, util.GitHubVariant.DOTCOM, false, SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false); const result = await codeql.setupCodeQL(url, sampleApiDetails, tmpDir, util.GitHubVariant.DOTCOM, false, SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false);
@ -147,6 +172,24 @@ async function installIntoToolcache({ apiDetails = sampleApiDetails, cliVersion,
t.deepEqual(result.toolsVersion, "0.0.0-20200610"); t.deepEqual(result.toolsVersion, "0.0.0-20200610");
}); });
}); });
(0, ava_1.default)("tries to cache an explicitly requested bundle with its CLI version number", async (t) => {
await util.withTmpDir(async (tmpDir) => {
(0, testing_utils_1.setupActionsVars)(tmpDir, tmpDir);
mockApiDetails(sampleApiDetails);
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const releaseApiMock = mockReleaseApi({
assetNames: ["cli-version-2.10.0.txt"],
tagName: "codeql-bundle-20200610",
});
const url = mockDownloadApi({
tagName: "codeql-bundle-20200610",
});
const result = await codeql.setupCodeQL(url, sampleApiDetails, tmpDir, util.GitHubVariant.DOTCOM, false, SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false);
t.assert(releaseApiMock.isDone(), "Releases API should have been called");
t.assert(toolcache.find("CodeQL", "2.10.0"));
t.deepEqual(result.toolsVersion, "0.0.0-20200610");
});
});
for (const { isCached, tagName, toolcacheCliVersion } of [ for (const { isCached, tagName, toolcacheCliVersion } of [
{ {
isCached: true, isCached: true,
@ -178,7 +221,7 @@ for (const { isCached, tagName, toolcacheCliVersion } of [
}); });
} }
else { else {
await mockDownloadApi({ mockDownloadApi({
tagName, tagName,
}); });
sinon.stub(api, "getApiClient").value(() => ({ sinon.stub(api, "getApiClient").value(() => ({
@ -229,7 +272,7 @@ for (const variant of [util.GitHubVariant.GHAE, util.GitHubVariant.GHES]) {
isPinned: false, isPinned: false,
tmpDir, tmpDir,
}); });
await mockDownloadApi({ mockDownloadApi({
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
}); });
const result = await codeql.setupCodeQL(undefined, sampleApiDetails, tmpDir, variant, false, { const result = await codeql.setupCodeQL(undefined, sampleApiDetails, tmpDir, variant, false, {
@ -251,7 +294,7 @@ for (const variant of [util.GitHubVariant.GHAE, util.GitHubVariant.GHES]) {
isPinned: true, isPinned: true,
tmpDir, tmpDir,
}); });
await mockDownloadApi({ mockDownloadApi({
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
}); });
const result = await codeql.setupCodeQL("latest", sampleApiDetails, tmpDir, util.GitHubVariant.DOTCOM, false, SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false); const result = await codeql.setupCodeQL("latest", sampleApiDetails, tmpDir, util.GitHubVariant.DOTCOM, false, SAMPLE_DEFAULT_CLI_VERSION, (0, logging_1.getRunnerLogger)(true), false);
@ -283,21 +326,7 @@ for (const variant of [util.GitHubVariant.GHAE, util.GitHubVariant.GHES]) {
(0, nock_1.default)("https://example.githubenterprise.com") (0, nock_1.default)("https://example.githubenterprise.com")
.get(`/github/codeql-action/releases/download/${defaults.bundleVersion}/${codeQLBundleName}`) .get(`/github/codeql-action/releases/download/${defaults.bundleVersion}/${codeQLBundleName}`)
.replyWithFile(200, path_1.default.join(__dirname, `/../src/testdata/codeql-bundle-pinned.tar.gz`)); .replyWithFile(200, path_1.default.join(__dirname, `/../src/testdata/codeql-bundle-pinned.tar.gz`));
// This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to mockApiDetails(sampleGHAEApiDetails);
// mock this directly. The difficulty is that `getApiDetails()` is called locally in
// `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(sampleGHAEApiDetails.auth);
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
requiredEnvParamStub
.withArgs("GITHUB_SERVER_URL")
.returns(sampleGHAEApiDetails.url);
requiredEnvParamStub
.withArgs("GITHUB_API_URL")
.returns(sampleGHAEApiDetails.apiURL);
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
process.env["GITHUB_ACTION_REPOSITORY"] = "github/codeql-action"; process.env["GITHUB_ACTION_REPOSITORY"] = "github/codeql-action";
await codeql.setupCodeQL(undefined, sampleGHAEApiDetails, tmpDir, util.GitHubVariant.GHAE, false, { await codeql.setupCodeQL(undefined, sampleGHAEApiDetails, tmpDir, util.GitHubVariant.GHAE, false, {

File diff suppressed because one or more lines are too long

154
lib/setup-codeql.js generated
View file

@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.setupCodeQLBundle = exports.getCodeQLURLVersion = exports.downloadCodeQL = exports.getCodeQLSource = exports.convertToSemVer = exports.getBundleTagNameFromUrl = exports.findCodeQLBundleTagDotcomOnly = exports.getCodeQLActionRepository = exports.CODEQL_DEFAULT_ACTION_REPOSITORY = void 0; 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 fs = __importStar(require("fs"));
const path = __importStar(require("path")); const path = __importStar(require("path"));
const toolcache = __importStar(require("@actions/tool-cache")); const toolcache = __importStar(require("@actions/tool-cache"));
@ -66,17 +66,21 @@ function getCodeQLActionRepository(logger) {
} }
exports.getCodeQLActionRepository = getCodeQLActionRepository; exports.getCodeQLActionRepository = getCodeQLActionRepository;
/** /**
* CodeQL bundles are currently tagged in the form `codeql-bundle-yyyymmdd`, so it is not possible * Gets the tag name and, if known, the CodeQL CLI version for each CodeQL bundle release.
* to directly find the CodeQL bundle release for a particular CLI version.
* *
* To get around this, we add a `codeql-version-x.y.z.txt` asset to each bundle release that * CodeQL bundles are currently tagged in the form `codeql-bundle-yyyymmdd`, so it is not possible
* specifies the CLI version for that bundle release. * 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 * 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`. * CodeQL bundle release for a particular CLI version, for example `codeql-bundle-vx.y.z`.
*/ */
async function findCodeQLBundleTagDotcomOnly(cliVersion, logger) { async function getCodeQLBundleReleasesDotcomOnly(logger) {
logger.debug(`Trying to find the CodeQL bundle release for CLI version ${cliVersion}.`); logger.debug(`Fetching CodeQL CLI version and CodeQL bundle tag name information for releases of the CodeQL tools.`);
const apiClient = api.getApiClient(); const apiClient = api.getApiClient();
const codeQLActionRepository = getCodeQLActionRepository(logger); const codeQLActionRepository = getCodeQLActionRepository(logger);
const releases = await apiClient.paginate(apiClient.repos.listReleases, { const releases = await apiClient.paginate(apiClient.repos.listReleases, {
@ -84,26 +88,65 @@ async function findCodeQLBundleTagDotcomOnly(cliVersion, logger) {
repo: codeQLActionRepository.split("/")[1], repo: codeQLActionRepository.split("/")[1],
}); });
logger.debug(`Found ${releases.length} releases.`); logger.debug(`Found ${releases.length} releases.`);
for (const release of releases) { return releases.flatMap((release) => {
const cliVersionFileVersions = release.assets const cliVersionFileVersions = release.assets
.map((asset) => { var _a; return (_a = asset.name.match(/cli-version-(.*)\.txt/)) === null || _a === void 0 ? void 0 : _a[1]; }) .map((asset) => { var _a; return (_a = asset.name.match(/cli-version-(.*)\.txt/)) === null || _a === void 0 ? void 0 : _a[1]; })
.filter((v) => v) .filter((v) => v)
.map((v) => v); .map((v) => v);
if (cliVersionFileVersions.length === 0) {
logger.debug(`Ignoring release ${release.tag_name} with no CLI version marker file.`);
continue;
}
if (cliVersionFileVersions.length > 1) { if (cliVersionFileVersions.length > 1) {
logger.warning(`Ignoring release ${release.tag_name} with multiple CLI version marker files.`); logger.warning(`Ignoring release ${release.tag_name} with multiple CLI version marker files.`);
continue; return [];
}
if (cliVersionFileVersions[0] === cliVersion) {
return release.tag_name;
} }
return [
{ cliVersion: cliVersionFileVersions[0], tagName: release.tag_name },
];
});
}
async function tryGetCodeQLCliVersionForRelease(release, logger) {
const cliVersionsFromMarkerFiles = release.assets
.map((asset) => { var _a; return (_a = asset.name.match(/cli-version-(.*)\.txt/)) === null || _a === void 0 ? void 0 : _a[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;
} }
throw new Error(`Failed to find a CodeQL bundle release for CLI version ${cliVersion}.`); 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; 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.error(`Failed to find the CLI version for the CodeQL bundle tagged ${tagName}. Error: ${e instanceof Error ? e.message : e}`);
return undefined;
}
}
exports.tryFindCliVersionDotcomOnly = tryFindCliVersionDotcomOnly;
async function getCodeQLBundleDownloadURL(tagName, apiDetails, variant, logger) { async function getCodeQLBundleDownloadURL(tagName, apiDetails, variant, logger) {
const codeQLActionRepository = getCodeQLActionRepository(logger); const codeQLActionRepository = getCodeQLActionRepository(logger);
const potentialDownloadSources = [ const potentialDownloadSources = [
@ -171,14 +214,14 @@ async function getCodeQLBundleDownloadURL(tagName, apiDetails, variant, logger)
} }
return `https://github.com/${exports.CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; return `https://github.com/${exports.CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
} }
function getBundleTagNameFromUrl(url) { function getBundleVersionFromUrl(url) {
const match = url.match(/\/codeql-bundle-(.*)\//); const match = url.match(/\/codeql-bundle-(.*)\//);
if (match === null || match.length < 2) { if (match === null || match.length < 2) {
throw new Error(`Malformed tools url: ${url}. Tag name could not be inferred`); throw new Error(`Malformed tools url: ${url}. Bundle version could not be inferred`);
} }
return match[1]; return match[1];
} }
exports.getBundleTagNameFromUrl = getBundleTagNameFromUrl; exports.getBundleVersionFromUrl = getBundleVersionFromUrl;
function convertToSemVer(version, logger) { function convertToSemVer(version, logger) {
if (!semver.valid(version)) { if (!semver.valid(version)) {
logger.debug(`Bundle version ${version} is not in SemVer format. Will treat it as pre-release 0.0.0-${version}.`); logger.debug(`Bundle version ${version} is not in SemVer format. Will treat it as pre-release 0.0.0-${version}.`);
@ -270,39 +313,45 @@ async function getCodeQLSource(toolsInput, bypassToolcache, defaultCliVersion, a
? // case 1 ? // case 1
{ {
cliVersion: defaults.cliVersion, cliVersion: defaults.cliVersion,
syntheticCliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
variant, variant,
} }
: toolsInput !== undefined : toolsInput !== undefined
? // case 2 ? // case 2
{ {
cliVersion: convertToSemVer(getBundleTagNameFromUrl(toolsInput), logger), syntheticCliVersion: convertToSemVer(getBundleVersionFromUrl(toolsInput), logger),
tagName: getBundleTagNameFromUrl(toolsInput), tagName: `codeql-bundle-${getBundleVersionFromUrl(toolsInput)}`,
url: toolsInput, url: toolsInput,
variant, variant,
} }
: // case 3 : // case 3
defaultCliVersion; {
...defaultCliVersion,
syntheticCliVersion: defaultCliVersion.cliVersion,
};
// If we find the specified version, we always use that. // If we find the specified version, we always use that.
let codeqlFolder = toolcache.find("CodeQL", requestedVersion.cliVersion); let codeqlFolder = toolcache.find("CodeQL", requestedVersion.syntheticCliVersion);
let tagName = requestedVersion["tagName"]; let tagName = requestedVersion["tagName"];
if (!codeqlFolder) { if (!codeqlFolder) {
logger.debug("Didn't find a version of the CodeQL tools in the toolcache with a version number " + logger.debug("Didn't find a version of the CodeQL tools in the toolcache with a version number " +
`exactly matching ${requestedVersion.cliVersion}.`); `exactly matching ${requestedVersion.syntheticCliVersion}.`);
const allVersions = toolcache.findAllVersions("CodeQL"); if (requestedVersion.cliVersion) {
logger.debug(`Found the following versions of the CodeQL tools in the toolcache: ${JSON.stringify(allVersions)}.`); const allVersions = toolcache.findAllVersions("CodeQL");
// If there is exactly one version of the CodeQL tools in the toolcache, and that version is logger.debug(`Found the following versions of the CodeQL tools in the toolcache: ${JSON.stringify(allVersions)}.`);
// the form `x.y.z-<tagName>`, then use it. // If there is exactly one version of the CodeQL tools in the toolcache, and that version is
const candidateVersions = allVersions.filter((version) => version.startsWith(`${requestedVersion.cliVersion}-`)); // the form `x.y.z-<tagName>`, then use it.
if (candidateVersions.length === 1) { const candidateVersions = allVersions.filter((version) => version.startsWith(`${requestedVersion.cliVersion}-`));
logger.debug("Exactly one candidate version found, using that."); if (candidateVersions.length === 1) {
codeqlFolder = toolcache.find("CodeQL", candidateVersions[0]); 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."); else {
logger.debug("Did not find exactly one version of the CodeQL tools starting with the requested version.");
}
} }
} }
if (!codeqlFolder && !requestedVersion.cliVersion.startsWith("0.0.0")) { if (!codeqlFolder && requestedVersion.cliVersion) {
// Fall back to accepting a `0.0.0-<tagName>` version if we didn't find the // Fall back to accepting a `0.0.0-<tagName>` version if we didn't find the
// `x.y.z` version. This is to support old versions of the toolcache. // `x.y.z` version. This is to support old versions of the toolcache.
// //
@ -319,29 +368,36 @@ async function getCodeQLSource(toolsInput, bypassToolcache, defaultCliVersion, a
return { return {
codeqlFolder, codeqlFolder,
sourceType: "toolcache", sourceType: "toolcache",
toolsVersion: requestedVersion.cliVersion, toolsVersion: requestedVersion.syntheticCliVersion,
}; };
} }
logger.debug(`Did not find CodeQL tools version ${requestedVersion.cliVersion} in the toolcache.`); 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 // 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 // different version to save download time if the version hasn't been
// specified explicitly (in which case we always honor it). // specified explicitly (in which case we always honor it).
if (variant !== util.GitHubVariant.DOTCOM && !forceLatest && !toolsInput) { if (variant !== util.GitHubVariant.DOTCOM && !forceLatest && !toolsInput) {
const result = await findOverridingToolsInCache(requestedVersion.cliVersion, logger); const result = await findOverridingToolsInCache(requestedVersion.syntheticCliVersion, logger);
if (result !== undefined) { if (result !== undefined) {
return result; return result;
} }
} }
return { return {
cliVersion: requestedVersion.cliVersion || undefined,
codeqlURL: requestedVersion["url"] || codeqlURL: requestedVersion["url"] ||
(await getCodeQLBundleDownloadURL(tagName || (await getOrFindBundleTagName(requestedVersion, logger)), apiDetails, variant, logger)), (await getCodeQLBundleDownloadURL(tagName ||
semanticVersion: requestedVersion.cliVersion, // 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", sourceType: "download",
toolsVersion: requestedVersion.cliVersion, toolsVersion: requestedVersion.syntheticCliVersion,
}; };
} }
exports.getCodeQLSource = getCodeQLSource; exports.getCodeQLSource = getCodeQLSource;
async function downloadCodeQL(codeqlURL, semanticVersion, apiDetails, tempDir, logger) { async function downloadCodeQL(codeqlURL, cliVersion, apiDetails, variant, tempDir, logger) {
const parsedCodeQLURL = new URL(codeqlURL); const parsedCodeQLURL = new URL(codeqlURL);
const searchParams = new URLSearchParams(parsedCodeQLURL.search); const searchParams = new URLSearchParams(parsedCodeQLURL.search);
const headers = { const headers = {
@ -367,7 +423,13 @@ async function downloadCodeQL(codeqlURL, semanticVersion, apiDetails, tempDir, l
const codeqlPath = await toolcache.downloadTool(codeqlURL, dest, undefined, finalHeaders); const codeqlPath = await toolcache.downloadTool(codeqlURL, dest, undefined, finalHeaders);
logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`); logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`);
const codeqlExtracted = await toolcache.extractTar(codeqlPath); const codeqlExtracted = await toolcache.extractTar(codeqlPath);
return await toolcache.cacheDir(codeqlExtracted, "CodeQL", semanticVersion); const bundleVersion = getBundleVersionFromUrl(codeqlURL);
// If we have a CLI version, use that. Otherwise, try to find the CLI version from the GitHub Releases
const toolcacheVersion = cliVersion ||
(variant === util.GitHubVariant.DOTCOM &&
(await tryFindCliVersionDotcomOnly(`codeql-bundle-${bundleVersion}`, logger))) ||
convertToSemVer(bundleVersion, logger);
return await toolcache.cacheDir(codeqlExtracted, "CodeQL", toolcacheVersion);
} }
exports.downloadCodeQL = downloadCodeQL; exports.downloadCodeQL = downloadCodeQL;
function getCodeQLURLVersion(url) { function getCodeQLURLVersion(url) {
@ -404,7 +466,7 @@ async function setupCodeQLBundle(toolsInput, apiDetails, tempDir, variant, bypas
logger.debug(`CodeQL found in cache ${codeqlFolder}`); logger.debug(`CodeQL found in cache ${codeqlFolder}`);
break; break;
case "download": case "download":
codeqlFolder = await downloadCodeQL(source.codeqlURL, source.semanticVersion, apiDetails, tempDir, logger); codeqlFolder = await downloadCodeQL(source.codeqlURL, source.cliVersion, apiDetails, variant, tempDir, logger);
break; break;
default: default:
util.assertNever(source); util.assertNever(source);

File diff suppressed because one or more lines are too long

View file

@ -110,7 +110,7 @@ ava_1.default.beforeEach(() => {
]), ]),
})); }));
await t.throwsAsync(async () => await setupCodeql.findCodeQLBundleTagDotcomOnly("2.12.1", (0, logging_1.getRunnerLogger)(true)), { await t.throwsAsync(async () => await setupCodeql.findCodeQLBundleTagDotcomOnly("2.12.1", (0, logging_1.getRunnerLogger)(true)), {
message: "Failed to find a CodeQL bundle release for CLI version 2.12.1.", message: "Failed to find a release of the CodeQL tools that contains CodeQL CLI 2.12.1.",
}); });
}); });
//# sourceMappingURL=setup-codeql.test.js.map //# sourceMappingURL=setup-codeql.test.js.map

View file

@ -1 +1 @@
{"version":3,"file":"setup-codeql.test.js","sourceRoot":"","sources":["../src/setup-codeql.test.ts"],"names":[],"mappings":"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"} {"version":3,"file":"setup-codeql.test.js","sourceRoot":"","sources":["../src/setup-codeql.test.ts"],"names":[],"mappings":"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"}

View file

@ -83,7 +83,7 @@ test.beforeEach(() => {
* @returns the download URL for the bundle. This can be passed to the tools parameter of * @returns the download URL for the bundle. This can be passed to the tools parameter of
* `codeql.setupCodeQL`. * `codeql.setupCodeQL`.
*/ */
async function mockDownloadApi({ function mockDownloadApi({
apiDetails = sampleApiDetails, apiDetails = sampleApiDetails,
isPinned, isPinned,
tagName, tagName,
@ -91,7 +91,7 @@ async function mockDownloadApi({
apiDetails?: GitHubApiDetails; apiDetails?: GitHubApiDetails;
isPinned?: boolean; isPinned?: boolean;
tagName: string; tagName: string;
}): Promise<string> { }): string {
const platform = const platform =
process.platform === "win32" process.platform === "win32"
? "win64" ? "win64"
@ -130,7 +130,7 @@ async function installIntoToolcache({
tagName: string; tagName: string;
tmpDir: string; tmpDir: string;
}) { }) {
const url = await mockDownloadApi({ apiDetails, isPinned, tagName }); const url = mockDownloadApi({ apiDetails, isPinned, tagName });
await codeql.setupCodeQL( await codeql.setupCodeQL(
cliVersion !== undefined ? undefined : url, cliVersion !== undefined ? undefined : url,
apiDetails, apiDetails,
@ -145,6 +145,41 @@ async function installIntoToolcache({
); );
} }
function mockReleaseApi({
apiDetails = sampleApiDetails,
assetNames,
tagName,
}: {
apiDetails?: GitHubApiDetails;
assetNames: string[];
tagName: string;
}): nock.Scope {
return nock(apiDetails.apiURL!)
.get(`/repos/github/codeql-action/releases/tags/${tagName}`)
.reply(200, {
assets: assetNames.map((name) => ({
name,
})),
tag_name: tagName,
});
}
function mockApiDetails(apiDetails: GitHubApiDetails) {
// This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to
// mock this directly. The difficulty is that `getApiDetails()` is called locally in
// `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(apiDetails.auth);
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
requiredEnvParamStub.withArgs("GITHUB_SERVER_URL").returns(apiDetails.url);
requiredEnvParamStub
.withArgs("GITHUB_API_URL")
.returns(apiDetails.apiURL || "");
}
test("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => { test("downloads and caches explicitly requested bundles that aren't in the toolcache", async (t) => {
await util.withTmpDir(async (tmpDir) => { await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir); setupActionsVars(tmpDir, tmpDir);
@ -154,7 +189,7 @@ test("downloads and caches explicitly requested bundles that aren't in the toolc
for (let i = 0; i < versions.length; i++) { for (let i = 0; i < versions.length; i++) {
const version = versions[i]; const version = versions[i];
const url = await mockDownloadApi({ const url = mockDownloadApi({
tagName: `codeql-bundle-${version}`, tagName: `codeql-bundle-${version}`,
isPinned: false, isPinned: false,
}); });
@ -186,7 +221,7 @@ test("downloads an explicitly requested bundle even if a different version is ca
tmpDir, tmpDir,
}); });
const url = await mockDownloadApi({ const url = mockDownloadApi({
tagName: "codeql-bundle-20200610", tagName: "codeql-bundle-20200610",
}); });
const result = await codeql.setupCodeQL( const result = await codeql.setupCodeQL(
@ -204,6 +239,37 @@ test("downloads an explicitly requested bundle even if a different version is ca
}); });
}); });
test("tries to cache an explicitly requested bundle with its CLI version number", async (t) => {
await util.withTmpDir(async (tmpDir) => {
setupActionsVars(tmpDir, tmpDir);
mockApiDetails(sampleApiDetails);
sinon.stub(actionsUtil, "isRunningLocalAction").returns(true);
const releaseApiMock = mockReleaseApi({
assetNames: ["cli-version-2.10.0.txt"],
tagName: "codeql-bundle-20200610",
});
const url = mockDownloadApi({
tagName: "codeql-bundle-20200610",
});
const result = await codeql.setupCodeQL(
url,
sampleApiDetails,
tmpDir,
util.GitHubVariant.DOTCOM,
false,
SAMPLE_DEFAULT_CLI_VERSION,
getRunnerLogger(true),
false
);
t.assert(releaseApiMock.isDone(), "Releases API should have been called");
t.assert(toolcache.find("CodeQL", "2.10.0"));
t.deepEqual(result.toolsVersion, "0.0.0-20200610");
});
});
for (const { isCached, tagName, toolcacheCliVersion } of [ for (const { isCached, tagName, toolcacheCliVersion } of [
{ {
isCached: true, isCached: true,
@ -237,7 +303,7 @@ for (const { isCached, tagName, toolcacheCliVersion } of [
tmpDir, tmpDir,
}); });
} else { } else {
await mockDownloadApi({ mockDownloadApi({
tagName, tagName,
}); });
sinon.stub(api, "getApiClient").value(() => ({ sinon.stub(api, "getApiClient").value(() => ({
@ -314,7 +380,7 @@ for (const variant of [util.GitHubVariant.GHAE, util.GitHubVariant.GHES]) {
tmpDir, tmpDir,
}); });
await mockDownloadApi({ mockDownloadApi({
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
}); });
const result = await codeql.setupCodeQL( const result = await codeql.setupCodeQL(
@ -349,7 +415,7 @@ test('downloads bundle if "latest" tools specified but not cached', async (t) =>
tmpDir, tmpDir,
}); });
await mockDownloadApi({ mockDownloadApi({
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
}); });
const result = await codeql.setupCodeQL( const result = await codeql.setupCodeQL(
@ -408,22 +474,7 @@ test("download codeql bundle from github ae endpoint", async (t) => {
path.join(__dirname, `/../src/testdata/codeql-bundle-pinned.tar.gz`) path.join(__dirname, `/../src/testdata/codeql-bundle-pinned.tar.gz`)
); );
// This is a workaround to mock `api.getApiDetails()` since it doesn't seem to be possible to mockApiDetails(sampleGHAEApiDetails);
// mock this directly. The difficulty is that `getApiDetails()` is called locally in
// `api-client.ts`, but `sinon.stub(api, "getApiDetails")` only affects calls to
// `getApiDetails()` via an imported `api` module.
sinon
.stub(actionsUtil, "getRequiredInput")
.withArgs("token")
.returns(sampleGHAEApiDetails.auth);
const requiredEnvParamStub = sinon.stub(util, "getRequiredEnvParam");
requiredEnvParamStub
.withArgs("GITHUB_SERVER_URL")
.returns(sampleGHAEApiDetails.url);
requiredEnvParamStub
.withArgs("GITHUB_API_URL")
.returns(sampleGHAEApiDetails.apiURL);
sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); sinon.stub(actionsUtil, "isRunningLocalAction").returns(false);
process.env["GITHUB_ACTION_REPOSITORY"] = "github/codeql-action"; process.env["GITHUB_ACTION_REPOSITORY"] = "github/codeql-action";

View file

@ -118,7 +118,8 @@ test("findCodeQLBundleTagDotcomOnly() errors if no GitHub Release matches marker
getRunnerLogger(true) getRunnerLogger(true)
), ),
{ {
message: "Failed to find a CodeQL bundle release for CLI version 2.12.1.", message:
"Failed to find a release of the CodeQL tools that contains CodeQL CLI 2.12.1.",
} }
); );
}); });

View file

@ -49,21 +49,24 @@ export function getCodeQLActionRepository(logger: Logger): string {
} }
/** /**
* CodeQL bundles are currently tagged in the form `codeql-bundle-yyyymmdd`, so it is not possible * Gets the tag name and, if known, the CodeQL CLI version for each CodeQL bundle release.
* to directly find the CodeQL bundle release for a particular CLI version.
* *
* To get around this, we add a `codeql-version-x.y.z.txt` asset to each bundle release that * CodeQL bundles are currently tagged in the form `codeql-bundle-yyyymmdd`, so it is not possible
* specifies the CLI version for that bundle release. * 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 * 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`. * CodeQL bundle release for a particular CLI version, for example `codeql-bundle-vx.y.z`.
*/ */
export async function findCodeQLBundleTagDotcomOnly( async function getCodeQLBundleReleasesDotcomOnly(
cliVersion: string,
logger: Logger logger: Logger
): Promise<string> { ): Promise<Array<{ cliVersion?: string; tagName: string }>> {
logger.debug( logger.debug(
`Trying to find the CodeQL bundle release for CLI version ${cliVersion}.` `Fetching CodeQL CLI version and CodeQL bundle tag name information for releases of the CodeQL tools.`
); );
const apiClient = api.getApiClient(); const apiClient = api.getApiClient();
const codeQLActionRepository = getCodeQLActionRepository(logger); const codeQLActionRepository = getCodeQLActionRepository(logger);
@ -73,31 +76,90 @@ export async function findCodeQLBundleTagDotcomOnly(
}); });
logger.debug(`Found ${releases.length} releases.`); logger.debug(`Found ${releases.length} releases.`);
for (const release of releases) { return releases.flatMap((release) => {
const cliVersionFileVersions = release.assets const cliVersionFileVersions = release.assets
.map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1]) .map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1])
.filter((v) => v) .filter((v) => v)
.map((v) => v as string); .map((v) => v as string);
if (cliVersionFileVersions.length === 0) {
logger.debug(
`Ignoring release ${release.tag_name} with no CLI version marker file.`
);
continue;
}
if (cliVersionFileVersions.length > 1) { if (cliVersionFileVersions.length > 1) {
logger.warning( logger.warning(
`Ignoring release ${release.tag_name} with multiple CLI version marker files.` `Ignoring release ${release.tag_name} with multiple CLI version marker files.`
); );
continue; return [];
}
if (cliVersionFileVersions[0] === cliVersion) {
return release.tag_name;
} }
return [
{ cliVersion: cliVersionFileVersions[0], tagName: release.tag_name },
];
});
}
async function tryGetCodeQLCliVersionForRelease(
release,
logger: Logger
): Promise<string | undefined> {
const cliVersionsFromMarkerFiles = release.assets
.map((asset) => asset.name.match(/cli-version-(.*)\.txt/)?.[1])
.filter((v) => v)
.map((v) => v as string);
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;
} }
throw new Error( return cliVersionsFromMarkerFiles[0];
`Failed to find a CodeQL bundle release for CLI version ${cliVersion}.` }
export async function findCodeQLBundleTagDotcomOnly(
cliVersion: string,
logger: Logger
): Promise<string> {
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;
}
export async function tryFindCliVersionDotcomOnly(
tagName: string,
logger: Logger
): Promise<string | undefined> {
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.error(
`Failed to find the CLI version for the CodeQL bundle tagged ${tagName}. Error: ${
e instanceof Error ? e.message : e
}`
);
return undefined;
}
} }
async function getCodeQLBundleDownloadURL( async function getCodeQLBundleDownloadURL(
@ -189,11 +251,11 @@ async function getCodeQLBundleDownloadURL(
return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`;
} }
export function getBundleTagNameFromUrl(url: string): string { export function getBundleVersionFromUrl(url: string): string {
const match = url.match(/\/codeql-bundle-(.*)\//); const match = url.match(/\/codeql-bundle-(.*)\//);
if (match === null || match.length < 2) { if (match === null || match.length < 2) {
throw new Error( throw new Error(
`Malformed tools url: ${url}. Tag name could not be inferred` `Malformed tools url: ${url}. Bundle version could not be inferred`
); );
} }
return match[1]; return match[1];
@ -216,16 +278,24 @@ export function convertToSemVer(version: string, logger: Logger): string {
} }
type CodeQLToolsSource = type CodeQLToolsSource =
| { codeqlTarPath: string; sourceType: "local"; toolsVersion: "local" } | {
codeqlTarPath: string;
sourceType: "local";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: "local";
}
| { | {
codeqlFolder: string; codeqlFolder: string;
sourceType: "toolcache"; sourceType: "toolcache";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string; toolsVersion: string;
} }
| { | {
/** CLI version of the tools, if known. */
cliVersion?: string;
codeqlURL: string; codeqlURL: string;
semanticVersion: string;
sourceType: "download"; sourceType: "download";
/** Human-readable description of the source of the tools for telemetry purposes. */
toolsVersion: string; toolsVersion: string;
}; };
@ -331,54 +401,63 @@ export async function getCodeQLSource(
? // case 1 ? // case 1
{ {
cliVersion: defaults.cliVersion, cliVersion: defaults.cliVersion,
syntheticCliVersion: defaults.cliVersion,
tagName: defaults.bundleVersion, tagName: defaults.bundleVersion,
variant, variant,
} }
: toolsInput !== undefined : toolsInput !== undefined
? // case 2 ? // case 2
{ {
cliVersion: convertToSemVer( syntheticCliVersion: convertToSemVer(
getBundleTagNameFromUrl(toolsInput), getBundleVersionFromUrl(toolsInput),
logger logger
), ),
tagName: getBundleTagNameFromUrl(toolsInput), tagName: `codeql-bundle-${getBundleVersionFromUrl(toolsInput)}`,
url: toolsInput, url: toolsInput,
variant, variant,
} }
: // case 3 : // case 3
defaultCliVersion; {
...defaultCliVersion,
syntheticCliVersion: defaultCliVersion.cliVersion,
};
// If we find the specified version, we always use that. // If we find the specified version, we always use that.
let codeqlFolder = toolcache.find("CodeQL", requestedVersion.cliVersion); let codeqlFolder = toolcache.find(
"CodeQL",
requestedVersion.syntheticCliVersion
);
let tagName: string | undefined = requestedVersion["tagName"]; let tagName: string | undefined = requestedVersion["tagName"];
if (!codeqlFolder) { if (!codeqlFolder) {
logger.debug( logger.debug(
"Didn't find a version of the CodeQL tools in the toolcache with a version number " + "Didn't find a version of the CodeQL tools in the toolcache with a version number " +
`exactly matching ${requestedVersion.cliVersion}.` `exactly matching ${requestedVersion.syntheticCliVersion}.`
); );
const allVersions = toolcache.findAllVersions("CodeQL"); if (requestedVersion.cliVersion) {
logger.debug( const allVersions = toolcache.findAllVersions("CodeQL");
`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( logger.debug(
"Did not find exactly one version of the CodeQL tools starting with the requested version." `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.startsWith("0.0.0")) { if (!codeqlFolder && requestedVersion.cliVersion) {
// Fall back to accepting a `0.0.0-<tagName>` version if we didn't find the // Fall back to accepting a `0.0.0-<tagName>` version if we didn't find the
// `x.y.z` version. This is to support old versions of the toolcache. // `x.y.z` version. This is to support old versions of the toolcache.
// //
@ -398,11 +477,11 @@ export async function getCodeQLSource(
return { return {
codeqlFolder, codeqlFolder,
sourceType: "toolcache", sourceType: "toolcache",
toolsVersion: requestedVersion.cliVersion, toolsVersion: requestedVersion.syntheticCliVersion,
}; };
} }
logger.debug( logger.debug(
`Did not find CodeQL tools version ${requestedVersion.cliVersion} in the toolcache.` `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 // If we don't find the requested version on Enterprise, we may allow a
@ -410,7 +489,7 @@ export async function getCodeQLSource(
// specified explicitly (in which case we always honor it). // specified explicitly (in which case we always honor it).
if (variant !== util.GitHubVariant.DOTCOM && !forceLatest && !toolsInput) { if (variant !== util.GitHubVariant.DOTCOM && !forceLatest && !toolsInput) {
const result = await findOverridingToolsInCache( const result = await findOverridingToolsInCache(
requestedVersion.cliVersion, requestedVersion.syntheticCliVersion,
logger logger
); );
if (result !== undefined) { if (result !== undefined) {
@ -419,24 +498,32 @@ export async function getCodeQLSource(
} }
return { return {
cliVersion: requestedVersion.cliVersion || undefined,
codeqlURL: codeqlURL:
requestedVersion["url"] || requestedVersion["url"] ||
(await getCodeQLBundleDownloadURL( (await getCodeQLBundleDownloadURL(
tagName || (await getOrFindBundleTagName(requestedVersion, logger)), 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, apiDetails,
variant, variant,
logger logger
)), )),
semanticVersion: requestedVersion.cliVersion,
sourceType: "download", sourceType: "download",
toolsVersion: requestedVersion.cliVersion, toolsVersion: requestedVersion.syntheticCliVersion,
}; };
} }
export async function downloadCodeQL( export async function downloadCodeQL(
codeqlURL: string, codeqlURL: string,
semanticVersion: string, cliVersion: string | undefined,
apiDetails: api.GitHubApiDetails, apiDetails: api.GitHubApiDetails,
variant: util.GitHubVariant,
tempDir: string, tempDir: string,
logger: Logger logger: Logger
): Promise<string> { ): Promise<string> {
@ -475,7 +562,18 @@ export async function downloadCodeQL(
logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`); logger.debug(`CodeQL bundle download to ${codeqlPath} complete.`);
const codeqlExtracted = await toolcache.extractTar(codeqlPath); const codeqlExtracted = await toolcache.extractTar(codeqlPath);
return await toolcache.cacheDir(codeqlExtracted, "CodeQL", semanticVersion);
const bundleVersion = getBundleVersionFromUrl(codeqlURL);
// If we have a CLI version, use that. Otherwise, try to find the CLI version from the GitHub Releases
const toolcacheVersion =
cliVersion ||
(variant === util.GitHubVariant.DOTCOM &&
(await tryFindCliVersionDotcomOnly(
`codeql-bundle-${bundleVersion}`,
logger
))) ||
convertToSemVer(bundleVersion, logger);
return await toolcache.cacheDir(codeqlExtracted, "CodeQL", toolcacheVersion);
} }
export function getCodeQLURLVersion(url: string): string { export function getCodeQLURLVersion(url: string): string {
@ -532,8 +630,9 @@ export async function setupCodeQLBundle(
case "download": case "download":
codeqlFolder = await downloadCodeQL( codeqlFolder = await downloadCodeQL(
source.codeqlURL, source.codeqlURL,
source.semanticVersion, source.cliVersion,
apiDetails, apiDetails,
variant,
tempDir, tempDir,
logger logger
); );