Speed up version client by partial response reads (#807)

This commit is contained in:
Kevin Stillhammer
2026-07-21 17:26:32 +02:00
committed by GitHub
parent 47a7f4fb2e
commit 2269552d54
6 changed files with 719 additions and 201 deletions
+38 -4
View File
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import * as semver from "semver"; import * as semver from "semver";
import { VERSIONS_MANIFEST_URL } from "../../src/utils/constants";
const mockInfo = jest.fn(); const mockInfo = jest.fn();
const mockWarning = jest.fn(); const mockWarning = jest.fn();
@@ -36,11 +37,14 @@ const mockGetLatestVersion = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetAllVersions = jest.fn<any>(); const mockGetAllVersions = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests. // biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetFirstMatchingVersion = jest.fn<any>();
// biome-ignore lint/suspicious/noExplicitAny: Mock requires flexible typing in tests.
const mockGetArtifact = jest.fn<any>(); const mockGetArtifact = jest.fn<any>();
jest.unstable_mockModule("../../src/download/manifest", () => ({ jest.unstable_mockModule("../../src/download/manifest", () => ({
getAllVersions: mockGetAllVersions, getAllVersions: mockGetAllVersions,
getArtifact: mockGetArtifact, getArtifact: mockGetArtifact,
getFirstMatchingVersion: mockGetFirstMatchingVersion,
getLatestVersion: mockGetLatestVersion, getLatestVersion: mockGetLatestVersion,
})); }));
@@ -65,6 +69,7 @@ describe("download-version", () => {
mockCacheDir.mockReset(); mockCacheDir.mockReset();
mockGetLatestVersion.mockReset(); mockGetLatestVersion.mockReset();
mockGetAllVersions.mockReset(); mockGetAllVersions.mockReset();
mockGetFirstMatchingVersion.mockReset();
mockGetArtifact.mockReset(); mockGetArtifact.mockReset();
mockValidateChecksum.mockReset(); mockValidateChecksum.mockReset();
@@ -85,14 +90,43 @@ describe("download-version", () => {
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined); expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
}); });
it("uses the default manifest to resolve available versions", async () => { it("stops at the first matching version in the default manifest", async () => {
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]); mockGetFirstMatchingVersion.mockImplementation(
(predicate: (version: string) => boolean) =>
["0.9.26", "0.9.25"].find(predicate),
);
const version = await resolveVersion("^0.9.0", undefined); const version = await resolveVersion("^0.9.0", undefined);
expect(version).toBe("0.9.26"); expect(version).toBe("0.9.26");
expect(mockGetAllVersions).toHaveBeenCalledTimes(1); expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined); expect(mockGetAllVersions).not.toHaveBeenCalled();
});
it("streams ranges when the default manifest URL is explicit", async () => {
mockGetFirstMatchingVersion.mockImplementation(
(predicate: (version: string) => boolean) =>
["0.9.26", "0.9.25"].find(predicate),
);
const version = await resolveVersion("^0.9.0", VERSIONS_MANIFEST_URL);
expect(version).toBe("0.9.26");
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).not.toHaveBeenCalled();
});
it("streams PEP 440 ranges from the default manifest", async () => {
mockGetFirstMatchingVersion.mockImplementation(
(predicate: (version: string) => boolean) =>
["0.9.26", "0.9.25"].find(predicate),
);
const version = await resolveVersion("!=0.9.26", undefined);
expect(version).toBe("0.9.25");
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
expect(mockGetAllVersions).not.toHaveBeenCalled();
}); });
it("treats == exact pins as explicit versions", async () => { it("treats == exact pins as explicit versions", async () => {
+150
View File
@@ -17,6 +17,7 @@ const {
fetchManifest, fetchManifest,
getAllVersions, getAllVersions,
getArtifact, getArtifact,
getFirstMatchingVersion,
getLatestVersion, getLatestVersion,
parseManifest, parseManifest,
} = await import("../../src/download/manifest"); } = await import("../../src/download/manifest");
@@ -33,6 +34,7 @@ function createMockResponse(
data: string, data: string,
) { ) {
return { return {
body: null,
ok, ok,
status, status,
statusText, statusText,
@@ -40,6 +42,31 @@ function createMockResponse(
}; };
} }
function createStreamingMockResponse(
chunks: string[],
cancel: () => void | Promise<void> = () => {},
close = false,
) {
const encoder = new TextEncoder();
return {
body: new ReadableStream<Uint8Array>({
cancel,
start(controller) {
for (const chunk of chunks) {
controller.enqueue(encoder.encode(chunk));
}
if (close) {
controller.close();
}
},
}),
ok: true,
status: 200,
statusText: "OK",
text: async () => chunks.join(""),
};
}
describe("manifest", () => { describe("manifest", () => {
beforeEach(() => { beforeEach(() => {
clearManifestCache(); clearManifestCache();
@@ -105,6 +132,75 @@ describe("manifest", () => {
getLatestVersion("https://example.com/custom.ndjson"), getLatestVersion("https://example.com/custom.ndjson"),
).resolves.toBe("0.9.26"); ).resolves.toBe("0.9.26");
}); });
it("stops reading the default manifest after the first record", async () => {
const [latestVersion] = sampleManifestResponse.split("\n");
const cancel = jest.fn();
mockFetch.mockResolvedValue(
createStreamingMockResponse(
[
latestVersion.slice(0, 100),
`${latestVersion.slice(100)}\n`,
"invalid trailing data\n",
],
cancel,
),
);
await expect(getLatestVersion()).resolves.toBe("0.9.26");
await expect(
getArtifact("0.9.26", "aarch64", "apple-darwin"),
).resolves.toBeDefined();
expect(cancel).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it("does not fail when canceling the remaining response fails", async () => {
const [latestVersion] = sampleManifestResponse.split("\n");
mockFetch.mockResolvedValue(
createStreamingMockResponse([`${latestVersion}\n`], () =>
Promise.reject(new Error("cancel failed")),
),
);
await expect(getLatestVersion()).resolves.toBe("0.9.26");
});
});
describe("getFirstMatchingVersion", () => {
it("stops at the first matching record", async () => {
const cancel = jest.fn();
mockFetch.mockResolvedValue(
createStreamingMockResponse(
[`${sampleManifestResponse}\n`, "invalid trailing data\n"],
cancel,
),
);
await expect(
getFirstMatchingVersion((version) => version === "0.9.25"),
).resolves.toBe("0.9.25");
expect(cancel).toHaveBeenCalledTimes(1);
});
it("caches a fully consumed response stream", async () => {
mockFetch.mockResolvedValue(
createStreamingMockResponse(
[`${sampleManifestResponse}\n`],
undefined,
true,
),
);
await expect(
getFirstMatchingVersion((version) => version === "0.0.1"),
).resolves.toBeUndefined();
await expect(getLatestVersion()).resolves.toBe("0.9.26");
expect(mockFetch).toHaveBeenCalledTimes(1);
});
}); });
describe("getArtifact", () => { describe("getArtifact", () => {
@@ -168,6 +264,60 @@ describe("manifest", () => {
expect(artifact).toBeUndefined(); expect(artifact).toBeUndefined();
}); });
it("does not cache records from a failed stream read", async () => {
const [latestVersion] = sampleManifestResponse.split("\n");
mockFetch
.mockResolvedValueOnce(
createStreamingMockResponse([
`${latestVersion}\n`,
"invalid manifest record\n",
]),
)
.mockResolvedValueOnce(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await expect(
getArtifact("0.0.1", "aarch64", "apple-darwin"),
).rejects.toThrow("Failed to parse manifest data");
await expect(getLatestVersion()).resolves.toBe("0.9.26");
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it("does not cache records when the response stream fails", async () => {
const [latestVersion] = sampleManifestResponse.split("\n");
const encoder = new TextEncoder();
let sentVersion = false;
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (!sentVersion) {
sentVersion = true;
controller.enqueue(encoder.encode(`${latestVersion}\n`));
return;
}
controller.error(new Error("response stream failed"));
},
});
mockFetch
.mockResolvedValueOnce({
body,
ok: true,
status: 200,
statusText: "OK",
})
.mockResolvedValueOnce(
createMockResponse(true, 200, "OK", sampleManifestResponse),
);
await expect(
getArtifact("0.0.1", "aarch64", "apple-darwin"),
).rejects.toThrow("response stream failed");
await expect(getLatestVersion()).resolves.toBe("0.9.26");
expect(mockFetch).toHaveBeenCalledTimes(2);
});
}); });
describe("parseManifest", () => { describe("parseManifest", () => {
Generated Vendored
+206 -99
View File
@@ -9161,7 +9161,7 @@ var require_readable = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) {
"use strict"; "use strict";
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var { Readable: Readable5 } = require("node:stream"); var { Readable: Readable6 } = require("node:stream");
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError: AbortError3 } = require_errors(); var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError: AbortError3 } = require_errors();
var util7 = require_util(); var util7 = require_util();
var { ReadableStreamFrom } = require_util(); var { ReadableStreamFrom } = require_util();
@@ -9173,7 +9173,7 @@ var require_readable = __commonJS({
var kContentLength = /* @__PURE__ */ Symbol("kContentLength"); var kContentLength = /* @__PURE__ */ Symbol("kContentLength");
var noop = () => { var noop = () => {
}; };
var BodyReadable = class extends Readable5 { var BodyReadable = class extends Readable6 {
constructor({ constructor({
resume, resume,
abort, abort,
@@ -9515,7 +9515,7 @@ var require_api_request = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/api-request.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/api-request.js"(exports2, module2) {
"use strict"; "use strict";
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var { Readable: Readable5 } = require_readable(); var { Readable: Readable6 } = require_readable();
var { InvalidArgumentError, RequestAbortedError } = require_errors(); var { InvalidArgumentError, RequestAbortedError } = require_errors();
var util7 = require_util(); var util7 = require_util();
var { getResolveErrorBodyCallback } = require_util3(); var { getResolveErrorBodyCallback } = require_util3();
@@ -9610,7 +9610,7 @@ var require_api_request = __commonJS({
const parsedHeaders = responseHeaders === "raw" ? util7.parseHeaders(rawHeaders) : headers; const parsedHeaders = responseHeaders === "raw" ? util7.parseHeaders(rawHeaders) : headers;
const contentType2 = parsedHeaders["content-type"]; const contentType2 = parsedHeaders["content-type"];
const contentLength2 = parsedHeaders["content-length"]; const contentLength2 = parsedHeaders["content-length"];
const res = new Readable5({ const res = new Readable6({
resume, resume,
abort, abort,
contentType: contentType2, contentType: contentType2,
@@ -9925,7 +9925,7 @@ var require_api_pipeline = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
"use strict"; "use strict";
var { var {
Readable: Readable5, Readable: Readable6,
Duplex, Duplex,
PassThrough PassThrough
} = require("node:stream"); } = require("node:stream");
@@ -9939,7 +9939,7 @@ var require_api_pipeline = __commonJS({
var { addSignal, removeSignal } = require_abort_signal(); var { addSignal, removeSignal } = require_abort_signal();
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var kResume = /* @__PURE__ */ Symbol("resume"); var kResume = /* @__PURE__ */ Symbol("resume");
var PipelineRequest = class extends Readable5 { var PipelineRequest = class extends Readable6 {
constructor() { constructor() {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = null; this[kResume] = null;
@@ -9956,7 +9956,7 @@ var require_api_pipeline = __commonJS({
callback(err); callback(err);
} }
}; };
var PipelineResponse = class extends Readable5 { var PipelineResponse = class extends Readable6 {
constructor(resume) { constructor(resume) {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = resume; this[kResume] = resume;
@@ -13281,7 +13281,7 @@ var require_fetch = __commonJS({
subresourceSet subresourceSet
} = require_constants3(); } = require_constants3();
var EE = require("node:events"); var EE = require("node:events");
var { Readable: Readable5, pipeline: pipeline4, finished } = require("node:stream"); var { Readable: Readable6, pipeline: pipeline4, finished } = require("node:stream");
var { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require_util(); var { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require_util();
var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url(); var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url();
var { getGlobalDispatcher } = require_global2(); var { getGlobalDispatcher } = require_global2();
@@ -14182,7 +14182,7 @@ var require_fetch = __commonJS({
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
} }
location = headersList.get("location", true); location = headersList.get("location", true);
this.body = new Readable5({ read: resume }); this.body = new Readable6({ read: resume });
const decoders = []; const decoders = [];
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
@@ -20742,7 +20742,7 @@ var require_satisfies = __commonJS({
"node_modules/@actions/cache/node_modules/semver/functions/satisfies.js"(exports2, module2) { "node_modules/@actions/cache/node_modules/semver/functions/satisfies.js"(exports2, module2) {
"use strict"; "use strict";
var Range = require_range(); var Range = require_range();
var satisfies4 = (version3, range2, options) => { var satisfies5 = (version3, range2, options) => {
try { try {
range2 = new Range(range2, options); range2 = new Range(range2, options);
} catch (er) { } catch (er) {
@@ -20750,7 +20750,7 @@ var require_satisfies = __commonJS({
} }
return range2.test(version3); return range2.test(version3);
}; };
module2.exports = satisfies4; module2.exports = satisfies5;
} }
}); });
@@ -20886,14 +20886,14 @@ var require_valid2 = __commonJS({
"node_modules/@actions/cache/node_modules/semver/ranges/valid.js"(exports2, module2) { "node_modules/@actions/cache/node_modules/semver/ranges/valid.js"(exports2, module2) {
"use strict"; "use strict";
var Range = require_range(); var Range = require_range();
var validRange2 = (range2, options) => { var validRange3 = (range2, options) => {
try { try {
return new Range(range2, options).range || "*"; return new Range(range2, options).range || "*";
} catch (er) { } catch (er) {
return null; return null;
} }
}; };
module2.exports = validRange2; module2.exports = validRange3;
} }
}); });
@@ -20905,7 +20905,7 @@ var require_outside = __commonJS({
var Comparator = require_comparator(); var Comparator = require_comparator();
var { ANY } = Comparator; var { ANY } = Comparator;
var Range = require_range(); var Range = require_range();
var satisfies4 = require_satisfies(); var satisfies5 = require_satisfies();
var gt3 = require_gt(); var gt3 = require_gt();
var lt2 = require_lt(); var lt2 = require_lt();
var lte = require_lte(); var lte = require_lte();
@@ -20932,7 +20932,7 @@ var require_outside = __commonJS({
default: default:
throw new TypeError('Must provide a hilo val of "<" or ">"'); throw new TypeError('Must provide a hilo val of "<" or ">"');
} }
if (satisfies4(version3, range2, options)) { if (satisfies5(version3, range2, options)) {
return false; return false;
} }
for (let i = 0; i < range2.set.length; ++i) { for (let i = 0; i < range2.set.length; ++i) {
@@ -21004,7 +21004,7 @@ var require_intersects = __commonJS({
var require_simplify = __commonJS({ var require_simplify = __commonJS({
"node_modules/@actions/cache/node_modules/semver/ranges/simplify.js"(exports2, module2) { "node_modules/@actions/cache/node_modules/semver/ranges/simplify.js"(exports2, module2) {
"use strict"; "use strict";
var satisfies4 = require_satisfies(); var satisfies5 = require_satisfies();
var compare2 = require_compare(); var compare2 = require_compare();
module2.exports = (versions, range2, options) => { module2.exports = (versions, range2, options) => {
const set = []; const set = [];
@@ -21012,7 +21012,7 @@ var require_simplify = __commonJS({
let prev = null; let prev = null;
const v = versions.sort((a, b) => compare2(a, b, options)); const v = versions.sort((a, b) => compare2(a, b, options));
for (const version3 of v) { for (const version3 of v) {
const included = satisfies4(version3, range2, options); const included = satisfies5(version3, range2, options);
if (included) { if (included) {
prev = version3; prev = version3;
if (!first) { if (!first) {
@@ -21057,7 +21057,7 @@ var require_subset = __commonJS({
var Range = require_range(); var Range = require_range();
var Comparator = require_comparator(); var Comparator = require_comparator();
var { ANY } = Comparator; var { ANY } = Comparator;
var satisfies4 = require_satisfies(); var satisfies5 = require_satisfies();
var compare2 = require_compare(); var compare2 = require_compare();
var subset = (sub, dom, options = {}) => { var subset = (sub, dom, options = {}) => {
if (sub === dom) { if (sub === dom) {
@@ -21126,14 +21126,14 @@ var require_subset = __commonJS({
} }
} }
for (const eq2 of eqSet) { for (const eq2 of eqSet) {
if (gt3 && !satisfies4(eq2, String(gt3), options)) { if (gt3 && !satisfies5(eq2, String(gt3), options)) {
return null; return null;
} }
if (lt2 && !satisfies4(eq2, String(lt2), options)) { if (lt2 && !satisfies5(eq2, String(lt2), options)) {
return null; return null;
} }
for (const c of dom) { for (const c of dom) {
if (!satisfies4(eq2, String(c), options)) { if (!satisfies5(eq2, String(c), options)) {
return false; return false;
} }
} }
@@ -21160,7 +21160,7 @@ var require_subset = __commonJS({
if (higher === c && higher !== gt3) { if (higher === c && higher !== gt3) {
return false; return false;
} }
} else if (gt3.operator === ">=" && !satisfies4(gt3.semver, String(c), options)) { } else if (gt3.operator === ">=" && !satisfies5(gt3.semver, String(c), options)) {
return false; return false;
} }
} }
@@ -21175,7 +21175,7 @@ var require_subset = __commonJS({
if (lower === c && lower !== lt2) { if (lower === c && lower !== lt2) {
return false; return false;
} }
} else if (lt2.operator === "<=" && !satisfies4(lt2.semver, String(c), options)) { } else if (lt2.operator === "<=" && !satisfies5(lt2.semver, String(c), options)) {
return false; return false;
} }
} }
@@ -21245,12 +21245,12 @@ var require_semver2 = __commonJS({
var coerce = require_coerce(); var coerce = require_coerce();
var Comparator = require_comparator(); var Comparator = require_comparator();
var Range = require_range(); var Range = require_range();
var satisfies4 = require_satisfies(); var satisfies5 = require_satisfies();
var toComparators = require_to_comparators(); var toComparators = require_to_comparators();
var maxSatisfying3 = require_max_satisfying(); var maxSatisfying3 = require_max_satisfying();
var minSatisfying4 = require_min_satisfying(); var minSatisfying4 = require_min_satisfying();
var minVersion = require_min_version(); var minVersion = require_min_version();
var validRange2 = require_valid2(); var validRange3 = require_valid2();
var outside = require_outside(); var outside = require_outside();
var gtr = require_gtr(); var gtr = require_gtr();
var ltr = require_ltr(); var ltr = require_ltr();
@@ -21283,12 +21283,12 @@ var require_semver2 = __commonJS({
coerce, coerce,
Comparator, Comparator,
Range, Range,
satisfies: satisfies4, satisfies: satisfies5,
toComparators, toComparators,
maxSatisfying: maxSatisfying3, maxSatisfying: maxSatisfying3,
minSatisfying: minSatisfying4, minSatisfying: minSatisfying4,
minVersion, minVersion,
validRange: validRange2, validRange: validRange3,
outside, outside,
gtr, gtr,
ltr, ltr,
@@ -28352,7 +28352,7 @@ var require_satisfies2 = __commonJS({
"node_modules/@actions/tool-cache/node_modules/semver/functions/satisfies.js"(exports2, module2) { "node_modules/@actions/tool-cache/node_modules/semver/functions/satisfies.js"(exports2, module2) {
"use strict"; "use strict";
var Range = require_range2(); var Range = require_range2();
var satisfies4 = (version3, range2, options) => { var satisfies5 = (version3, range2, options) => {
try { try {
range2 = new Range(range2, options); range2 = new Range(range2, options);
} catch (er) { } catch (er) {
@@ -28360,7 +28360,7 @@ var require_satisfies2 = __commonJS({
} }
return range2.test(version3); return range2.test(version3);
}; };
module2.exports = satisfies4; module2.exports = satisfies5;
} }
}); });
@@ -28496,14 +28496,14 @@ var require_valid4 = __commonJS({
"node_modules/@actions/tool-cache/node_modules/semver/ranges/valid.js"(exports2, module2) { "node_modules/@actions/tool-cache/node_modules/semver/ranges/valid.js"(exports2, module2) {
"use strict"; "use strict";
var Range = require_range2(); var Range = require_range2();
var validRange2 = (range2, options) => { var validRange3 = (range2, options) => {
try { try {
return new Range(range2, options).range || "*"; return new Range(range2, options).range || "*";
} catch (er) { } catch (er) {
return null; return null;
} }
}; };
module2.exports = validRange2; module2.exports = validRange3;
} }
}); });
@@ -28515,7 +28515,7 @@ var require_outside2 = __commonJS({
var Comparator = require_comparator2(); var Comparator = require_comparator2();
var { ANY } = Comparator; var { ANY } = Comparator;
var Range = require_range2(); var Range = require_range2();
var satisfies4 = require_satisfies2(); var satisfies5 = require_satisfies2();
var gt3 = require_gt2(); var gt3 = require_gt2();
var lt2 = require_lt2(); var lt2 = require_lt2();
var lte = require_lte2(); var lte = require_lte2();
@@ -28542,7 +28542,7 @@ var require_outside2 = __commonJS({
default: default:
throw new TypeError('Must provide a hilo val of "<" or ">"'); throw new TypeError('Must provide a hilo val of "<" or ">"');
} }
if (satisfies4(version3, range2, options)) { if (satisfies5(version3, range2, options)) {
return false; return false;
} }
for (let i = 0; i < range2.set.length; ++i) { for (let i = 0; i < range2.set.length; ++i) {
@@ -28614,7 +28614,7 @@ var require_intersects2 = __commonJS({
var require_simplify2 = __commonJS({ var require_simplify2 = __commonJS({
"node_modules/@actions/tool-cache/node_modules/semver/ranges/simplify.js"(exports2, module2) { "node_modules/@actions/tool-cache/node_modules/semver/ranges/simplify.js"(exports2, module2) {
"use strict"; "use strict";
var satisfies4 = require_satisfies2(); var satisfies5 = require_satisfies2();
var compare2 = require_compare2(); var compare2 = require_compare2();
module2.exports = (versions, range2, options) => { module2.exports = (versions, range2, options) => {
const set = []; const set = [];
@@ -28622,7 +28622,7 @@ var require_simplify2 = __commonJS({
let prev = null; let prev = null;
const v = versions.sort((a, b) => compare2(a, b, options)); const v = versions.sort((a, b) => compare2(a, b, options));
for (const version3 of v) { for (const version3 of v) {
const included = satisfies4(version3, range2, options); const included = satisfies5(version3, range2, options);
if (included) { if (included) {
prev = version3; prev = version3;
if (!first) { if (!first) {
@@ -28667,7 +28667,7 @@ var require_subset2 = __commonJS({
var Range = require_range2(); var Range = require_range2();
var Comparator = require_comparator2(); var Comparator = require_comparator2();
var { ANY } = Comparator; var { ANY } = Comparator;
var satisfies4 = require_satisfies2(); var satisfies5 = require_satisfies2();
var compare2 = require_compare2(); var compare2 = require_compare2();
var subset = (sub, dom, options = {}) => { var subset = (sub, dom, options = {}) => {
if (sub === dom) { if (sub === dom) {
@@ -28736,14 +28736,14 @@ var require_subset2 = __commonJS({
} }
} }
for (const eq2 of eqSet) { for (const eq2 of eqSet) {
if (gt3 && !satisfies4(eq2, String(gt3), options)) { if (gt3 && !satisfies5(eq2, String(gt3), options)) {
return null; return null;
} }
if (lt2 && !satisfies4(eq2, String(lt2), options)) { if (lt2 && !satisfies5(eq2, String(lt2), options)) {
return null; return null;
} }
for (const c of dom) { for (const c of dom) {
if (!satisfies4(eq2, String(c), options)) { if (!satisfies5(eq2, String(c), options)) {
return false; return false;
} }
} }
@@ -28770,7 +28770,7 @@ var require_subset2 = __commonJS({
if (higher === c && higher !== gt3) { if (higher === c && higher !== gt3) {
return false; return false;
} }
} else if (gt3.operator === ">=" && !satisfies4(gt3.semver, String(c), options)) { } else if (gt3.operator === ">=" && !satisfies5(gt3.semver, String(c), options)) {
return false; return false;
} }
} }
@@ -28785,7 +28785,7 @@ var require_subset2 = __commonJS({
if (lower === c && lower !== lt2) { if (lower === c && lower !== lt2) {
return false; return false;
} }
} else if (lt2.operator === "<=" && !satisfies4(lt2.semver, String(c), options)) { } else if (lt2.operator === "<=" && !satisfies5(lt2.semver, String(c), options)) {
return false; return false;
} }
} }
@@ -28855,12 +28855,12 @@ var require_semver4 = __commonJS({
var coerce = require_coerce2(); var coerce = require_coerce2();
var Comparator = require_comparator2(); var Comparator = require_comparator2();
var Range = require_range2(); var Range = require_range2();
var satisfies4 = require_satisfies2(); var satisfies5 = require_satisfies2();
var toComparators = require_to_comparators2(); var toComparators = require_to_comparators2();
var maxSatisfying3 = require_max_satisfying2(); var maxSatisfying3 = require_max_satisfying2();
var minSatisfying4 = require_min_satisfying2(); var minSatisfying4 = require_min_satisfying2();
var minVersion = require_min_version2(); var minVersion = require_min_version2();
var validRange2 = require_valid4(); var validRange3 = require_valid4();
var outside = require_outside2(); var outside = require_outside2();
var gtr = require_gtr2(); var gtr = require_gtr2();
var ltr = require_ltr2(); var ltr = require_ltr2();
@@ -28893,12 +28893,12 @@ var require_semver4 = __commonJS({
coerce, coerce,
Comparator, Comparator,
Range, Range,
satisfies: satisfies4, satisfies: satisfies5,
toComparators, toComparators,
maxSatisfying: maxSatisfying3, maxSatisfying: maxSatisfying3,
minSatisfying: minSatisfying4, minSatisfying: minSatisfying4,
minVersion, minVersion,
validRange: validRange2, validRange: validRange3,
outside, outside,
gtr, gtr,
ltr, ltr,
@@ -40422,7 +40422,7 @@ var require_readable2 = __commonJS({
"use strict"; "use strict";
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var { addAbortListener } = require("node:events"); var { addAbortListener } = require("node:events");
var { Readable: Readable5 } = require("node:stream"); var { Readable: Readable6 } = require("node:stream");
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError: AbortError3 } = require_errors2(); var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError: AbortError3 } = require_errors2();
var util7 = require_util9(); var util7 = require_util9();
var { ReadableStreamFrom } = require_util9(); var { ReadableStreamFrom } = require_util9();
@@ -40436,7 +40436,7 @@ var require_readable2 = __commonJS({
var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead"); var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead");
var noop = () => { var noop = () => {
}; };
var BodyReadable = class extends Readable5 { var BodyReadable = class extends Readable6 {
/** /**
* @param {object} opts * @param {object} opts
* @param {(this: Readable, size: number) => void} opts.resume * @param {(this: Readable, size: number) => void} opts.resume
@@ -40825,7 +40825,7 @@ var require_api_request2 = __commonJS({
"use strict"; "use strict";
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var { AsyncResource } = require("node:async_hooks"); var { AsyncResource } = require("node:async_hooks");
var { Readable: Readable5 } = require_readable2(); var { Readable: Readable6 } = require_readable2();
var { InvalidArgumentError, RequestAbortedError } = require_errors2(); var { InvalidArgumentError, RequestAbortedError } = require_errors2();
var util7 = require_util9(); var util7 = require_util9();
function noop() { function noop() {
@@ -40909,7 +40909,7 @@ var require_api_request2 = __commonJS({
const parsedHeaders = headers; const parsedHeaders = headers;
const contentType2 = parsedHeaders?.["content-type"]; const contentType2 = parsedHeaders?.["content-type"];
const contentLength2 = parsedHeaders?.["content-length"]; const contentLength2 = parsedHeaders?.["content-length"];
const res = new Readable5({ const res = new Readable6({
resume: () => controller.resume(), resume: () => controller.resume(),
abort: (reason) => controller.abort(reason), abort: (reason) => controller.abort(reason),
contentType: contentType2, contentType: contentType2,
@@ -41285,7 +41285,7 @@ var require_api_pipeline2 = __commonJS({
"node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) { "node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
"use strict"; "use strict";
var { var {
Readable: Readable5, Readable: Readable6,
Duplex, Duplex,
PassThrough PassThrough
} = require("node:stream"); } = require("node:stream");
@@ -41302,7 +41302,7 @@ var require_api_pipeline2 = __commonJS({
function noop() { function noop() {
} }
var kResume = /* @__PURE__ */ Symbol("resume"); var kResume = /* @__PURE__ */ Symbol("resume");
var PipelineRequest = class extends Readable5 { var PipelineRequest = class extends Readable6 {
constructor() { constructor() {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = null; this[kResume] = null;
@@ -41320,7 +41320,7 @@ var require_api_pipeline2 = __commonJS({
callback(err); callback(err);
} }
}; };
var PipelineResponse = class extends Readable5 { var PipelineResponse = class extends Readable6 {
constructor(resume) { constructor(resume) {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = resume; this[kResume] = resume;
@@ -45937,7 +45937,7 @@ var require_cache3 = __commonJS({
"node_modules/undici/lib/interceptor/cache.js"(exports2, module2) { "node_modules/undici/lib/interceptor/cache.js"(exports2, module2) {
"use strict"; "use strict";
var assert4 = require("node:assert"); var assert4 = require("node:assert");
var { Readable: Readable5 } = require("node:stream"); var { Readable: Readable6 } = require("node:stream");
var util7 = require_util9(); var util7 = require_util9();
var CacheHandler = require_cache_handler(); var CacheHandler = require_cache_handler();
var MemoryCacheStore = require_memory_cache_store(); var MemoryCacheStore = require_memory_cache_store();
@@ -46033,7 +46033,7 @@ var require_cache3 = __commonJS({
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)); return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler));
} }
function sendCachedValue(handler, opts, result, age, context3, isStale2) { function sendCachedValue(handler, opts, result, age, context3, isStale2) {
const stream4 = util7.isStream(result.body) ? result.body : Readable5.from(result.body ?? []); const stream4 = util7.isStream(result.body) ? result.body : Readable6.from(result.body ?? []);
assert4(!stream4.destroyed, "stream should not be destroyed"); assert4(!stream4.destroyed, "stream should not be destroyed");
assert4(!stream4.readableDidRead, "stream should not be readableDidRead"); assert4(!stream4.readableDidRead, "stream should not be readableDidRead");
const controller = { const controller = {
@@ -49156,7 +49156,7 @@ var require_fetch2 = __commonJS({
subresourceSet subresourceSet
} = require_constants10(); } = require_constants10();
var EE = require("node:events"); var EE = require("node:events");
var { Readable: Readable5, pipeline: pipeline4, finished, isErrored, isReadable } = require("node:stream"); var { Readable: Readable6, pipeline: pipeline4, finished, isErrored, isReadable } = require("node:stream");
var { addAbortListener, bufferToLowerCasedHeaderName } = require_util9(); var { addAbortListener, bufferToLowerCasedHeaderName } = require_util9();
var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2(); var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2();
var { getGlobalDispatcher } = require_global4(); var { getGlobalDispatcher } = require_global4();
@@ -50118,7 +50118,7 @@ var require_fetch2 = __commonJS({
const headersList = new HeadersList(); const headersList = new HeadersList();
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders); appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders);
const location = headersList.get("location", true); const location = headersList.get("location", true);
this.body = new Readable5({ read: () => controller.resume() }); this.body = new Readable6({ read: () => controller.resume() });
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
const decoders = []; const decoders = [];
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
@@ -55017,13 +55017,13 @@ var require_semver5 = __commonJS({
return true; return true;
} }
rangeTmp = new Range(comp26.value, options); rangeTmp = new Range(comp26.value, options);
return satisfies4(this.value, rangeTmp, options); return satisfies5(this.value, rangeTmp, options);
} else if (comp26.operator === "") { } else if (comp26.operator === "") {
if (comp26.value === "") { if (comp26.value === "") {
return true; return true;
} }
rangeTmp = new Range(this.value, options); rangeTmp = new Range(this.value, options);
return satisfies4(comp26.semver, rangeTmp, options); return satisfies5(comp26.semver, rangeTmp, options);
} }
var sameDirectionIncreasing = (this.operator === ">=" || this.operator === ">") && (comp26.operator === ">=" || comp26.operator === ">"); var sameDirectionIncreasing = (this.operator === ">=" || this.operator === ">") && (comp26.operator === ">=" || comp26.operator === ">");
var sameDirectionDecreasing = (this.operator === "<=" || this.operator === "<") && (comp26.operator === "<=" || comp26.operator === "<"); var sameDirectionDecreasing = (this.operator === "<=" || this.operator === "<") && (comp26.operator === "<=" || comp26.operator === "<");
@@ -55350,8 +55350,8 @@ var require_semver5 = __commonJS({
} }
return true; return true;
} }
exports2.satisfies = satisfies4; exports2.satisfies = satisfies5;
function satisfies4(version3, range2, options) { function satisfies5(version3, range2, options) {
try { try {
range2 = new Range(range2, options); range2 = new Range(range2, options);
} catch (er) { } catch (er) {
@@ -55442,8 +55442,8 @@ var require_semver5 = __commonJS({
} }
return null; return null;
} }
exports2.validRange = validRange2; exports2.validRange = validRange3;
function validRange2(range2, options) { function validRange3(range2, options) {
try { try {
return new Range(range2, options).range || "*"; return new Range(range2, options).range || "*";
} catch (er) { } catch (er) {
@@ -55481,7 +55481,7 @@ var require_semver5 = __commonJS({
default: default:
throw new TypeError('Must provide a hilo val of "<" or ">"'); throw new TypeError('Must provide a hilo val of "<" or ">"');
} }
if (satisfies4(version3, range2, options)) { if (satisfies5(version3, range2, options)) {
return false; return false;
} }
for (var i2 = 0; i2 < range2.set.length; ++i2) { for (var i2 = 0; i2 < range2.set.length; ++i2) {
@@ -96116,6 +96116,10 @@ async function validateFileCheckSum(filePath, expected) {
}); });
} }
// src/download/manifest.ts
var import_node_readline = require("node:readline");
var import_node_stream5 = require("node:stream");
// src/utils/fetch.ts // src/utils/fetch.ts
var import_undici2 = __toESM(require_undici2(), 1); var import_undici2 = __toESM(require_undici2(), 1);
function getProxyAgent() { function getProxyAgent() {
@@ -96169,21 +96173,15 @@ function formatVariants(entries) {
// src/download/manifest.ts // src/download/manifest.ts
var cachedManifestData = /* @__PURE__ */ new Map(); var cachedManifestData = /* @__PURE__ */ new Map();
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) { async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
const cachedVersions = cachedManifestData.get(manifestUrl); const cachedManifest = cachedManifestData.get(manifestUrl);
if (cachedVersions !== void 0) { if (cachedManifest?.complete === true) {
debug(`Using cached manifest data from ${manifestUrl}`); debug(`Using cached manifest data from ${manifestUrl}`);
return cachedVersions; return cachedManifest.versions;
}
info2(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
);
} }
const response = await fetchManifestResponse(manifestUrl);
const body2 = await response.text(); const body2 = await response.text();
const versions = parseManifest(body2, manifestUrl); const versions = parseManifest(body2, manifestUrl);
cachedManifestData.set(manifestUrl, versions); cachedManifestData.set(manifestUrl, { complete: true, versions });
return versions; return versions;
} }
function parseManifest(data, sourceDescription) { function parseManifest(data, sourceDescription) {
@@ -96191,31 +96189,14 @@ function parseManifest(data, sourceDescription) {
if (trimmed === "") { if (trimmed === "") {
throw new Error(`Manifest at ${sourceDescription} is empty.`); throw new Error(`Manifest at ${sourceDescription} is empty.`);
} }
if (trimmed.startsWith("[")) { rejectLegacyManifest(trimmed, sourceDescription);
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
);
}
const versions = []; const versions = [];
for (const [index, line] of data.split("\n").entries()) { for (const [index, line] of data.split("\n").entries()) {
const record = line.trim(); const record = line.trim();
if (record === "") { if (record === "") {
continue; continue;
} }
let parsed; versions.push(parseManifestRecord(record, sourceDescription, index + 1));
try {
parsed = JSON.parse(record);
} catch (error2) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${error2.message}`
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`
);
}
versions.push(parsed);
} }
if (versions.length === 0) { if (versions.length === 0) {
throw new Error(`No manifest data found in ${sourceDescription}.`); throw new Error(`No manifest data found in ${sourceDescription}.`);
@@ -96223,13 +96204,16 @@ function parseManifest(data, sourceDescription) {
return versions; return versions;
} }
async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) { async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) {
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version; const latestVersion = manifestUrl === VERSIONS_MANIFEST_URL ? (await findManifestVersion(() => true))?.version : (await fetchManifest(manifestUrl))[0]?.version;
if (latestVersion === void 0) { if (latestVersion === void 0) {
throw new Error("No versions found in manifest data"); throw new Error("No versions found in manifest data");
} }
debug(`Latest version from manifest: ${latestVersion}`); debug(`Latest version from manifest: ${latestVersion}`);
return latestVersion; return latestVersion;
} }
async function getFirstMatchingVersion(predicate) {
return (await findManifestVersion((versionData) => predicate(versionData.version)))?.version;
}
async function getAllVersions(manifestUrl = VERSIONS_MANIFEST_URL) { async function getAllVersions(manifestUrl = VERSIONS_MANIFEST_URL) {
info2( info2(
`Getting available versions from ${manifestSource(manifestUrl)} ...` `Getting available versions from ${manifestSource(manifestUrl)} ...`
@@ -96238,8 +96222,7 @@ async function getAllVersions(manifestUrl = VERSIONS_MANIFEST_URL) {
return versions.map((versionData) => versionData.version); return versions.map((versionData) => versionData.version);
} }
async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MANIFEST_URL) { async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MANIFEST_URL) {
const versions = await fetchManifest(manifestUrl); const versionData = manifestUrl === VERSIONS_MANIFEST_URL ? await findManifestVersion((candidate) => candidate.version === version3) : (await fetchManifest(manifestUrl)).find(
const versionData = versions.find(
(candidate) => candidate.version === version3 (candidate) => candidate.version === version3
); );
if (!versionData) { if (!versionData) {
@@ -96266,12 +96249,103 @@ async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MA
downloadUrl: artifact.url downloadUrl: artifact.url
}; };
} }
async function fetchManifestResponse(manifestUrl) {
info2(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
);
}
return response;
}
async function findManifestVersion(predicate) {
const cachedManifest = cachedManifestData.get(VERSIONS_MANIFEST_URL);
const cachedVersion = cachedManifest?.versions.find(predicate);
if (cachedVersion !== void 0 || cachedManifest?.complete === true) {
return cachedVersion;
}
const response = await fetchManifestResponse(VERSIONS_MANIFEST_URL);
if (response.body === null) {
const versions2 = parseManifest(
await response.text(),
VERSIONS_MANIFEST_URL
);
cachedManifestData.set(VERSIONS_MANIFEST_URL, {
complete: true,
versions: versions2
});
return versions2.find(predicate);
}
const input = import_node_stream5.Readable.fromWeb(response.body);
const lines = (0, import_node_readline.createInterface)({ crlfDelay: Number.POSITIVE_INFINITY, input });
const versions = [];
let complete = false;
let lineNumber = 0;
let matchedVersion;
try {
for await (const line of lines) {
lineNumber += 1;
const record = line.trim();
if (record === "") {
continue;
}
if (versions.length === 0) {
rejectLegacyManifest(record, VERSIONS_MANIFEST_URL);
}
const versionData = parseManifestRecord(
record,
VERSIONS_MANIFEST_URL,
lineNumber
);
versions.push(versionData);
if (predicate(versionData)) {
matchedVersion = versionData;
break;
}
}
complete = matchedVersion === void 0;
} finally {
lines.close();
if (!complete) {
input.destroy();
}
}
if (versions.length === 0) {
throw new Error(`Manifest at ${VERSIONS_MANIFEST_URL} is empty.`);
}
cachedManifestData.set(VERSIONS_MANIFEST_URL, { complete, versions });
return matchedVersion;
}
function manifestSource(manifestUrl) { function manifestSource(manifestUrl) {
if (manifestUrl === VERSIONS_MANIFEST_URL) { if (manifestUrl === VERSIONS_MANIFEST_URL) {
return VERSIONS_MANIFEST_URL; return VERSIONS_MANIFEST_URL;
} }
return `manifest-file ${manifestUrl}`; return `manifest-file ${manifestUrl}`;
} }
function parseManifestRecord(record, sourceDescription, lineNumber) {
let parsed;
try {
parsed = JSON.parse(record);
} catch (error2) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${lineNumber}: ${error2.message}`
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${lineNumber}.`
);
}
return parsed;
}
function rejectLegacyManifest(data, sourceDescription) {
if (data.startsWith("[")) {
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
);
}
}
function isManifestVersion(value) { function isManifestVersion(value) {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
@@ -97735,9 +97809,19 @@ var RangeVersionResolver = class {
if (context3.parsedSpecifier.kind !== "range") { if (context3.parsedSpecifier.kind !== "range") {
return void 0; return void 0;
} }
const availableVersions = await getAllVersions(context3.manifestUrl); let resolvedVersion;
debug(`Available versions: ${availableVersions}`); if (context3.resolutionStrategy === "highest" && (context3.manifestUrl === void 0 || context3.manifestUrl === VERSIONS_MANIFEST_URL)) {
const resolvedVersion = context3.resolutionStrategy === "lowest" ? minSatisfying3(availableVersions, context3.parsedSpecifier.normalized) : maxSatisfying2(availableVersions, context3.parsedSpecifier.normalized); resolvedVersion = await findHighestSatisfyingVersion(
context3.parsedSpecifier.normalized
);
} else {
const availableVersions = await getAllVersions(context3.manifestUrl);
debug(`Available versions: ${availableVersions}`);
resolvedVersion = context3.resolutionStrategy === "lowest" ? minSatisfying3(availableVersions, context3.parsedSpecifier.normalized) : maxSatisfying2(
availableVersions,
context3.parsedSpecifier.normalized
);
}
if (resolvedVersion === void 0) { if (resolvedVersion === void 0) {
throw new Error(`No version found for ${context3.parsedSpecifier.raw}`); throw new Error(`No version found for ${context3.parsedSpecifier.raw}`);
} }
@@ -97774,6 +97858,29 @@ async function resolveVersion(versionInput, manifestUrl, resolutionStrategy = "h
} }
throw new Error(`No version found for ${versionInput}`); throw new Error(`No version found for ${versionInput}`);
} }
async function findHighestSatisfyingVersion(versionSpecifier) {
const semverRange = semver4.validRange(versionSpecifier);
if (semverRange !== null) {
const semverMatch = await getFirstMatchingVersion(
(version3) => semver4.satisfies(version3, semverRange)
);
if (semverMatch !== void 0) {
debug(
`Found a version that satisfies the semver range: ${semverMatch}`
);
return semverMatch;
}
}
const pep440Match = await getFirstMatchingVersion(
(version3) => satisfies3(version3, versionSpecifier)
);
if (pep440Match !== void 0) {
debug(
`Found a version that satisfies the pep440 specifier: ${pep440Match}`
);
}
return pep440Match;
}
function maxSatisfying2(versions, version3) { function maxSatisfying2(versions, version3) {
const maxSemver = evaluateVersions(versions, version3); const maxSemver = evaluateVersions(versions, version3);
if (maxSemver !== "") { if (maxSemver !== "") {
+123 -51
View File
@@ -9157,7 +9157,7 @@ var require_readable = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) {
"use strict"; "use strict";
var assert = require("node:assert"); var assert = require("node:assert");
var { Readable } = require("node:stream"); var { Readable: Readable2 } = require("node:stream");
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors(); var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors();
var util = require_util(); var util = require_util();
var { ReadableStreamFrom } = require_util(); var { ReadableStreamFrom } = require_util();
@@ -9169,7 +9169,7 @@ var require_readable = __commonJS({
var kContentLength = /* @__PURE__ */ Symbol("kContentLength"); var kContentLength = /* @__PURE__ */ Symbol("kContentLength");
var noop = () => { var noop = () => {
}; };
var BodyReadable = class extends Readable { var BodyReadable = class extends Readable2 {
constructor({ constructor({
resume, resume,
abort, abort,
@@ -9511,7 +9511,7 @@ var require_api_request = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/api-request.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/api-request.js"(exports2, module2) {
"use strict"; "use strict";
var assert = require("node:assert"); var assert = require("node:assert");
var { Readable } = require_readable(); var { Readable: Readable2 } = require_readable();
var { InvalidArgumentError, RequestAbortedError } = require_errors(); var { InvalidArgumentError, RequestAbortedError } = require_errors();
var util = require_util(); var util = require_util();
var { getResolveErrorBodyCallback } = require_util3(); var { getResolveErrorBodyCallback } = require_util3();
@@ -9606,7 +9606,7 @@ var require_api_request = __commonJS({
const parsedHeaders = responseHeaders === "raw" ? util.parseHeaders(rawHeaders) : headers; const parsedHeaders = responseHeaders === "raw" ? util.parseHeaders(rawHeaders) : headers;
const contentType = parsedHeaders["content-type"]; const contentType = parsedHeaders["content-type"];
const contentLength = parsedHeaders["content-length"]; const contentLength = parsedHeaders["content-length"];
const res = new Readable({ const res = new Readable2({
resume, resume,
abort, abort,
contentType, contentType,
@@ -9921,7 +9921,7 @@ var require_api_pipeline = __commonJS({
"node_modules/@actions/http-client/node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) { "node_modules/@actions/http-client/node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
"use strict"; "use strict";
var { var {
Readable, Readable: Readable2,
Duplex, Duplex,
PassThrough PassThrough
} = require("node:stream"); } = require("node:stream");
@@ -9935,7 +9935,7 @@ var require_api_pipeline = __commonJS({
var { addSignal, removeSignal } = require_abort_signal(); var { addSignal, removeSignal } = require_abort_signal();
var assert = require("node:assert"); var assert = require("node:assert");
var kResume = /* @__PURE__ */ Symbol("resume"); var kResume = /* @__PURE__ */ Symbol("resume");
var PipelineRequest = class extends Readable { var PipelineRequest = class extends Readable2 {
constructor() { constructor() {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = null; this[kResume] = null;
@@ -9952,7 +9952,7 @@ var require_api_pipeline = __commonJS({
callback(err); callback(err);
} }
}; };
var PipelineResponse = class extends Readable { var PipelineResponse = class extends Readable2 {
constructor(resume) { constructor(resume) {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = resume; this[kResume] = resume;
@@ -13277,7 +13277,7 @@ var require_fetch = __commonJS({
subresourceSet subresourceSet
} = require_constants3(); } = require_constants3();
var EE = require("node:events"); var EE = require("node:events");
var { Readable, pipeline, finished } = require("node:stream"); var { Readable: Readable2, pipeline, finished } = require("node:stream");
var { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require_util(); var { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require_util();
var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url(); var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url();
var { getGlobalDispatcher } = require_global2(); var { getGlobalDispatcher } = require_global2();
@@ -14178,7 +14178,7 @@ var require_fetch = __commonJS({
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
} }
location = headersList.get("location", true); location = headersList.get("location", true);
this.body = new Readable({ read: resume }); this.body = new Readable2({ read: resume });
const decoders = []; const decoders = [];
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
@@ -31244,7 +31244,7 @@ var require_readable2 = __commonJS({
"use strict"; "use strict";
var assert = require("node:assert"); var assert = require("node:assert");
var { addAbortListener } = require("node:events"); var { addAbortListener } = require("node:events");
var { Readable } = require("node:stream"); var { Readable: Readable2 } = require("node:stream");
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors2(); var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors2();
var util = require_util9(); var util = require_util9();
var { ReadableStreamFrom } = require_util9(); var { ReadableStreamFrom } = require_util9();
@@ -31258,7 +31258,7 @@ var require_readable2 = __commonJS({
var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead"); var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead");
var noop = () => { var noop = () => {
}; };
var BodyReadable = class extends Readable { var BodyReadable = class extends Readable2 {
/** /**
* @param {object} opts * @param {object} opts
* @param {(this: Readable, size: number) => void} opts.resume * @param {(this: Readable, size: number) => void} opts.resume
@@ -31647,7 +31647,7 @@ var require_api_request2 = __commonJS({
"use strict"; "use strict";
var assert = require("node:assert"); var assert = require("node:assert");
var { AsyncResource } = require("node:async_hooks"); var { AsyncResource } = require("node:async_hooks");
var { Readable } = require_readable2(); var { Readable: Readable2 } = require_readable2();
var { InvalidArgumentError, RequestAbortedError } = require_errors2(); var { InvalidArgumentError, RequestAbortedError } = require_errors2();
var util = require_util9(); var util = require_util9();
function noop() { function noop() {
@@ -31731,7 +31731,7 @@ var require_api_request2 = __commonJS({
const parsedHeaders = headers; const parsedHeaders = headers;
const contentType = parsedHeaders?.["content-type"]; const contentType = parsedHeaders?.["content-type"];
const contentLength = parsedHeaders?.["content-length"]; const contentLength = parsedHeaders?.["content-length"];
const res = new Readable({ const res = new Readable2({
resume: () => controller.resume(), resume: () => controller.resume(),
abort: (reason) => controller.abort(reason), abort: (reason) => controller.abort(reason),
contentType, contentType,
@@ -32107,7 +32107,7 @@ var require_api_pipeline2 = __commonJS({
"node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) { "node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
"use strict"; "use strict";
var { var {
Readable, Readable: Readable2,
Duplex, Duplex,
PassThrough PassThrough
} = require("node:stream"); } = require("node:stream");
@@ -32124,7 +32124,7 @@ var require_api_pipeline2 = __commonJS({
function noop() { function noop() {
} }
var kResume = /* @__PURE__ */ Symbol("resume"); var kResume = /* @__PURE__ */ Symbol("resume");
var PipelineRequest = class extends Readable { var PipelineRequest = class extends Readable2 {
constructor() { constructor() {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = null; this[kResume] = null;
@@ -32142,7 +32142,7 @@ var require_api_pipeline2 = __commonJS({
callback(err); callback(err);
} }
}; };
var PipelineResponse = class extends Readable { var PipelineResponse = class extends Readable2 {
constructor(resume) { constructor(resume) {
super({ autoDestroy: true }); super({ autoDestroy: true });
this[kResume] = resume; this[kResume] = resume;
@@ -36759,7 +36759,7 @@ var require_cache3 = __commonJS({
"node_modules/undici/lib/interceptor/cache.js"(exports2, module2) { "node_modules/undici/lib/interceptor/cache.js"(exports2, module2) {
"use strict"; "use strict";
var assert = require("node:assert"); var assert = require("node:assert");
var { Readable } = require("node:stream"); var { Readable: Readable2 } = require("node:stream");
var util = require_util9(); var util = require_util9();
var CacheHandler = require_cache_handler(); var CacheHandler = require_cache_handler();
var MemoryCacheStore = require_memory_cache_store(); var MemoryCacheStore = require_memory_cache_store();
@@ -36855,7 +36855,7 @@ var require_cache3 = __commonJS({
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)); return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler));
} }
function sendCachedValue(handler, opts, result, age, context, isStale2) { function sendCachedValue(handler, opts, result, age, context, isStale2) {
const stream = util.isStream(result.body) ? result.body : Readable.from(result.body ?? []); const stream = util.isStream(result.body) ? result.body : Readable2.from(result.body ?? []);
assert(!stream.destroyed, "stream should not be destroyed"); assert(!stream.destroyed, "stream should not be destroyed");
assert(!stream.readableDidRead, "stream should not be readableDidRead"); assert(!stream.readableDidRead, "stream should not be readableDidRead");
const controller = { const controller = {
@@ -39978,7 +39978,7 @@ var require_fetch2 = __commonJS({
subresourceSet subresourceSet
} = require_constants8(); } = require_constants8();
var EE = require("node:events"); var EE = require("node:events");
var { Readable, pipeline, finished, isErrored, isReadable } = require("node:stream"); var { Readable: Readable2, pipeline, finished, isErrored, isReadable } = require("node:stream");
var { addAbortListener, bufferToLowerCasedHeaderName } = require_util9(); var { addAbortListener, bufferToLowerCasedHeaderName } = require_util9();
var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2(); var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2();
var { getGlobalDispatcher } = require_global4(); var { getGlobalDispatcher } = require_global4();
@@ -40940,7 +40940,7 @@ var require_fetch2 = __commonJS({
const headersList = new HeadersList(); const headersList = new HeadersList();
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders); appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders);
const location = headersList.get("location", true); const location = headersList.get("location", true);
this.body = new Readable({ read: () => controller.resume() }); this.body = new Readable2({ read: () => controller.resume() });
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
const decoders = []; const decoders = [];
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
@@ -50701,6 +50701,10 @@ async function updateChecksums(filePath, checksumEntries) {
await import_node_fs.promises.writeFile(filePath, content); await import_node_fs.promises.writeFile(filePath, content);
} }
// src/download/manifest.ts
var import_node_readline = require("node:readline");
var import_node_stream = require("node:stream");
// src/utils/constants.ts // src/utils/constants.ts
var VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson"; var VERSIONS_MANIFEST_URL = "https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";
@@ -50745,21 +50749,15 @@ function info2(msg) {
// src/download/manifest.ts // src/download/manifest.ts
var cachedManifestData = /* @__PURE__ */ new Map(); var cachedManifestData = /* @__PURE__ */ new Map();
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) { async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
const cachedVersions = cachedManifestData.get(manifestUrl); const cachedManifest = cachedManifestData.get(manifestUrl);
if (cachedVersions !== void 0) { if (cachedManifest?.complete === true) {
debug(`Using cached manifest data from ${manifestUrl}`); debug(`Using cached manifest data from ${manifestUrl}`);
return cachedVersions; return cachedManifest.versions;
}
info2(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
);
} }
const response = await fetchManifestResponse(manifestUrl);
const body = await response.text(); const body = await response.text();
const versions = parseManifest(body, manifestUrl); const versions = parseManifest(body, manifestUrl);
cachedManifestData.set(manifestUrl, versions); cachedManifestData.set(manifestUrl, { complete: true, versions });
return versions; return versions;
} }
function parseManifest(data, sourceDescription) { function parseManifest(data, sourceDescription) {
@@ -50767,31 +50765,14 @@ function parseManifest(data, sourceDescription) {
if (trimmed === "") { if (trimmed === "") {
throw new Error(`Manifest at ${sourceDescription} is empty.`); throw new Error(`Manifest at ${sourceDescription} is empty.`);
} }
if (trimmed.startsWith("[")) { rejectLegacyManifest(trimmed, sourceDescription);
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
);
}
const versions = []; const versions = [];
for (const [index, line] of data.split("\n").entries()) { for (const [index, line] of data.split("\n").entries()) {
const record = line.trim(); const record = line.trim();
if (record === "") { if (record === "") {
continue; continue;
} }
let parsed; versions.push(parseManifestRecord(record, sourceDescription, index + 1));
try {
parsed = JSON.parse(record);
} catch (error2) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${error2.message}`
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`
);
}
versions.push(parsed);
} }
if (versions.length === 0) { if (versions.length === 0) {
throw new Error(`No manifest data found in ${sourceDescription}.`); throw new Error(`No manifest data found in ${sourceDescription}.`);
@@ -50799,13 +50780,104 @@ function parseManifest(data, sourceDescription) {
return versions; return versions;
} }
async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) { async function getLatestVersion(manifestUrl = VERSIONS_MANIFEST_URL) {
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version; const latestVersion = manifestUrl === VERSIONS_MANIFEST_URL ? (await findManifestVersion(() => true))?.version : (await fetchManifest(manifestUrl))[0]?.version;
if (latestVersion === void 0) { if (latestVersion === void 0) {
throw new Error("No versions found in manifest data"); throw new Error("No versions found in manifest data");
} }
debug(`Latest version from manifest: ${latestVersion}`); debug(`Latest version from manifest: ${latestVersion}`);
return latestVersion; return latestVersion;
} }
async function fetchManifestResponse(manifestUrl) {
info2(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`
);
}
return response;
}
async function findManifestVersion(predicate) {
const cachedManifest = cachedManifestData.get(VERSIONS_MANIFEST_URL);
const cachedVersion = cachedManifest?.versions.find(predicate);
if (cachedVersion !== void 0 || cachedManifest?.complete === true) {
return cachedVersion;
}
const response = await fetchManifestResponse(VERSIONS_MANIFEST_URL);
if (response.body === null) {
const versions2 = parseManifest(
await response.text(),
VERSIONS_MANIFEST_URL
);
cachedManifestData.set(VERSIONS_MANIFEST_URL, {
complete: true,
versions: versions2
});
return versions2.find(predicate);
}
const input = import_node_stream.Readable.fromWeb(response.body);
const lines = (0, import_node_readline.createInterface)({ crlfDelay: Number.POSITIVE_INFINITY, input });
const versions = [];
let complete = false;
let lineNumber = 0;
let matchedVersion;
try {
for await (const line of lines) {
lineNumber += 1;
const record = line.trim();
if (record === "") {
continue;
}
if (versions.length === 0) {
rejectLegacyManifest(record, VERSIONS_MANIFEST_URL);
}
const versionData = parseManifestRecord(
record,
VERSIONS_MANIFEST_URL,
lineNumber
);
versions.push(versionData);
if (predicate(versionData)) {
matchedVersion = versionData;
break;
}
}
complete = matchedVersion === void 0;
} finally {
lines.close();
if (!complete) {
input.destroy();
}
}
if (versions.length === 0) {
throw new Error(`Manifest at ${VERSIONS_MANIFEST_URL} is empty.`);
}
cachedManifestData.set(VERSIONS_MANIFEST_URL, { complete, versions });
return matchedVersion;
}
function parseManifestRecord(record, sourceDescription, lineNumber) {
let parsed;
try {
parsed = JSON.parse(record);
} catch (error2) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${lineNumber}: ${error2.message}`
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${lineNumber}.`
);
}
return parsed;
}
function rejectLegacyManifest(data, sourceDescription) {
if (data.startsWith("[")) {
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
);
}
}
function isManifestVersion(value) { function isManifestVersion(value) {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
+147 -39
View File
@@ -1,3 +1,5 @@
import { createInterface } from "node:readline";
import { Readable } from "node:stream";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { VERSIONS_MANIFEST_URL } from "../utils/constants"; import { VERSIONS_MANIFEST_URL } from "../utils/constants";
import { fetch } from "../utils/fetch"; import { fetch } from "../utils/fetch";
@@ -23,28 +25,26 @@ export interface ArtifactResult {
downloadUrl: string; downloadUrl: string;
} }
const cachedManifestData = new Map<string, ManifestVersion[]>(); interface CachedManifest {
complete: boolean;
versions: ManifestVersion[];
}
const cachedManifestData = new Map<string, CachedManifest>();
export async function fetchManifest( export async function fetchManifest(
manifestUrl: string = VERSIONS_MANIFEST_URL, manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ManifestVersion[]> { ): Promise<ManifestVersion[]> {
const cachedVersions = cachedManifestData.get(manifestUrl); const cachedManifest = cachedManifestData.get(manifestUrl);
if (cachedVersions !== undefined) { if (cachedManifest?.complete === true) {
core.debug(`Using cached manifest data from ${manifestUrl}`); core.debug(`Using cached manifest data from ${manifestUrl}`);
return cachedVersions; return cachedManifest.versions;
}
log.info(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
);
} }
const response = await fetchManifestResponse(manifestUrl);
const body = await response.text(); const body = await response.text();
const versions = parseManifest(body, manifestUrl); const versions = parseManifest(body, manifestUrl);
cachedManifestData.set(manifestUrl, versions); cachedManifestData.set(manifestUrl, { complete: true, versions });
return versions; return versions;
} }
@@ -57,11 +57,7 @@ export function parseManifest(
throw new Error(`Manifest at ${sourceDescription} is empty.`); throw new Error(`Manifest at ${sourceDescription} is empty.`);
} }
if (trimmed.startsWith("[")) { rejectLegacyManifest(trimmed, sourceDescription);
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
);
}
const versions: ManifestVersion[] = []; const versions: ManifestVersion[] = [];
@@ -71,22 +67,7 @@ export function parseManifest(
continue; continue;
} }
let parsed: unknown; versions.push(parseManifestRecord(record, sourceDescription, index + 1));
try {
parsed = JSON.parse(record);
} catch (error) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${index + 1}.`,
);
}
versions.push(parsed);
} }
if (versions.length === 0) { if (versions.length === 0) {
@@ -99,7 +80,10 @@ export function parseManifest(
export async function getLatestVersion( export async function getLatestVersion(
manifestUrl: string = VERSIONS_MANIFEST_URL, manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string> { ): Promise<string> {
const latestVersion = (await fetchManifest(manifestUrl))[0]?.version; const latestVersion =
manifestUrl === VERSIONS_MANIFEST_URL
? (await findManifestVersion(() => true))?.version
: (await fetchManifest(manifestUrl))[0]?.version;
if (latestVersion === undefined) { if (latestVersion === undefined) {
throw new Error("No versions found in manifest data"); throw new Error("No versions found in manifest data");
@@ -109,6 +93,16 @@ export async function getLatestVersion(
return latestVersion; return latestVersion;
} }
// The default manifest is guaranteed to be ordered newest-first:
// https://github.com/astral-sh/versions#format
export async function getFirstMatchingVersion(
predicate: (version: string) => boolean,
): Promise<string | undefined> {
return (
await findManifestVersion((versionData) => predicate(versionData.version))
)?.version;
}
export async function getAllVersions( export async function getAllVersions(
manifestUrl: string = VERSIONS_MANIFEST_URL, manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<string[]> { ): Promise<string[]> {
@@ -125,10 +119,12 @@ export async function getArtifact(
platform: string, platform: string,
manifestUrl: string = VERSIONS_MANIFEST_URL, manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ArtifactResult | undefined> { ): Promise<ArtifactResult | undefined> {
const versions = await fetchManifest(manifestUrl); const versionData =
const versionData = versions.find( manifestUrl === VERSIONS_MANIFEST_URL
(candidate) => candidate.version === version, ? await findManifestVersion((candidate) => candidate.version === version)
); : (await fetchManifest(manifestUrl)).find(
(candidate) => candidate.version === version,
);
if (!versionData) { if (!versionData) {
core.debug(`Version ${version} not found in manifest ${manifestUrl}`); core.debug(`Version ${version} not found in manifest ${manifestUrl}`);
return undefined; return undefined;
@@ -169,6 +165,87 @@ export function clearManifestCache(manifestUrl?: string): void {
cachedManifestData.delete(manifestUrl); cachedManifestData.delete(manifestUrl);
} }
async function fetchManifestResponse(manifestUrl: string) {
log.info(`Fetching manifest data from ${manifestUrl} ...`);
const response = await fetch(manifestUrl, {});
if (!response.ok) {
throw new Error(
`Failed to fetch manifest data: ${response.status} ${response.statusText}`,
);
}
return response;
}
async function findManifestVersion(
predicate: (versionData: ManifestVersion) => boolean,
): Promise<ManifestVersion | undefined> {
const cachedManifest = cachedManifestData.get(VERSIONS_MANIFEST_URL);
const cachedVersion = cachedManifest?.versions.find(predicate);
if (cachedVersion !== undefined || cachedManifest?.complete === true) {
return cachedVersion;
}
const response = await fetchManifestResponse(VERSIONS_MANIFEST_URL);
if (response.body === null) {
const versions = parseManifest(
await response.text(),
VERSIONS_MANIFEST_URL,
);
cachedManifestData.set(VERSIONS_MANIFEST_URL, {
complete: true,
versions,
});
return versions.find(predicate);
}
const input = Readable.fromWeb(response.body);
const lines = createInterface({ crlfDelay: Number.POSITIVE_INFINITY, input });
const versions: ManifestVersion[] = [];
let complete = false;
let lineNumber = 0;
let matchedVersion: ManifestVersion | undefined;
try {
for await (const line of lines) {
lineNumber += 1;
const record = line.trim();
if (record === "") {
continue;
}
if (versions.length === 0) {
rejectLegacyManifest(record, VERSIONS_MANIFEST_URL);
}
const versionData = parseManifestRecord(
record,
VERSIONS_MANIFEST_URL,
lineNumber,
);
versions.push(versionData);
if (predicate(versionData)) {
matchedVersion = versionData;
break;
}
}
complete = matchedVersion === undefined;
} finally {
lines.close();
if (!complete) {
input.destroy();
}
}
if (versions.length === 0) {
throw new Error(`Manifest at ${VERSIONS_MANIFEST_URL} is empty.`);
}
cachedManifestData.set(VERSIONS_MANIFEST_URL, { complete, versions });
return matchedVersion;
}
function manifestSource(manifestUrl: string): string { function manifestSource(manifestUrl: string): string {
if (manifestUrl === VERSIONS_MANIFEST_URL) { if (manifestUrl === VERSIONS_MANIFEST_URL) {
return VERSIONS_MANIFEST_URL; return VERSIONS_MANIFEST_URL;
@@ -177,6 +254,37 @@ function manifestSource(manifestUrl: string): string {
return `manifest-file ${manifestUrl}`; return `manifest-file ${manifestUrl}`;
} }
function parseManifestRecord(
record: string,
sourceDescription: string,
lineNumber: number,
): ManifestVersion {
let parsed: unknown;
try {
parsed = JSON.parse(record);
} catch (error) {
throw new Error(
`Failed to parse manifest data from ${sourceDescription} at line ${lineNumber}: ${(error as Error).message}`,
);
}
if (!isManifestVersion(parsed)) {
throw new Error(
`Invalid manifest record in ${sourceDescription} at line ${lineNumber}.`,
);
}
return parsed;
}
function rejectLegacyManifest(data: string, sourceDescription: string): void {
if (data.startsWith("[")) {
throw new Error(
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
);
}
}
function isManifestVersion(value: unknown): value is ManifestVersion { function isManifestVersion(value: unknown): value is ManifestVersion {
if (!isRecord(value)) { if (!isRecord(value)) {
return false; return false;
+55 -8
View File
@@ -2,7 +2,12 @@ import * as core from "@actions/core";
import * as tc from "@actions/tool-cache"; import * as tc from "@actions/tool-cache";
import * as pep440 from "@renovatebot/pep440"; import * as pep440 from "@renovatebot/pep440";
import * as semver from "semver"; import * as semver from "semver";
import { getAllVersions, getLatestVersion } from "../download/manifest"; import {
getAllVersions,
getFirstMatchingVersion,
getLatestVersion,
} from "../download/manifest";
import { VERSIONS_MANIFEST_URL } from "../utils/constants";
import type { ResolutionStrategy } from "../utils/inputs"; import type { ResolutionStrategy } from "../utils/inputs";
import * as log from "../utils/logging"; import * as log from "../utils/logging";
import { import {
@@ -82,13 +87,26 @@ class RangeVersionResolver implements ConcreteVersionResolver {
return undefined; return undefined;
} }
const availableVersions = await getAllVersions(context.manifestUrl); let resolvedVersion: string | undefined;
core.debug(`Available versions: ${availableVersions}`); if (
context.resolutionStrategy === "highest" &&
const resolvedVersion = (context.manifestUrl === undefined ||
context.resolutionStrategy === "lowest" context.manifestUrl === VERSIONS_MANIFEST_URL)
? minSatisfying(availableVersions, context.parsedSpecifier.normalized) ) {
: maxSatisfying(availableVersions, context.parsedSpecifier.normalized); resolvedVersion = await findHighestSatisfyingVersion(
context.parsedSpecifier.normalized,
);
} else {
const availableVersions = await getAllVersions(context.manifestUrl);
core.debug(`Available versions: ${availableVersions}`);
resolvedVersion =
context.resolutionStrategy === "lowest"
? minSatisfying(availableVersions, context.parsedSpecifier.normalized)
: maxSatisfying(
availableVersions,
context.parsedSpecifier.normalized,
);
}
if (resolvedVersion === undefined) { if (resolvedVersion === undefined) {
throw new Error(`No version found for ${context.parsedSpecifier.raw}`); throw new Error(`No version found for ${context.parsedSpecifier.raw}`);
@@ -141,6 +159,35 @@ export async function resolveVersion(
throw new Error(`No version found for ${versionInput}`); throw new Error(`No version found for ${versionInput}`);
} }
async function findHighestSatisfyingVersion(
versionSpecifier: string,
): Promise<string | undefined> {
// This fast path assumes uv releases are semver-monotonic: newer manifest
// entries always have higher versions, with no lower-version backports.
const semverRange = semver.validRange(versionSpecifier);
if (semverRange !== null) {
const semverMatch = await getFirstMatchingVersion((version) =>
semver.satisfies(version, semverRange),
);
if (semverMatch !== undefined) {
core.debug(
`Found a version that satisfies the semver range: ${semverMatch}`,
);
return semverMatch;
}
}
const pep440Match = await getFirstMatchingVersion((version) =>
pep440.satisfies(version, versionSpecifier),
);
if (pep440Match !== undefined) {
core.debug(
`Found a version that satisfies the pep440 specifier: ${pep440Match}`,
);
}
return pep440Match;
}
function maxSatisfying( function maxSatisfying(
versions: string[], versions: string[],
version: string, version: string,