From 0e574bc821de54b0a37dc62d855c724effe6b331 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Fri, 25 Sep 2026 17:44:56 +0100 Subject: [PATCH] Accept GitHub release URLs in the tools input Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- init/action.yml | 2 + lib/entry-points.js | 156 +++++++++++++++++++--- setup-codeql/action.yml | 2 + src/codeql-bundle.ts | 5 + src/setup-codeql.test.ts | 280 +++++++++++++++++++++++++++++++++++++++ src/setup-codeql.ts | 108 +++++++++++---- 7 files changed, 509 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 508ed0ce5..3d4d9be57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ See the [releases page](https://github.com/github/codeql-action/releases) for th ## [UNRELEASED] -No user facing changes. +- The `tools` input to the `init` and `setup-codeql` actions now accepts GitHub release URLs. The Action selects a compatible bundle from the specified release, including a per-language bundle when eligible. ## 4.38.2 - 24 Sept 2026 diff --git a/init/action.yml b/init/action.yml index 7787a0a07..e2a0419b6 100644 --- a/init/action.yml +++ b/init/action.yml @@ -10,6 +10,8 @@ inputs: - A local path to a CodeQL Bundle tarball, or - The URL of a CodeQL Bundle tarball GitHub release asset, or + - A release URL from GitHub.com or the current GitHub instance, such as + https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.0. - A special value `linked` which uses the version of the CodeQL tools that the Action has been bundled with. - A special value `nightly` which uses the latest nightly version of the diff --git a/lib/entry-points.js b/lib/entry-points.js index 908c89dfa..3f2c4adb9 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -151352,6 +151352,74 @@ function logMissingPerLanguageBundle({ logger }, language, location) { } // src/codeql-release.ts +function parseCodeQLReleaseUrl(input, apiDetails) { + let url2; + try { + url2 = new URL(input); + } catch { + return void 0; + } + if (url2.protocol !== "https:" || url2.username || url2.password) { + return void 0; + } + const isCurrentInstance = url2.origin === new URL(apiDetails.url).origin; + if (!isCurrentInstance && url2.origin !== new URL(GITHUB_DOTCOM_URL).origin) { + return void 0; + } + const match2 = url2.pathname.replace(/\/$/, "").match( + /^\/([\w.-]+)\/([\w.-]+)\/releases\/(?:tag\/(.+)|(codeql-bundle-[^/]+))$/ + ); + if (match2 === null) { + return void 0; + } + let tagName; + try { + tagName = decodeURIComponent(match2[3] ?? match2[4]); + } catch { + throw new ConfigurationError( + "Invalid URL encoding in the CodeQL release tag." + ); + } + return { + serverURL: url2.origin, + isCurrentInstance, + owner: match2[1], + repo: match2[2], + tagName + }; +} +function parseCliVersion(value) { + const parsed = semver7.parse(value); + if (parsed === null) { + return void 0; + } + return parsed.version + (parsed.build.length ? `+${parsed.build.join(".")}` : ""); +} +function getReleaseCliVersion(tagName, assetNames, logger) { + const versions = /* @__PURE__ */ new Set(); + for (const name of assetNames) { + const match2 = name.match(/^cli-version-(.+)\.txt$/); + if (match2 === null) { + continue; + } + const version = parseCliVersion(match2[1]); + if (version !== void 0) { + versions.add(version); + } else { + logger.debug(`Ignoring invalid CLI version marker ${name}.`); + } + } + if (versions.size > 1) { + logger.warning( + `Release ${tagName} has conflicting CLI version markers. Using a combined CodeQL bundle.` + ); + return void 0; + } + if (versions.size === 1) { + return versions.values().next().value; + } + return parseCliVersion(tagName.replace(/^codeql-bundle-/, "")); +} function encodeTag(tagName) { return tagName.split("/").map(encodeURIComponent).join("/"); } @@ -151379,6 +151447,21 @@ function getPublicRelease(reference) { getAssetURL: (name) => `${serverURL}/${owner}/${repo}/releases/download/${encodeTag(tagName)}/${name}` }; } +async function getRequestedRelease(action, requested) { + if (!requested.isCurrentInstance) { + return getPublicRelease(requested); + } + try { + return await getRelease(action, requested); + } catch (e) { + if (asHTTPError(e)?.status === 404) { + throw new ConfigurationError( + `Could not find the CodeQL release ${getReleasePageURL(requested)}. Check that it exists and that the token has access to it.` + ); + } + throw e; + } +} function getCompressionMethods({ cliVersion: cliVersion2, isLatestNightly, @@ -152348,11 +152431,14 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO toolsVersion: "local" }; } + const requestedRelease = toolsInput === void 0 ? void 0 : parseCodeQLReleaseUrl(toolsInput, apiDetails); let cliVersion2; let tagName; let url2; let bundle; - const canForceNightlyWithFF = isDynamicWorkflow() || isInTestMode(); + let release2; + let customReleaseURL; + const canForceNightlyWithFF = requestedRelease === void 0 && (isDynamicWorkflow() || isInTestMode()); const forceNightlyValueFF = await features.getValue("force_nightly" /* ForceNightly */); const forceNightly = forceNightlyValueFF && canForceNightlyWithFF; const nightlyRequestedByToolsInput = toolsInput !== void 0 && CODEQL_NIGHTLY_TOOLS_INPUTS.includes(toolsInput); @@ -152434,6 +152520,18 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO cliVersion2 = version.cliVersion; tagName = version.tagName; } + } else if (requestedRelease !== void 0) { + release2 = await getRequestedRelease( + { apiClient: getApiClient() }, + requestedRelease + ); + tagName = requestedRelease.tagName; + cliVersion2 = getReleaseCliVersion( + tagName, + release2.assetNames ?? [], + logger + ); + customReleaseURL = release2.url; } else if (toolsInput !== void 0) { tagName = tryGetTagNameFromUrl(toolsInput, logger); url2 = toolsInput; @@ -152454,18 +152552,21 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO cliVersion2 = version.cliVersion; tagName = version.tagName; } - const bundleVersion2 = tagName !== void 0 ? tryGetBundleVersionFromTagName(tagName, logger) : void 0; + const bundleVersion2 = ( + // Custom releases aren't cached, and their tags needn't contain a bundle version. + tagName !== void 0 && customReleaseURL === void 0 ? tryGetBundleVersionFromTagName(tagName, logger) : void 0 + ); const resolvedVersion = cliVersion2 ?? (bundleVersion2 !== void 0 ? convertToSemVer(bundleVersion2, logger) : void 0); const humanReadableVersion = resolvedVersion ?? tagName ?? url2 ?? "unknown"; logger.debug( `Attempting to obtain CodeQL tools. CLI version: ${cliVersion2 ?? "unknown"}, bundle tag name: ${tagName ?? "unknown"}, URL: ${url2 ?? "unspecified"}.` ); - const codeqlFolder = await findCodeQLInToolcache( + const codeqlFolder = customReleaseURL === void 0 ? await findCodeQLInToolcache( cliVersion2, tagName, humanReadableVersion, logger - ); + ) : void 0; if (codeqlFolder) { if (cliVersion2) { logger.info( @@ -152492,23 +152593,34 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO let compressionMethod; let perLanguageBundleFallback; if (!url2) { - if (tagName === void 0) { - throw new Error( - "Could not determine a release tag for the requested CodeQL bundle." + const selectionOptions = { + rawLanguages, + cliVersion: cliVersion2, + platform: getBundlePlatform(), + variant, + tarSupportsZstd + }; + let selection; + if (release2 !== void 0) { + selection = await selectBundle( + { env: getEnv(), features, logger }, + release2, + selectionOptions + ); + } else { + if (tagName === void 0) { + throw new Error( + "Could not determine a release tag for the requested CodeQL bundle." + ); + } + selection = await selectDefaultBundle( + { env: getEnv(), features, logger }, + tagName, + apiDetails, + selectionOptions ); } - ({ bundle, compressionMethod, perLanguageBundleFallback } = await selectDefaultBundle( - { env: getEnv(), features, logger }, - tagName, - apiDetails, - { - rawLanguages, - cliVersion: cliVersion2, - platform: getBundlePlatform(), - variant, - tarSupportsZstd - } - )); + ({ bundle, compressionMethod, perLanguageBundleFallback } = selection); url2 = bundle.url; } else { const method = inferCompressionMethod(url2); @@ -152535,6 +152647,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO bundleVersion: bundleVersion2, cliVersion: cliVersion2, compressionMethod, + ...customReleaseURL !== void 0 ? { customReleaseURL } : {}, ...perLanguageBundleFallback ? { perLanguageBundleFallback } : {}, sourceType: "download", toolsVersion: resolvedVersion ?? "unknown" @@ -152658,6 +152771,11 @@ var downloadCodeQL = async function(source, apiDetails, tarVersion, tempDir, log }; }; function getToolcacheDestination({ logger }, source) { + if (source.customReleaseURL !== void 0) { + return new Failure( + `Not caching the CodeQL tools from ${source.customReleaseURL}, since we don't cache releases requested by URL.` + ); + } if (source.bundle.kind !== "combined") { return new Failure( "Not caching the CodeQL tools because they came from a bundle that contains only a single language." diff --git a/setup-codeql/action.yml b/setup-codeql/action.yml index 8d13eeaff..b48a3f441 100644 --- a/setup-codeql/action.yml +++ b/setup-codeql/action.yml @@ -10,6 +10,8 @@ inputs: - A local path to a CodeQL Bundle tarball, or - The URL of a CodeQL Bundle tarball GitHub release asset, or + - A release URL from GitHub.com or the current GitHub instance, such as + https://github.com/github/codeql-action/releases/tag/codeql-bundle-v2.27.0. - A special value `linked` which uses the version of the CodeQL tools that the Action has been bundled with. - A special value `nightly` which uses the latest nightly version of the diff --git a/src/codeql-bundle.ts b/src/codeql-bundle.ts index 99481834d..242d9270c 100644 --- a/src/codeql-bundle.ts +++ b/src/codeql-bundle.ts @@ -29,6 +29,11 @@ export interface CodeQLDownloadSource { toolsVersion: string; /** The release lacks the eligible per-language bundle, so we selected the combined bundle. */ perLanguageBundleFallback?: true; + /** + * The page of a requested release that may contain a different build than the bundle we cache for + * its version, so we don't cache it. + */ + customReleaseURL?: string; } /** Returns the exact bundle asset name for a platform and optional language. */ diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index d8d4cfe25..98a75d98c 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -38,6 +38,7 @@ import { } from "./testing-utils"; import * as toolsDownload from "./tools-download"; import { + ConfigurationError, getErrorMessage, GitHubVariant, HTTPError, @@ -1382,6 +1383,285 @@ for (const scenario of ["per-language", "fallback", "missing"] as const) { ); } +const GHES_API_DETAILS = { + auth: "enterprise-token", + url: "https://github.example.test", + apiURL: "https://github.example.test/api/v3", +}; + +/** Models a hosted Linux runner with zstd and a release in `repository` on the instance `apiDetails` describes. */ +function stubRequestedRelease({ + apiDetails = SAMPLE_DOTCOM_API_DETAILS, + repository = "octo/tools", + tagName = `codeql-bundle-v${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}`, + assetNames = [ + "codeql-bundle-linux64.tar.zst", + "codeql-bundle-java-linux64.tar.zst", + ], + markers = [] as string[], + status = 200, +} = {}) { + sinon.stub(process, "platform").value("linux"); + sinon.stub(process, "arch").value("x64"); + sinon.stub(actionsUtil, "isRunningLocalAction").returns(false); + process.env[ActionsEnvVars.RUNNER_ENVIRONMENT] = "github-hosted"; + sinon.stub(tar, "isZstdAvailable").resolves({ + available: true, + foundZstdBinary: true, + }); + const apiBase = `${apiDetails.apiURL}/repos/${repository}/releases`; + const assets = [ + ...assetNames, + ...markers.map((version) => `cli-version-${version}.txt`), + ].map((name, index) => ({ name, url: `${apiBase}/assets/${1000 + index}` })); + const requests: string[] = []; + const client = github.getOctokit("123", { + baseUrl: apiDetails.apiURL, + request: { + fetch: async (input) => { + const url = String(input); + requests.push(url); + if (url !== `${apiBase}/tags/${tagName}`) { + throw new Error(`Unexpected API request: ${url}`); + } + return new Response(JSON.stringify({ tag_name: tagName, assets }), { + status, + headers: { "content-type": "application/json" }, + }); + }, + }, + }); + sinon.stub(api, "getApiClient").value(() => client); + return { + assets, + requests, + releaseURL: `${apiDetails.url}/${repository}/releases/tag/${tagName}`, + }; +} + +for (const scenario of ["combined", "per-language", "fallback"] as const) { + test.serial( + `setupCodeQLBundle downloads a ${scenario} bundle from a custom release without using the toolcache`, + async (t) => { + const fixture = stubRequestedRelease(); + const extract = stubDownloadAndExtract(); + if (scenario === "fallback") { + extract.onFirstCall().rejects(new HTTPError("Not Found", 404)); + } + const find = sinon.spy(toolcache, "find"); + sinon.stub(actionsUtil, "isDynamicWorkflow").returns(true); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + createToolcacheEntry( + tmpDir, + "CodeQL", + MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION, + ); + const result = await setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + GitHubVariant.DOTCOM, + PER_LANGUAGE_CLI_VERSION, + scenario === "combined" ? undefined : ["java"], + false, // useOverlayAwareDefaultCliVersion + // The requested release takes precedence over forcing the nightly. + createFeatures([Feature.PerLanguageBundles, Feature.ForceNightly]), + getRunnerLogger(true), + ); + + t.is(fixture.requests.length, 1); + t.true(find.notCalled); + t.is(result.toolsSource, setupCodeql.ToolsSource.Download); + t.is(result.toolsVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION); + t.is(extract.callCount, scenario === "fallback" ? 2 : 1); + t.is( + extract.lastCall.args[0], + fixture.assets[scenario === "per-language" ? 1 : 0].url, + ); + t.is(extract.lastCall.args[3], "token token"); + t.is(path.dirname(result.codeqlFolder), tmpDir); + t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); + }); + }, + ); +} + +test.serial( + "setupCodeQLBundle downloads the gzip bundle from a requested release that only has gzip bundles", + async (t) => { + const fixture = stubRequestedRelease({ + assetNames: ["codeql-bundle-linux64.tar.gz"], + }); + const extract = stubDownloadAndExtract(); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const result = await setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + GitHubVariant.DOTCOM, + PER_LANGUAGE_CLI_VERSION, + ["java"], + false, // useOverlayAwareDefaultCliVersion + createFeatures([Feature.PerLanguageBundles]), + getRunnerLogger(true), + ); + + t.true(extract.calledOnce); + t.is(extract.firstCall.args[0], fixture.assets[0].url); + t.is(extract.firstCall.args[1], "gzip"); + t.is(result.toolsSource, setupCodeql.ToolsSource.Download); + t.is(result.toolsDownloadStatusReport?.perLanguage, undefined); + }); + }, +); + +for (const { repository, tagName, markers } of [ + { + repository: "octo/tools", + tagName: "codeql-bundle-feature_branch", + markers: [], + }, +]) { + test.serial( + `setupCodeQLBundle doesn't share the toolcache with ${tagName} in ${repository}`, + async (t) => { + const fixture = stubRequestedRelease({ repository, tagName, markers }); + const extract = stubDownloadAndExtract(); + const find = sinon.spy(toolcache, "find"); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + createToolcacheEntry( + tmpDir, + "CodeQL", + MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION, + ); + const result = await setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + GitHubVariant.DOTCOM, + PER_LANGUAGE_CLI_VERSION, + undefined, // rawLanguages + false, // useOverlayAwareDefaultCliVersion + createFeatures([]), + getRunnerLogger(true), + ); + + t.true(find.notCalled); + t.is(result.toolsSource, setupCodeql.ToolsSource.Download); + t.is(extract.firstCall.args[0], fixture.assets[0].url); + t.is(path.dirname(result.codeqlFolder), tmpDir); + t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); + }); + }, + ); +} + +for (const repository of ["github/codeql-action", "octo/tools"]) { + test.serial( + `setupCodeQLBundle downloads a GitHub.com release in ${repository} from GHES by URL without credentials`, + async (t) => { + const fixture = stubRequestedRelease({ repository }); + const extract = stubDownloadAndExtract(); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const result = await setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + GHES_API_DETAILS, + tmpDir, + GitHubVariant.GHES, + PER_LANGUAGE_CLI_VERSION, + ["java"], + false, // useOverlayAwareDefaultCliVersion + createFeatures([Feature.PerLanguageBundles]), + getRunnerLogger(true), + ); + + t.deepEqual(fixture.requests, []); + t.true(extract.calledOnce); + t.is( + extract.firstCall.args[0], + `https://github.com/${repository}/releases/download/codeql-bundle-v${MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION}/codeql-bundle-linux64.tar.zst`, + ); + t.is(extract.firstCall.args[3], undefined); + t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); + }); + }, + ); +} + +test.serial( + "setupCodeQLBundle doesn't substitute another release for a requested release that can't be found", + async (t) => { + const fixture = stubRequestedRelease({ status: 404 }); + const extract = stubDownloadAndExtract(); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + await t.throwsAsync( + setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + GitHubVariant.DOTCOM, + PER_LANGUAGE_CLI_VERSION, + undefined, // rawLanguages + false, // useOverlayAwareDefaultCliVersion + createFeatures([]), + getRunnerLogger(true), + ), + { + instanceOf: ConfigurationError, + message: `Could not find the CodeQL release ${fixture.releaseURL}. Check that it exists and that the token has access to it.`, + }, + ); + t.is(fixture.requests.length, 1); + t.true(extract.notCalled); + }); + }, +); + +test.serial( + "setupCodeQLBundle uses the CLI version marker of a requested release", + async (t) => { + const fixture = stubRequestedRelease({ + tagName: "run-123", + markers: [MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION], + }); + const extract = stubDownloadAndExtract(); + + await withTmpDir(async (tmpDir) => { + setupActionsVars(tmpDir, tmpDir); + const result = await setupCodeql.setupCodeQLBundle( + fixture.releaseURL, + SAMPLE_DOTCOM_API_DETAILS, + tmpDir, + GitHubVariant.DOTCOM, + PER_LANGUAGE_CLI_VERSION, + ["java"], + false, // useOverlayAwareDefaultCliVersion + createFeatures([Feature.PerLanguageBundles]), + getRunnerLogger(true), + ); + + // The tag doesn't give a version, so the job can only use the per-language bundle because of + // the marker. + t.is(extract.firstCall.args[0], fixture.assets[1].url); + t.is(result.toolsVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION); + t.is( + result.toolsDownloadStatusReport?.perLanguage?.tools_bundle_language, + BuiltInLanguage.java, + ); + }); + }, +); + for (const bundle of ["per-language", "combined", "fallback"] as const) { test.serial( `setupCodeQLBundle preserves the nightly version for a ${bundle} download`, diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index 61b6e500a..be16b6d2d 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -25,8 +25,12 @@ import { import { BundleSelection, BundleSelectionOptions, + CodeQLRelease, getPublicRelease, getRelease, + getReleaseCliVersion, + getRequestedRelease, + parseCodeQLReleaseUrl, selectBundle, } from "./codeql-release"; import * as defaults from "./defaults.json"; @@ -392,6 +396,10 @@ async function resolveDefaultCliVersion( * We handle the `tools` input in this order: * * - A local path is extracted without using the toolcache. + * - A release URL selects a bundle from that release, and takes precedence over the `force_nightly` + * feature flag. We don't use the toolcache, since a release may contain a different build than + * the cached bundle for its version. Bundle URLs keep using the toolcache for the version in + * their tag, for compatibility. * - `nightly` or `nightly-latest`, or the `force_nightly` feature flag in a dynamic workflow, * selects a bundle from the latest nightly release. We then continue with that bundle's URL. * - `linked`, or its old name `latest`, selects the version shipped with the Action. @@ -400,8 +408,9 @@ async function resolveDefaultCliVersion( * - Any other value is the URL of a bundle. * - Without a `tools` input, we use the default version. * - * Apart from a local path, we look for the resolved version in the toolcache before downloading. A - * cached version takes precedence even if the job could use a per-language bundle. + * For other inputs apart from a local path, we look for the resolved version in the toolcache + * before downloading. A cached version takes precedence even if the job could use a per-language + * bundle. * * @param toolsInput The argument provided for the `tools` input, if any. * @param defaultCliVersion The default CLI version that's linked to the CodeQL Action. @@ -450,6 +459,11 @@ export async function getCodeQLSource( }; } + const requestedRelease = + toolsInput === undefined + ? undefined + : parseCodeQLReleaseUrl(toolsInput, apiDetails); + /** Requested CLI version number, for example 2.12.6. */ let cliVersion: string | undefined; /** Tag name of the CodeQL bundle, for example `codeql-bundle-20230120`. */ @@ -461,10 +475,17 @@ export async function getCodeQLSource( */ let url: string | undefined; let bundle: CodeQLBundle | undefined; + /** The release requested by URL, which we select the bundle from. */ + let release: CodeQLRelease | undefined; + /** The page of a requested release whose bundles we don't cache. */ + let customReleaseURL: string | undefined; // We allow forcing the nightly CLI via the FF for `dynamic` events (or in test mode) where the - // `tools` input cannot be adjusted to explicitly request it. - const canForceNightlyWithFF = isDynamicWorkflow() || util.isInTestMode(); + // `tools` input cannot be adjusted to explicitly request it. An explicitly requested release + // takes precedence. + const canForceNightlyWithFF = + requestedRelease === undefined && + (isDynamicWorkflow() || util.isInTestMode()); const forceNightlyValueFF = await features.getValue(Feature.ForceNightly); const forceNightly = forceNightlyValueFF && canForceNightlyWithFF; @@ -585,6 +606,18 @@ export async function getCodeQLSource( cliVersion = version.cliVersion; tagName = version.tagName; } + } else if (requestedRelease !== undefined) { + release = await getRequestedRelease( + { apiClient: api.getApiClient() }, + requestedRelease, + ); + tagName = requestedRelease.tagName; + cliVersion = getReleaseCliVersion( + tagName, + release.assetNames ?? [], + logger, + ); + customReleaseURL = release.url; } else if (toolsInput !== undefined) { // Any other value is a bundle URL, including one we selected from the latest nightly above. // We use the version in its tag, if any, for the toolcache, so we assume that bundles with the @@ -612,7 +645,8 @@ export async function getCodeQLSource( } const bundleVersion = - tagName !== undefined + // Custom releases aren't cached, and their tags needn't contain a bundle version. + tagName !== undefined && customReleaseURL === undefined ? tryGetBundleVersionFromTagName(tagName, logger) : undefined; const resolvedVersion = @@ -629,12 +663,16 @@ export async function getCodeQLSource( `URL: ${url ?? "unspecified"}.`, ); - const codeqlFolder = await findCodeQLInToolcache( - cliVersion, - tagName, - humanReadableVersion, - logger, - ); + // A custom release may contain a different build than the bundle with the same version. + const codeqlFolder = + customReleaseURL === undefined + ? await findCodeQLInToolcache( + cliVersion, + tagName, + humanReadableVersion, + logger, + ) + : undefined; if (codeqlFolder) { if (cliVersion) { logger.info( @@ -671,24 +709,34 @@ export async function getCodeQLSource( let perLanguageBundleFallback: true | undefined; if (!url) { - if (tagName === undefined) { - throw new Error( - "Could not determine a release tag for the requested CodeQL bundle.", + const selectionOptions: BundleSelectionOptions = { + rawLanguages, + cliVersion, + platform: getBundlePlatform(), + variant, + tarSupportsZstd, + }; + let selection: BundleSelection; + if (release !== undefined) { + selection = await selectBundle( + { env: getEnv(), features, logger }, + release, + selectionOptions, ); - } - ({ bundle, compressionMethod, perLanguageBundleFallback } = - await selectDefaultBundle( + } else { + if (tagName === undefined) { + throw new Error( + "Could not determine a release tag for the requested CodeQL bundle.", + ); + } + selection = await selectDefaultBundle( { env: getEnv(), features, logger }, tagName, apiDetails, - { - rawLanguages, - cliVersion, - platform: getBundlePlatform(), - variant, - tarSupportsZstd, - }, - )); + selectionOptions, + ); + } + ({ bundle, compressionMethod, perLanguageBundleFallback } = selection); url = bundle.url; } else { const method = tar.inferCompressionMethod(url); @@ -720,6 +768,7 @@ export async function getCodeQLSource( bundleVersion, cliVersion, compressionMethod, + ...(customReleaseURL !== undefined ? { customReleaseURL } : {}), ...(perLanguageBundleFallback ? { perLanguageBundleFallback } : {}), sourceType: "download", toolsVersion: resolvedVersion ?? "unknown", @@ -905,12 +954,19 @@ export const downloadCodeQL = async function ( * Returns the canonical toolcache directory, or the reason the bundle cannot be cached. * * The toolcache is keyed by version, so we don't cache bundles that would give a later request for - * the same version the wrong tools, such as per-language bundles, which lack the other languages. + * the same version the wrong tools: per-language bundles, which lack the other languages, and + * custom releases, which may be a different build. */ function getToolcacheDestination( { logger }: ActionState<["Logger"]>, source: CodeQLDownloadSource, ): util.Result { + if (source.customReleaseURL !== undefined) { + return new util.Failure( + `Not caching the CodeQL tools from ${source.customReleaseURL}, since we don't cache ` + + "releases requested by URL.", + ); + } if (source.bundle.kind !== "combined") { return new util.Failure( "Not caching the CodeQL tools because they came from a bundle that contains only a " +