From 7f964a9e6e42cdd9cc531a942ceab765c0d6f4d8 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 24 Sep 2026 15:38:23 +0100 Subject: [PATCH] Select the default bundle from the first compatible release Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- lib/entry-points.js | 138 +++++++++++++++++-------------------- src/setup-codeql.test.ts | 70 +++++++++++-------- src/setup-codeql.ts | 143 +++++++++++++++------------------------ 3 files changed, 158 insertions(+), 193 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index ecba603a6..f78a951f0 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -27216,8 +27216,8 @@ var require_gte = __commonJS({ "node_modules/semver/functions/gte.js"(exports2, module2) { "use strict"; var compare3 = require_compare(); - var gte9 = (a, b, loose) => compare3(a, b, loose) >= 0; - module2.exports = gte9; + var gte8 = (a, b, loose) => compare3(a, b, loose) >= 0; + module2.exports = gte8; } }); @@ -27238,7 +27238,7 @@ var require_cmp = __commonJS({ var eq = require_eq(); var neq = require_neq(); var gt = require_gt(); - var gte9 = require_gte(); + var gte8 = require_gte(); var lt2 = require_lt(); var lte2 = require_lte(); var cmp = (a, op, b, loose) => { @@ -27268,7 +27268,7 @@ var require_cmp = __commonJS({ case ">": return gt(a, b, loose); case ">=": - return gte9(a, b, loose); + return gte8(a, b, loose); case "<": return lt2(a, b, loose); case "<=": @@ -28076,7 +28076,7 @@ var require_outside = __commonJS({ var gt = require_gt(); var lt2 = require_lt(); var lte2 = require_lte(); - var gte9 = require_gte(); + var gte8 = require_gte(); var outside = (version, range2, hilo, options) => { version = new SemVer(version, options); range2 = new Range2(range2, options); @@ -28091,7 +28091,7 @@ var require_outside = __commonJS({ break; case "<": gtfn = lt2; - ltefn = gte9; + ltefn = gte8; ltfn = gt; comp = "<"; ecomp = "<="; @@ -28406,7 +28406,7 @@ var require_semver2 = __commonJS({ var lt2 = require_lt(); var eq = require_eq(); var neq = require_neq(); - var gte9 = require_gte(); + var gte8 = require_gte(); var lte2 = require_lte(); var cmp = require_cmp(); var coerce3 = require_coerce(); @@ -28445,7 +28445,7 @@ var require_semver2 = __commonJS({ lt: lt2, eq, neq, - gte: gte9, + gte: gte8, lte: lte2, cmp, coerce: coerce3, @@ -31721,7 +31721,7 @@ var require_brace_expansion = __commonJS({ function lte2(i, y) { return i <= y; } - function gte9(i, y) { + function gte8(i, y) { return i >= y; } function combine2(acc, base, pre, values, max, maxLength, dropEmpties, outBase) { @@ -31754,7 +31754,7 @@ var require_brace_expansion = __commonJS({ var reverse = y < x; if (reverse) { incr *= -1; - test = gte9; + test = gte8; } var pad = n.some(isPadded2); var length = 0; @@ -33901,8 +33901,8 @@ var require_semver3 = __commonJS({ function neq(a, b, loose) { return compare3(a, b, loose) !== 0; } - exports2.gte = gte9; - function gte9(a, b, loose) { + exports2.gte = gte8; + function gte8(a, b, loose) { return compare3(a, b, loose) >= 0; } exports2.lte = lte2; @@ -33933,7 +33933,7 @@ var require_semver3 = __commonJS({ case ">": return gt(a, b, loose); case ">=": - return gte9(a, b, loose); + return gte8(a, b, loose); case "<": return lt2(a, b, loose); case "<=": @@ -34478,7 +34478,7 @@ var require_semver3 = __commonJS({ break; case "<": gtfn = lt2; - ltefn = gte9; + ltefn = gte8; ltfn = gt; comp = "<"; ecomp = "<="; @@ -89595,7 +89595,7 @@ var require_brace_expansion2 = __commonJS({ function lte2(i, y) { return i <= y; } - function gte9(i, y) { + function gte8(i, y) { return i >= y; } function combine2(acc, pre, values, max, maxLength, dropEmpties) { @@ -89627,7 +89627,7 @@ var require_brace_expansion2 = __commonJS({ var reverse = y < x; if (reverse) { incr *= -1; - test = gte9; + test = gte8; } var pad = n.some(isPadded2); var length = 0; @@ -151359,6 +151359,18 @@ function getReleasePageURL(reference) { const { serverURL, owner, repo, tagName } = reference; return `${serverURL}/${owner}/${repo}/releases/tag/${encodeTag(tagName)}`; } +async function getRelease({ apiClient }, reference) { + const { owner, repo, tagName } = reference; + const { data: release2 } = await apiClient.rest.repos.getReleaseByTag({ + owner, + repo, + tag: tagName + }); + return { + url: getReleasePageURL(reference), + getAssetURL: (name) => release2.assets.find((asset) => asset.name === name)?.url + }; +} function getPublicRelease(reference) { const { serverURL, owner, repo, tagName } = reference; return { @@ -152133,7 +152145,8 @@ function getCodeQLActionRepository(logger) { } return getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); } -async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, logger) { +async function selectDefaultBundle(action, tagName, apiDetails, options) { + const { logger } = action; const codeQLActionRepository = getCodeQLActionRepository(logger); const potentialDownloadSources = [ // This GitHub instance, and this Action. @@ -152148,33 +152161,34 @@ async function getCodeQLBundleDownloadURL(tagName, apiDetails, codeQLBundleName, return !self2.slice(0, index2).some((other) => (0, import_fast_deep_equal.default)(source, other)); } ); - for (const downloadSource of uniqueDownloadSources) { - const [apiURL, repository] = downloadSource; - if (apiURL === GITHUB_DOTCOM_URL && repository === CODEQL_DEFAULT_ACTION_REPOSITORY) { + for (const [serverURL, repository] of uniqueDownloadSources) { + if (serverURL === GITHUB_DOTCOM_URL && repository === CODEQL_DEFAULT_ACTION_REPOSITORY) { break; } - const [repositoryOwner, repositoryName] = repository.split("/"); + const [owner2, repo2] = repository.split("/"); try { - const release2 = await getApiClient().rest.repos.getReleaseByTag({ - owner: repositoryOwner, - repo: repositoryName, - tag: tagName - }); - for (const asset of release2.data.assets) { - if (asset.name === codeQLBundleName) { - logger.info( - `Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.` - ); - return asset.url; - } - } + const release2 = await getRelease( + { apiClient: getApiClient() }, + { serverURL, owner: owner2, repo: repo2, tagName } + ); + return await selectBundle(action, release2, options); } catch (e) { logger.info( - `Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.` + `Looked for CodeQL bundles in release ${tagName} of ${repository} on ${serverURL} but got error ${e}.` ); } } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; + const [owner, repo] = CODEQL_DEFAULT_ACTION_REPOSITORY.split("/"); + return selectBundle( + action, + getPublicRelease({ + serverURL: GITHUB_DOTCOM_URL, + owner, + repo, + tagName + }), + options + ); } function tryGetBundleVersionFromTagName(tagName, logger) { const match2 = tagName.match(/^codeql-bundle-(.+)$/); @@ -152525,47 +152539,26 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO } } let compressionMethod; + let perLanguageBundleFallback; if (!url2) { - const bundleTagName = tagName; - if (bundleTagName === void 0) { + if (tagName === void 0) { throw new Error( "Could not determine a release tag for the requested CodeQL bundle." ); } - compressionMethod = cliVersion2 !== void 0 && await useZstdBundle(cliVersion2, tarSupportsZstd) ? "zstd" : "gzip"; - const platform2 = getBundlePlatform(); - const perLanguageBundleLanguage = await getPerLanguageBundleLanguage( + ({ bundle, compressionMethod, perLanguageBundleFallback } = await selectDefaultBundle( { env: getEnv(), features, logger }, + tagName, + apiDetails, { rawLanguages, cliVersion: cliVersion2, - compressionMethod, - platform: platform2, - variant + platform: getBundlePlatform(), + variant, + tarSupportsZstd } - ); - const resolveBundleURL = (language) => getCodeQLBundleDownloadURL( - bundleTagName, - apiDetails, - getCodeQLBundleName(compressionMethod, platform2, language), - logger - ); - const combinedBundleURL = await resolveBundleURL(); - if (perLanguageBundleLanguage !== void 0) { - logger.info( - `Selected the per-language CodeQL bundle for '${perLanguageBundleLanguage}'.` - ); - url2 = await resolveBundleURL(perLanguageBundleLanguage); - bundle = { - kind: "per-language", - url: url2, - language: perLanguageBundleLanguage, - combinedBundleURL - }; - } else { - url2 = combinedBundleURL; - bundle = { kind: "combined", url: url2 }; - } + )); + url2 = bundle.url; } else { const method = inferCompressionMethod(url2); if (method === void 0) { @@ -152591,6 +152584,7 @@ async function getCodeQLSource(toolsInput, defaultCliVersion, rawLanguages, useO bundleVersion: bundleVersion2, cliVersion: cliVersion2, compressionMethod, + ...perLanguageBundleFallback ? { perLanguageBundleFallback } : {}, sourceType: "download", toolsVersion: resolvedVersion ?? "unknown" }; @@ -152804,12 +152798,6 @@ async function downloadCodeQLBundle(action, source, apiDetails, tarVersion, temp }; } } -async function useZstdBundle(cliVersion2, tarSupportsZstd) { - return ( - // In testing, gzip performs better than zstd on Windows. - process.platform !== "win32" && tarSupportsZstd && semver11.gte(cliVersion2, CODEQL_VERSION_ZSTD_BUNDLE) - ); -} function getTempExtractionDir(tempDir) { return path13.join(tempDir, v4_default()); } @@ -157064,7 +157052,7 @@ function isPadded(el) { function lte(i, y) { return i <= y; } -function gte8(i, y) { +function gte7(i, y) { return i >= y; } function combine(acc, pre, values, max, maxLength, dropEmpties) { @@ -157099,7 +157087,7 @@ function expandSequence(body, isAlphaSequence, max, maxLength) { const reverse = y < x; if (reverse) { incr *= -1; - test = gte8; + test = gte7; } const pad = n.some(isPadded); let length = 0; diff --git a/src/setup-codeql.test.ts b/src/setup-codeql.test.ts index ef33ca0a6..d8d4cfe25 100644 --- a/src/setup-codeql.test.ts +++ b/src/setup-codeql.test.ts @@ -1272,9 +1272,14 @@ for (const status of [200, 404]) { ); } -for (const fallback of [false, true]) { +for (const scenario of ["per-language", "fallback", "missing"] as const) { + const suffix = { + "per-language": "", + fallback: " with fallback", + missing: " when the release lacks the per-language bundle", + }[scenario]; test.serial( - `setupCodeQLBundle retains the selected release identity for an opaque asset URL${fallback ? " with fallback" : ""}`, + `setupCodeQLBundle retains the selected release identity for an opaque asset URL${suffix}`, async (t) => { sinon.stub(process, "platform").value("linux"); sinon.stub(process, "arch").value("x64"); @@ -1288,22 +1293,22 @@ for (const fallback of [false, true]) { const assetURL = "https://api.github.com/repos/codeql-testing/action-fork/releases/assets/123"; const combinedURL = `${assetURL}4`; + const assets = [ + { name: "codeql-bundle-linux64.tar.zst", url: combinedURL }, + ]; + if (scenario !== "missing") { + assets.push({ + name: "codeql-bundle-java-linux64.tar.zst", + url: assetURL, + }); + } const fetchRelease = sinon .stub, ReturnType>() .callsFake( async () => - new Response( - JSON.stringify({ - assets: [ - { name: "codeql-bundle-java-linux64.tar.zst", url: assetURL }, - { - name: "codeql-bundle-linux64.tar.zst", - url: combinedURL, - }, - ], - }), - { headers: { "content-type": "application/json" } }, - ), + new Response(JSON.stringify({ assets }), { + headers: { "content-type": "application/json" }, + }), ); const client = github.getOctokit("123", { request: { fetch: fetchRelease }, @@ -1311,7 +1316,7 @@ for (const fallback of [false, true]) { sinon.stub(api, "getApiClient").value(() => client); const authorizationSpy = sinon.spy(api, "getAuthorizationHeaderFor"); const extractStub = stubDownloadAndExtract(); - if (fallback) { + if (scenario === "fallback") { extractStub.onFirstCall().rejects(new HTTPError("Not Found", 404)); } @@ -1331,41 +1336,46 @@ for (const fallback of [false, true]) { getRunnerLogger(true), ); - t.true(fetchRelease.calledTwice); + const usesPerLanguageBundle = scenario === "per-language"; + t.true(fetchRelease.calledOnce); t.is( fetchRelease.firstCall.args[0], `https://api.github.com/repos/codeql-testing/action-fork/releases/tags/${tag}`, ); - t.is(extractStub.callCount, fallback ? 2 : 1); - t.is(extractStub.firstCall.args[0], assetURL); - t.is(extractStub.lastCall.args[0], fallback ? combinedURL : assetURL); - t.is(authorizationSpy.callCount, extractStub.callCount); - t.is(authorizationSpy.firstCall.args[2], assetURL); + t.is(extractStub.callCount, scenario === "fallback" ? 2 : 1); t.is( - authorizationSpy.lastCall.args[2], - fallback ? combinedURL : assetURL, + extractStub.firstCall.args[0], + scenario === "missing" ? combinedURL : assetURL, + ); + t.is( + extractStub.lastCall.args[0], + usesPerLanguageBundle ? assetURL : combinedURL, + ); + t.deepEqual( + authorizationSpy.getCalls().map((call) => call.args[2]), + extractStub.getCalls().map((call) => call.args[0]), ); t.is(extractStub.lastCall.args[3], "token token"); t.is(result.toolsVersion, MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION); t.is( result.toolsDownloadStatusReport?.perLanguage?.tools_bundle_language, - fallback ? undefined : BuiltInLanguage.java, + usesPerLanguageBundle ? BuiltInLanguage.java : undefined, ); t.is( result.toolsDownloadStatusReport?.perLanguage ?.tools_per_language_bundle_fallback, - fallback ? true : undefined, + usesPerLanguageBundle ? undefined : true, ); - if (fallback) { + if (usesPerLanguageBundle) { + t.is(path.dirname(result.codeqlFolder), tmpDir); + t.deepEqual(toolcache.findAllVersions("CodeQL"), []); + t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); + } else { t.is( result.codeqlFolder, toolcache.find("CodeQL", MIN_PER_LANGUAGE_BUNDLE_CLI_VERSION), ); t.true(fs.existsSync(`${result.codeqlFolder}.complete`)); - } else { - t.is(path.dirname(result.codeqlFolder), tmpDir); - t.deepEqual(toolcache.findAllVersions("CodeQL"), []); - t.false(fs.existsSync(`${result.codeqlFolder}.complete`)); } }); }, diff --git a/src/setup-codeql.ts b/src/setup-codeql.ts index 63a4e28e9..98767143a 100644 --- a/src/setup-codeql.ts +++ b/src/setup-codeql.ts @@ -21,9 +21,14 @@ import { CodeQLBundle, CodeQLDownloadSource, getCodeQLBundleFromUrl, - getCodeQLBundleName, } from "./codeql-bundle"; -import { getPublicRelease, selectBundle } from "./codeql-release"; +import { + BundleSelection, + BundleSelectionOptions, + getPublicRelease, + getRelease, + selectBundle, +} from "./codeql-release"; import * as defaults from "./defaults.json"; import { addNoLanguageDiagnostic, @@ -32,19 +37,14 @@ import { } from "./diagnostics"; import { EnvVar, getEnv } from "./environment"; import { - CODEQL_VERSION_ZSTD_BUNDLE, CodeQLDefaultVersionInfo, CodeQLVersionInfo, Feature, FeatureEnablement, } from "./feature-flags"; -import { BuiltInLanguage } from "./languages"; import { Logger } from "./logging"; import { getCodeQlVersionsForOverlayBaseDatabases } from "./overlay/caching"; -import { - getPerLanguageBundleLanguage, - logMissingPerLanguageBundle, -} from "./per-language-bundles"; +import { logMissingPerLanguageBundle } from "./per-language-bundles"; import { getBundlePlatform } from "./platform"; import * as tar from "./tar"; import { @@ -89,12 +89,19 @@ export function getCodeQLActionRepository(logger: Logger): string { return util.getRequiredEnvParam("GITHUB_ACTION_REPOSITORY"); } -async function getCodeQLBundleDownloadURL( +/** + * Selects a bundle from the first release tagged `tagName` that has a compatible bundle, trying the + * Action repositories on this GitHub instance before the canonical Action on GitHub.com. If we + * can't look up a release, or it has no compatible bundle, we move on to the next repository. We + * assume that the public release on GitHub.com has every bundle. + */ +async function selectDefaultBundle( + action: ActionState<["Logger", "ReadOnlyEnv", "FeatureFlags"]>, tagName: string, apiDetails: api.GitHubApiDetails, - codeQLBundleName: string, - logger: Logger, -): Promise { + options: BundleSelectionOptions, +): Promise { + const { logger } = action; const codeQLActionRepository = getCodeQLActionRepository(logger); const potentialDownloadSources = [ // This GitHub instance, and this Action. @@ -111,37 +118,38 @@ async function getCodeQLBundleDownloadURL( return !self.slice(0, index).some((other) => deepEqual(source, other)); }, ); - for (const downloadSource of uniqueDownloadSources) { - const [apiURL, repository] = downloadSource; + for (const [serverURL, repository] of uniqueDownloadSources) { // 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 && + serverURL === util.GITHUB_DOTCOM_URL && repository === CODEQL_DEFAULT_ACTION_REPOSITORY ) { break; } - const [repositoryOwner, repositoryName] = repository.split("/"); + const [owner, repo] = repository.split("/"); try { - const release = await api.getApiClient().rest.repos.getReleaseByTag({ - owner: repositoryOwner, - repo: repositoryName, - tag: tagName, - }); - for (const asset of release.data.assets) { - if (asset.name === codeQLBundleName) { - logger.info( - `Found CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} with URL ${asset.url}.`, - ); - return asset.url; - } - } + const release = await getRelease( + { apiClient: api.getApiClient() }, + { serverURL, owner, repo, tagName }, + ); + return await selectBundle(action, release, options); } catch (e) { logger.info( - `Looked for CodeQL bundle ${codeQLBundleName} in ${repository} on ${apiURL} but got error ${e}.`, + `Looked for CodeQL bundles in release ${tagName} of ${repository} on ${serverURL} but got error ${e}.`, ); } } - return `https://github.com/${CODEQL_DEFAULT_ACTION_REPOSITORY}/releases/download/${tagName}/${codeQLBundleName}`; + const [owner, repo] = CODEQL_DEFAULT_ACTION_REPOSITORY.split("/"); + return selectBundle( + action, + getPublicRelease({ + serverURL: util.GITHUB_DOTCOM_URL, + owner, + repo, + tagName, + }), + options, + ); } function tryGetBundleVersionFromTagName( @@ -719,58 +727,28 @@ export async function getCodeQLSource( } let compressionMethod: tar.CompressionMethod; + let perLanguageBundleFallback: true | undefined; if (!url) { - const bundleTagName = tagName; - if (bundleTagName === undefined) { + if (tagName === undefined) { throw new Error( "Could not determine a release tag for the requested CodeQL bundle.", ); } - - compressionMethod = - cliVersion !== undefined && - (await useZstdBundle(cliVersion, tarSupportsZstd)) - ? "zstd" - : "gzip"; - - const platform = getBundlePlatform(); - const perLanguageBundleLanguage = await getPerLanguageBundleLanguage( - { env: getEnv(), features, logger }, - { - rawLanguages, - cliVersion, - compressionMethod, - platform, - variant, - }, - ); - - // Resolves the combined or per-language bundle URL for the requested release. - const resolveBundleURL = (language?: BuiltInLanguage) => - getCodeQLBundleDownloadURL( - bundleTagName, + ({ bundle, compressionMethod, perLanguageBundleFallback } = + await selectDefaultBundle( + { env: getEnv(), features, logger }, + tagName, apiDetails, - getCodeQLBundleName(compressionMethod, platform, language), - logger, - ); - - const combinedBundleURL = await resolveBundleURL(); - if (perLanguageBundleLanguage !== undefined) { - logger.info( - `Selected the per-language CodeQL bundle for '${perLanguageBundleLanguage}'.`, - ); - url = await resolveBundleURL(perLanguageBundleLanguage); - bundle = { - kind: "per-language", - url, - language: perLanguageBundleLanguage, - combinedBundleURL, - }; - } else { - url = combinedBundleURL; - bundle = { kind: "combined", url }; - } + { + rawLanguages, + cliVersion, + platform: getBundlePlatform(), + variant, + tarSupportsZstd, + }, + )); + url = bundle.url; } else { const method = tar.inferCompressionMethod(url); if (method === undefined) { @@ -801,6 +779,7 @@ export async function getCodeQLSource( bundleVersion, cliVersion, compressionMethod, + ...(perLanguageBundleFallback ? { perLanguageBundleFallback } : {}), sourceType: "download", toolsVersion: resolvedVersion ?? "unknown", }; @@ -1161,18 +1140,6 @@ export async function downloadCodeQLBundle( } } -async function useZstdBundle( - cliVersion: string, - tarSupportsZstd: boolean, -): Promise { - return ( - // In testing, gzip performs better than zstd on Windows. - process.platform !== "win32" && - tarSupportsZstd && - semver.gte(cliVersion, CODEQL_VERSION_ZSTD_BUNDLE) - ); -} - function getTempExtractionDir(tempDir: string) { return path.join(tempDir, uuidV4()); }