Read the CLI version of a release from its assets

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Henry Mercer
2026-09-25 17:41:13 +01:00
parent 4a4922e538
commit e8f5d72ffe
3 changed files with 116 additions and 36 deletions

73
lib/entry-points.js generated
View File

@@ -26953,7 +26953,7 @@ var require_parse2 = __commonJS({
"node_modules/semver/functions/parse.js"(exports2, module2) {
"use strict";
var SemVer = require_semver();
var parse3 = (version, options, throwErrors = false) => {
var parse4 = (version, options, throwErrors = false) => {
if (version instanceof SemVer) {
return version;
}
@@ -26966,7 +26966,7 @@ var require_parse2 = __commonJS({
throw er;
}
};
module2.exports = parse3;
module2.exports = parse4;
}
});
@@ -26974,9 +26974,9 @@ var require_parse2 = __commonJS({
var require_valid = __commonJS({
"node_modules/semver/functions/valid.js"(exports2, module2) {
"use strict";
var parse3 = require_parse2();
var parse4 = require_parse2();
var valid4 = (version, options) => {
const v = parse3(version, options);
const v = parse4(version, options);
return v ? v.version : null;
};
module2.exports = valid4;
@@ -26987,9 +26987,9 @@ var require_valid = __commonJS({
var require_clean = __commonJS({
"node_modules/semver/functions/clean.js"(exports2, module2) {
"use strict";
var parse3 = require_parse2();
var parse4 = require_parse2();
var clean3 = (version, options) => {
const s = parse3(version.trim().replace(/^[=v]+/, ""), options);
const s = parse4(version.trim().replace(/^[=v]+/, ""), options);
return s ? s.version : null;
};
module2.exports = clean3;
@@ -27024,10 +27024,10 @@ var require_inc = __commonJS({
var require_diff = __commonJS({
"node_modules/semver/functions/diff.js"(exports2, module2) {
"use strict";
var parse3 = require_parse2();
var parse4 = require_parse2();
var diff = (version1, version2) => {
const v1 = parse3(version1, null, true);
const v2 = parse3(version2, null, true);
const v1 = parse4(version1, null, true);
const v2 = parse4(version2, null, true);
const comparison = v1.compare(v2);
if (comparison === 0) {
return null;
@@ -27098,9 +27098,9 @@ var require_patch = __commonJS({
var require_prerelease = __commonJS({
"node_modules/semver/functions/prerelease.js"(exports2, module2) {
"use strict";
var parse3 = require_parse2();
var parse4 = require_parse2();
var prerelease = (version, options) => {
const parsed = parse3(version, options);
const parsed = parse4(version, options);
return parsed && parsed.prerelease.length ? parsed.prerelease : null;
};
module2.exports = prerelease;
@@ -27286,7 +27286,7 @@ var require_coerce = __commonJS({
"node_modules/semver/functions/coerce.js"(exports2, module2) {
"use strict";
var SemVer = require_semver();
var parse3 = require_parse2();
var parse4 = require_parse2();
var { safeRe: re, t } = require_re();
var coerce3 = (version, options) => {
if (version instanceof SemVer) {
@@ -27321,7 +27321,7 @@ var require_coerce = __commonJS({
const patch = match2[4] || "0";
const prerelease = options.includePrerelease && match2[5] ? `-${match2[5]}` : "";
const build2 = options.includePrerelease && match2[6] ? `+${match2[6]}` : "";
return parse3(`${major}.${minor}.${patch}${prerelease}${build2}`, options);
return parse4(`${major}.${minor}.${patch}${prerelease}${build2}`, options);
};
module2.exports = coerce3;
}
@@ -27331,7 +27331,7 @@ var require_coerce = __commonJS({
var require_truncate = __commonJS({
"node_modules/semver/functions/truncate.js"(exports2, module2) {
"use strict";
var parse3 = require_parse2();
var parse4 = require_parse2();
var constants = require_constants6();
var SemVer = require_semver();
var truncate = (version, truncation, options) => {
@@ -27343,7 +27343,7 @@ var require_truncate = __commonJS({
};
var cloneInputVersion = (version, options) => {
const versionStringToParse = version instanceof SemVer ? version.version : version;
return parse3(versionStringToParse, options);
return parse4(versionStringToParse, options);
};
var doTruncation = (version, truncation) => {
if (isPrerelease(truncation)) {
@@ -28387,7 +28387,7 @@ var require_semver2 = __commonJS({
var constants = require_constants6();
var SemVer = require_semver();
var identifiers = require_identifiers();
var parse3 = require_parse2();
var parse4 = require_parse2();
var valid4 = require_valid();
var clean3 = require_clean();
var inc = require_inc();
@@ -28426,7 +28426,7 @@ var require_semver2 = __commonJS({
var simplifyRange = require_simplify();
var subset = require_subset();
module2.exports = {
parse: parse3,
parse: parse4,
valid: valid4,
clean: clean3,
inc,
@@ -32089,9 +32089,9 @@ var require_minimatch = __commonJS({
throw new TypeError("pattern is too long");
}
};
Minimatch2.prototype.parse = parse3;
Minimatch2.prototype.parse = parse4;
var SUBPARSE = {};
function parse3(pattern, isSub) {
function parse4(pattern, isSub) {
assertValidPattern2(pattern);
var options = this.options;
if (pattern === "**") {
@@ -33541,8 +33541,8 @@ var require_semver3 = __commonJS({
}
}
var i;
exports2.parse = parse3;
function parse3(version, options) {
exports2.parse = parse4;
function parse4(version, options) {
if (!options || typeof options !== "object") {
options = {
loose: !!options,
@@ -33570,12 +33570,12 @@ var require_semver3 = __commonJS({
}
exports2.valid = valid4;
function valid4(version, options) {
var v = parse3(version, options);
var v = parse4(version, options);
return v ? v.version : null;
}
exports2.clean = clean3;
function clean3(version, options) {
var s = parse3(version.trim().replace(/^[=v]+/, ""), options);
var s = parse4(version.trim().replace(/^[=v]+/, ""), options);
return s ? s.version : null;
}
exports2.SemVer = SemVer;
@@ -33811,8 +33811,8 @@ var require_semver3 = __commonJS({
if (eq(version1, version2)) {
return null;
} else {
var v1 = parse3(version1);
var v2 = parse3(version2);
var v1 = parse4(version1);
var v2 = parse4(version2);
var prefix = "";
if (v1.prerelease.length || v2.prerelease.length) {
prefix = "pre";
@@ -34518,7 +34518,7 @@ var require_semver3 = __commonJS({
}
exports2.prerelease = prerelease;
function prerelease(version, options) {
var parsed = parse3(version, options);
var parsed = parse4(version, options);
return parsed && parsed.prerelease.length ? parsed.prerelease : null;
}
exports2.intersects = intersects;
@@ -34555,7 +34555,7 @@ var require_semver3 = __commonJS({
if (match2 === null) {
return null;
}
return parse3(match2[2] + "." + (match2[3] || "0") + "." + (match2[4] || "0"), options);
return parse4(match2[2] + "." + (match2[3] || "0") + "." + (match2[4] || "0"), options);
}
}
});
@@ -37255,7 +37255,7 @@ var require_ms = __commonJS({
options = options || {};
var type = typeof val;
if (type === "string" && val.length > 0) {
return parse3(val);
return parse4(val);
} else if (type === "number" && isFinite(val)) {
return options.long ? fmtLong(val) : fmtShort(val);
}
@@ -37263,7 +37263,7 @@ var require_ms = __commonJS({
"val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
);
};
function parse3(str) {
function parse4(str) {
str = String(str);
if (str.length > 100) {
return;
@@ -43199,7 +43199,7 @@ var require_deserializationPolicy = __commonJS({
return result;
}
async function deserializeResponseBody(jsonContentTypes, xmlContentTypes, response, options, parseXML) {
const parsedResponse = await parse3(jsonContentTypes, xmlContentTypes, response, options, parseXML);
const parsedResponse = await parse4(jsonContentTypes, xmlContentTypes, response, options, parseXML);
if (!shouldDeserializeResponse(parsedResponse)) {
return parsedResponse;
}
@@ -43300,7 +43300,7 @@ var require_deserializationPolicy = __commonJS({
}
return { error: error3, shouldReturnResponse: false };
}
async function parse3(jsonContentTypes, xmlContentTypes, operationResponse, opts, parseXML) {
async function parse4(jsonContentTypes, xmlContentTypes, operationResponse, opts, parseXML) {
if (!operationResponse.request.streamResponseStatusCodes?.has(operationResponse.status) && operationResponse.bodyAsText) {
const text = operationResponse.bodyAsText;
const contentType = operationResponse.headers.get("Content-Type") || "";
@@ -112628,7 +112628,7 @@ var require_dist_node2 = __commonJS({
return template.replace(/\/$/, "");
}
}
function parse3(options) {
function parse4(options) {
let method = options.method.toUpperCase();
let url2 = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
let headers = Object.assign({}, options.headers);
@@ -112692,7 +112692,7 @@ var require_dist_node2 = __commonJS({
);
}
function endpointWithDefaults2(defaults3, route, options) {
return parse3(merge2(defaults3, route, options));
return parse4(merge2(defaults3, route, options));
}
function withDefaults4(oldDefaults, newDefaults) {
const DEFAULTS22 = merge2(oldDefaults, newDefaults);
@@ -112701,7 +112701,7 @@ var require_dist_node2 = __commonJS({
DEFAULTS: DEFAULTS22,
defaults: withDefaults4.bind(null, DEFAULTS22),
merge: merge2.bind(null, DEFAULTS22),
parse: parse3
parse: parse4
});
}
var endpoint2 = withDefaults4(null, DEFAULTS2);
@@ -116933,7 +116933,7 @@ var require_binary = __commonJS({
});
return stream2;
};
exports2.parse = function parse3(buffer) {
exports2.parse = function parse4(buffer) {
var self2 = words(function(bytes, cb) {
return function(name) {
if (offset + bytes <= buffer.length) {
@@ -151368,7 +151368,8 @@ async function getRelease({ apiClient }, reference) {
});
return {
url: getReleasePageURL(reference),
getAssetURL: (name) => release2.assets.find((asset) => asset.name === name)?.url
getAssetURL: (name) => release2.assets.find((asset) => asset.name === name)?.url,
assetNames: release2.assets.map((asset) => asset.name)
};
}
function getPublicRelease(reference) {

View File

@@ -5,6 +5,7 @@ import {
BundleSelectionOptions,
getPublicRelease,
getRelease,
getReleaseCliVersion,
parseCodeQLReleaseUrl,
selectBundle,
} from "./codeql-release";
@@ -290,3 +291,32 @@ test("parseCodeQLReleaseUrl excludes archives, REST references and untrusted URL
);
}
});
test("getRelease returns the names of the release's assets", async (t) => {
const assetNames = [COMBINED, "cli-version-2.27.2.txt"];
const fixture = releaseFixture({ assetNames });
const release = await getRelease(fixture.state, REFERENCE);
t.deepEqual(release.assetNames, assetNames);
});
test("getReleaseCliVersion prefers an unambiguous marker asset to the tag", (t) => {
for (const [tagName, markers, cliVersion] of [
[TAG, [], "2.27.1"],
[TAG, ["invalid"], "2.27.1"],
[TAG, ["2.27.2"], "2.27.2"],
[TAG, ["2.27.1", "2.28.0"], undefined],
["codeql-bundle-20260101", [], undefined],
["run-123", [], undefined],
["run-123", ["2.27.2+202601011200"], "2.27.2+202601011200"],
] as const) {
t.is(
getReleaseCliVersion(
tagName,
[COMBINED, ...markers.map((version) => `cli-version-${version}.txt`)],
getRecordingLogger([], { logToConsole: false }),
),
cliVersion,
`${tagName} ${markers.join(",")}`,
);
}
});

View File

@@ -4,6 +4,7 @@ import { ActionState } from "./action-common";
import type { GitHubApiDetails } from "./api-client";
import { CodeQLBundle, getCodeQLBundleName } from "./codeql-bundle";
import { CODEQL_VERSION_ZSTD_BUNDLE } from "./feature-flags";
import { Logger } from "./logging";
import {
getPerLanguageBundleLanguage,
logMissingPerLanguageBundle,
@@ -26,6 +27,8 @@ export interface CodeQLRelease {
url: string;
/** Returns the download URL for an asset, or `undefined` if the release doesn't have it. */
getAssetURL(name: string): string | undefined;
/** Names of the release's assets, if we looked up the release. */
assetNames?: string[];
}
/** A release requested by URL, on this GitHub instance or on GitHub.com. */
@@ -86,6 +89,51 @@ export function parseCodeQLReleaseUrl(
};
}
function parseCliVersion(value: string): string | undefined {
const parsed = semver.parse(value);
if (parsed === null) {
return undefined;
}
// semver's normalized version omits build metadata, which distinguishes nightly builds.
return (
parsed.version + (parsed.build.length ? `+${parsed.build.join(".")}` : "")
);
}
/**
* Returns the CLI version from the release's `cli-version-<version>.txt` asset if it has one, and
* otherwise the version in the tag. Returns `undefined` if the release has conflicting markers.
*/
export function getReleaseCliVersion(
tagName: string,
assetNames: string[],
logger: Logger,
): string | undefined {
const versions = new Set<string>();
for (const name of assetNames) {
const match = name.match(/^cli-version-(.+)\.txt$/);
if (match === null) {
continue;
}
const version = parseCliVersion(match[1]);
if (version !== undefined) {
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 undefined;
}
if (versions.size === 1) {
return versions.values().next().value;
}
return parseCliVersion(tagName.replace(/^codeql-bundle-/, ""));
}
/**
* Encodes a tag for use in a URL path. Slashes stay as path separators, as in GitHub's release URLs
* for tags like `build/123`.
@@ -121,6 +169,7 @@ export async function getRelease(
url: getReleasePageURL(reference),
getAssetURL: (name) =>
release.assets.find((asset) => asset.name === name)?.url,
assetNames: release.assets.map((asset) => asset.name),
};
}