Select the default bundle from the first compatible release

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Henry Mercer
2026-09-24 15:38:23 +01:00
parent 027ab3179c
commit 7f964a9e6e
3 changed files with 158 additions and 193 deletions

138
lib/entry-points.js generated
View File

@@ -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;

View File

@@ -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<Parameters<typeof fetch>, ReturnType<typeof fetch>>()
.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`));
}
});
},

View File

@@ -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<string> {
options: BundleSelectionOptions,
): Promise<BundleSelection> {
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<boolean> {
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());
}