mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-07-22 20:20:23 +00:00
Speed up version client by partial response reads (#807)
This commit is contained in:
committed by
GitHub
parent
47a7f4fb2e
commit
2269552d54
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
||||
import * as semver from "semver";
|
||||
import { VERSIONS_MANIFEST_URL } from "../../src/utils/constants";
|
||||
|
||||
const mockInfo = 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.
|
||||
const mockGetAllVersions = jest.fn<any>();
|
||||
// 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>();
|
||||
|
||||
jest.unstable_mockModule("../../src/download/manifest", () => ({
|
||||
getAllVersions: mockGetAllVersions,
|
||||
getArtifact: mockGetArtifact,
|
||||
getFirstMatchingVersion: mockGetFirstMatchingVersion,
|
||||
getLatestVersion: mockGetLatestVersion,
|
||||
}));
|
||||
|
||||
@@ -65,6 +69,7 @@ describe("download-version", () => {
|
||||
mockCacheDir.mockReset();
|
||||
mockGetLatestVersion.mockReset();
|
||||
mockGetAllVersions.mockReset();
|
||||
mockGetFirstMatchingVersion.mockReset();
|
||||
mockGetArtifact.mockReset();
|
||||
mockValidateChecksum.mockReset();
|
||||
|
||||
@@ -85,14 +90,43 @@ describe("download-version", () => {
|
||||
expect(mockGetLatestVersion).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("uses the default manifest to resolve available versions", async () => {
|
||||
mockGetAllVersions.mockResolvedValue(["0.9.26", "0.9.25"]);
|
||||
it("stops at the first matching version in the default manifest", async () => {
|
||||
mockGetFirstMatchingVersion.mockImplementation(
|
||||
(predicate: (version: string) => boolean) =>
|
||||
["0.9.26", "0.9.25"].find(predicate),
|
||||
);
|
||||
|
||||
const version = await resolveVersion("^0.9.0", undefined);
|
||||
|
||||
expect(version).toBe("0.9.26");
|
||||
expect(mockGetAllVersions).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetAllVersions).toHaveBeenCalledWith(undefined);
|
||||
expect(mockGetFirstMatchingVersion).toHaveBeenCalledTimes(1);
|
||||
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 () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
fetchManifest,
|
||||
getAllVersions,
|
||||
getArtifact,
|
||||
getFirstMatchingVersion,
|
||||
getLatestVersion,
|
||||
parseManifest,
|
||||
} = await import("../../src/download/manifest");
|
||||
@@ -33,6 +34,7 @@ function createMockResponse(
|
||||
data: string,
|
||||
) {
|
||||
return {
|
||||
body: null,
|
||||
ok,
|
||||
status,
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
clearManifestCache();
|
||||
@@ -105,6 +132,75 @@ describe("manifest", () => {
|
||||
getLatestVersion("https://example.com/custom.ndjson"),
|
||||
).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", () => {
|
||||
@@ -168,6 +264,60 @@ describe("manifest", () => {
|
||||
|
||||
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", () => {
|
||||
|
||||
+206
-99
@@ -9161,7 +9161,7 @@ var require_readable = __commonJS({
|
||||
"node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) {
|
||||
"use strict";
|
||||
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 util7 = require_util();
|
||||
var { ReadableStreamFrom } = require_util();
|
||||
@@ -9173,7 +9173,7 @@ var require_readable = __commonJS({
|
||||
var kContentLength = /* @__PURE__ */ Symbol("kContentLength");
|
||||
var noop = () => {
|
||||
};
|
||||
var BodyReadable = class extends Readable5 {
|
||||
var BodyReadable = class extends Readable6 {
|
||||
constructor({
|
||||
resume,
|
||||
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) {
|
||||
"use strict";
|
||||
var assert4 = require("node:assert");
|
||||
var { Readable: Readable5 } = require_readable();
|
||||
var { Readable: Readable6 } = require_readable();
|
||||
var { InvalidArgumentError, RequestAbortedError } = require_errors();
|
||||
var util7 = require_util();
|
||||
var { getResolveErrorBodyCallback } = require_util3();
|
||||
@@ -9610,7 +9610,7 @@ var require_api_request = __commonJS({
|
||||
const parsedHeaders = responseHeaders === "raw" ? util7.parseHeaders(rawHeaders) : headers;
|
||||
const contentType2 = parsedHeaders["content-type"];
|
||||
const contentLength2 = parsedHeaders["content-length"];
|
||||
const res = new Readable5({
|
||||
const res = new Readable6({
|
||||
resume,
|
||||
abort,
|
||||
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) {
|
||||
"use strict";
|
||||
var {
|
||||
Readable: Readable5,
|
||||
Readable: Readable6,
|
||||
Duplex,
|
||||
PassThrough
|
||||
} = require("node:stream");
|
||||
@@ -9939,7 +9939,7 @@ var require_api_pipeline = __commonJS({
|
||||
var { addSignal, removeSignal } = require_abort_signal();
|
||||
var assert4 = require("node:assert");
|
||||
var kResume = /* @__PURE__ */ Symbol("resume");
|
||||
var PipelineRequest = class extends Readable5 {
|
||||
var PipelineRequest = class extends Readable6 {
|
||||
constructor() {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = null;
|
||||
@@ -9956,7 +9956,7 @@ var require_api_pipeline = __commonJS({
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
var PipelineResponse = class extends Readable5 {
|
||||
var PipelineResponse = class extends Readable6 {
|
||||
constructor(resume) {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = resume;
|
||||
@@ -13281,7 +13281,7 @@ var require_fetch = __commonJS({
|
||||
subresourceSet
|
||||
} = require_constants3();
|
||||
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 { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url();
|
||||
var { getGlobalDispatcher } = require_global2();
|
||||
@@ -14182,7 +14182,7 @@ var require_fetch = __commonJS({
|
||||
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
|
||||
}
|
||||
location = headersList.get("location", true);
|
||||
this.body = new Readable5({ read: resume });
|
||||
this.body = new Readable6({ read: resume });
|
||||
const decoders = [];
|
||||
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
|
||||
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) {
|
||||
"use strict";
|
||||
var Range = require_range();
|
||||
var satisfies4 = (version3, range2, options) => {
|
||||
var satisfies5 = (version3, range2, options) => {
|
||||
try {
|
||||
range2 = new Range(range2, options);
|
||||
} catch (er) {
|
||||
@@ -20750,7 +20750,7 @@ var require_satisfies = __commonJS({
|
||||
}
|
||||
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) {
|
||||
"use strict";
|
||||
var Range = require_range();
|
||||
var validRange2 = (range2, options) => {
|
||||
var validRange3 = (range2, options) => {
|
||||
try {
|
||||
return new Range(range2, options).range || "*";
|
||||
} catch (er) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
module2.exports = validRange2;
|
||||
module2.exports = validRange3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20905,7 +20905,7 @@ var require_outside = __commonJS({
|
||||
var Comparator = require_comparator();
|
||||
var { ANY } = Comparator;
|
||||
var Range = require_range();
|
||||
var satisfies4 = require_satisfies();
|
||||
var satisfies5 = require_satisfies();
|
||||
var gt3 = require_gt();
|
||||
var lt2 = require_lt();
|
||||
var lte = require_lte();
|
||||
@@ -20932,7 +20932,7 @@ var require_outside = __commonJS({
|
||||
default:
|
||||
throw new TypeError('Must provide a hilo val of "<" or ">"');
|
||||
}
|
||||
if (satisfies4(version3, range2, options)) {
|
||||
if (satisfies5(version3, range2, options)) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < range2.set.length; ++i) {
|
||||
@@ -21004,7 +21004,7 @@ var require_intersects = __commonJS({
|
||||
var require_simplify = __commonJS({
|
||||
"node_modules/@actions/cache/node_modules/semver/ranges/simplify.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var satisfies4 = require_satisfies();
|
||||
var satisfies5 = require_satisfies();
|
||||
var compare2 = require_compare();
|
||||
module2.exports = (versions, range2, options) => {
|
||||
const set = [];
|
||||
@@ -21012,7 +21012,7 @@ var require_simplify = __commonJS({
|
||||
let prev = null;
|
||||
const v = versions.sort((a, b) => compare2(a, b, options));
|
||||
for (const version3 of v) {
|
||||
const included = satisfies4(version3, range2, options);
|
||||
const included = satisfies5(version3, range2, options);
|
||||
if (included) {
|
||||
prev = version3;
|
||||
if (!first) {
|
||||
@@ -21057,7 +21057,7 @@ var require_subset = __commonJS({
|
||||
var Range = require_range();
|
||||
var Comparator = require_comparator();
|
||||
var { ANY } = Comparator;
|
||||
var satisfies4 = require_satisfies();
|
||||
var satisfies5 = require_satisfies();
|
||||
var compare2 = require_compare();
|
||||
var subset = (sub, dom, options = {}) => {
|
||||
if (sub === dom) {
|
||||
@@ -21126,14 +21126,14 @@ var require_subset = __commonJS({
|
||||
}
|
||||
}
|
||||
for (const eq2 of eqSet) {
|
||||
if (gt3 && !satisfies4(eq2, String(gt3), options)) {
|
||||
if (gt3 && !satisfies5(eq2, String(gt3), options)) {
|
||||
return null;
|
||||
}
|
||||
if (lt2 && !satisfies4(eq2, String(lt2), options)) {
|
||||
if (lt2 && !satisfies5(eq2, String(lt2), options)) {
|
||||
return null;
|
||||
}
|
||||
for (const c of dom) {
|
||||
if (!satisfies4(eq2, String(c), options)) {
|
||||
if (!satisfies5(eq2, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21160,7 +21160,7 @@ var require_subset = __commonJS({
|
||||
if (higher === c && higher !== gt3) {
|
||||
return false;
|
||||
}
|
||||
} else if (gt3.operator === ">=" && !satisfies4(gt3.semver, String(c), options)) {
|
||||
} else if (gt3.operator === ">=" && !satisfies5(gt3.semver, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21175,7 +21175,7 @@ var require_subset = __commonJS({
|
||||
if (lower === c && lower !== lt2) {
|
||||
return false;
|
||||
}
|
||||
} else if (lt2.operator === "<=" && !satisfies4(lt2.semver, String(c), options)) {
|
||||
} else if (lt2.operator === "<=" && !satisfies5(lt2.semver, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -21245,12 +21245,12 @@ var require_semver2 = __commonJS({
|
||||
var coerce = require_coerce();
|
||||
var Comparator = require_comparator();
|
||||
var Range = require_range();
|
||||
var satisfies4 = require_satisfies();
|
||||
var satisfies5 = require_satisfies();
|
||||
var toComparators = require_to_comparators();
|
||||
var maxSatisfying3 = require_max_satisfying();
|
||||
var minSatisfying4 = require_min_satisfying();
|
||||
var minVersion = require_min_version();
|
||||
var validRange2 = require_valid2();
|
||||
var validRange3 = require_valid2();
|
||||
var outside = require_outside();
|
||||
var gtr = require_gtr();
|
||||
var ltr = require_ltr();
|
||||
@@ -21283,12 +21283,12 @@ var require_semver2 = __commonJS({
|
||||
coerce,
|
||||
Comparator,
|
||||
Range,
|
||||
satisfies: satisfies4,
|
||||
satisfies: satisfies5,
|
||||
toComparators,
|
||||
maxSatisfying: maxSatisfying3,
|
||||
minSatisfying: minSatisfying4,
|
||||
minVersion,
|
||||
validRange: validRange2,
|
||||
validRange: validRange3,
|
||||
outside,
|
||||
gtr,
|
||||
ltr,
|
||||
@@ -28352,7 +28352,7 @@ var require_satisfies2 = __commonJS({
|
||||
"node_modules/@actions/tool-cache/node_modules/semver/functions/satisfies.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var Range = require_range2();
|
||||
var satisfies4 = (version3, range2, options) => {
|
||||
var satisfies5 = (version3, range2, options) => {
|
||||
try {
|
||||
range2 = new Range(range2, options);
|
||||
} catch (er) {
|
||||
@@ -28360,7 +28360,7 @@ var require_satisfies2 = __commonJS({
|
||||
}
|
||||
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) {
|
||||
"use strict";
|
||||
var Range = require_range2();
|
||||
var validRange2 = (range2, options) => {
|
||||
var validRange3 = (range2, options) => {
|
||||
try {
|
||||
return new Range(range2, options).range || "*";
|
||||
} catch (er) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
module2.exports = validRange2;
|
||||
module2.exports = validRange3;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28515,7 +28515,7 @@ var require_outside2 = __commonJS({
|
||||
var Comparator = require_comparator2();
|
||||
var { ANY } = Comparator;
|
||||
var Range = require_range2();
|
||||
var satisfies4 = require_satisfies2();
|
||||
var satisfies5 = require_satisfies2();
|
||||
var gt3 = require_gt2();
|
||||
var lt2 = require_lt2();
|
||||
var lte = require_lte2();
|
||||
@@ -28542,7 +28542,7 @@ var require_outside2 = __commonJS({
|
||||
default:
|
||||
throw new TypeError('Must provide a hilo val of "<" or ">"');
|
||||
}
|
||||
if (satisfies4(version3, range2, options)) {
|
||||
if (satisfies5(version3, range2, options)) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < range2.set.length; ++i) {
|
||||
@@ -28614,7 +28614,7 @@ var require_intersects2 = __commonJS({
|
||||
var require_simplify2 = __commonJS({
|
||||
"node_modules/@actions/tool-cache/node_modules/semver/ranges/simplify.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var satisfies4 = require_satisfies2();
|
||||
var satisfies5 = require_satisfies2();
|
||||
var compare2 = require_compare2();
|
||||
module2.exports = (versions, range2, options) => {
|
||||
const set = [];
|
||||
@@ -28622,7 +28622,7 @@ var require_simplify2 = __commonJS({
|
||||
let prev = null;
|
||||
const v = versions.sort((a, b) => compare2(a, b, options));
|
||||
for (const version3 of v) {
|
||||
const included = satisfies4(version3, range2, options);
|
||||
const included = satisfies5(version3, range2, options);
|
||||
if (included) {
|
||||
prev = version3;
|
||||
if (!first) {
|
||||
@@ -28667,7 +28667,7 @@ var require_subset2 = __commonJS({
|
||||
var Range = require_range2();
|
||||
var Comparator = require_comparator2();
|
||||
var { ANY } = Comparator;
|
||||
var satisfies4 = require_satisfies2();
|
||||
var satisfies5 = require_satisfies2();
|
||||
var compare2 = require_compare2();
|
||||
var subset = (sub, dom, options = {}) => {
|
||||
if (sub === dom) {
|
||||
@@ -28736,14 +28736,14 @@ var require_subset2 = __commonJS({
|
||||
}
|
||||
}
|
||||
for (const eq2 of eqSet) {
|
||||
if (gt3 && !satisfies4(eq2, String(gt3), options)) {
|
||||
if (gt3 && !satisfies5(eq2, String(gt3), options)) {
|
||||
return null;
|
||||
}
|
||||
if (lt2 && !satisfies4(eq2, String(lt2), options)) {
|
||||
if (lt2 && !satisfies5(eq2, String(lt2), options)) {
|
||||
return null;
|
||||
}
|
||||
for (const c of dom) {
|
||||
if (!satisfies4(eq2, String(c), options)) {
|
||||
if (!satisfies5(eq2, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -28770,7 +28770,7 @@ var require_subset2 = __commonJS({
|
||||
if (higher === c && higher !== gt3) {
|
||||
return false;
|
||||
}
|
||||
} else if (gt3.operator === ">=" && !satisfies4(gt3.semver, String(c), options)) {
|
||||
} else if (gt3.operator === ">=" && !satisfies5(gt3.semver, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -28785,7 +28785,7 @@ var require_subset2 = __commonJS({
|
||||
if (lower === c && lower !== lt2) {
|
||||
return false;
|
||||
}
|
||||
} else if (lt2.operator === "<=" && !satisfies4(lt2.semver, String(c), options)) {
|
||||
} else if (lt2.operator === "<=" && !satisfies5(lt2.semver, String(c), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -28855,12 +28855,12 @@ var require_semver4 = __commonJS({
|
||||
var coerce = require_coerce2();
|
||||
var Comparator = require_comparator2();
|
||||
var Range = require_range2();
|
||||
var satisfies4 = require_satisfies2();
|
||||
var satisfies5 = require_satisfies2();
|
||||
var toComparators = require_to_comparators2();
|
||||
var maxSatisfying3 = require_max_satisfying2();
|
||||
var minSatisfying4 = require_min_satisfying2();
|
||||
var minVersion = require_min_version2();
|
||||
var validRange2 = require_valid4();
|
||||
var validRange3 = require_valid4();
|
||||
var outside = require_outside2();
|
||||
var gtr = require_gtr2();
|
||||
var ltr = require_ltr2();
|
||||
@@ -28893,12 +28893,12 @@ var require_semver4 = __commonJS({
|
||||
coerce,
|
||||
Comparator,
|
||||
Range,
|
||||
satisfies: satisfies4,
|
||||
satisfies: satisfies5,
|
||||
toComparators,
|
||||
maxSatisfying: maxSatisfying3,
|
||||
minSatisfying: minSatisfying4,
|
||||
minVersion,
|
||||
validRange: validRange2,
|
||||
validRange: validRange3,
|
||||
outside,
|
||||
gtr,
|
||||
ltr,
|
||||
@@ -40422,7 +40422,7 @@ var require_readable2 = __commonJS({
|
||||
"use strict";
|
||||
var assert4 = require("node:assert");
|
||||
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 util7 = require_util9();
|
||||
var { ReadableStreamFrom } = require_util9();
|
||||
@@ -40436,7 +40436,7 @@ var require_readable2 = __commonJS({
|
||||
var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead");
|
||||
var noop = () => {
|
||||
};
|
||||
var BodyReadable = class extends Readable5 {
|
||||
var BodyReadable = class extends Readable6 {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {(this: Readable, size: number) => void} opts.resume
|
||||
@@ -40825,7 +40825,7 @@ var require_api_request2 = __commonJS({
|
||||
"use strict";
|
||||
var assert4 = require("node:assert");
|
||||
var { AsyncResource } = require("node:async_hooks");
|
||||
var { Readable: Readable5 } = require_readable2();
|
||||
var { Readable: Readable6 } = require_readable2();
|
||||
var { InvalidArgumentError, RequestAbortedError } = require_errors2();
|
||||
var util7 = require_util9();
|
||||
function noop() {
|
||||
@@ -40909,7 +40909,7 @@ var require_api_request2 = __commonJS({
|
||||
const parsedHeaders = headers;
|
||||
const contentType2 = parsedHeaders?.["content-type"];
|
||||
const contentLength2 = parsedHeaders?.["content-length"];
|
||||
const res = new Readable5({
|
||||
const res = new Readable6({
|
||||
resume: () => controller.resume(),
|
||||
abort: (reason) => controller.abort(reason),
|
||||
contentType: contentType2,
|
||||
@@ -41285,7 +41285,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
"node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var {
|
||||
Readable: Readable5,
|
||||
Readable: Readable6,
|
||||
Duplex,
|
||||
PassThrough
|
||||
} = require("node:stream");
|
||||
@@ -41302,7 +41302,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
function noop() {
|
||||
}
|
||||
var kResume = /* @__PURE__ */ Symbol("resume");
|
||||
var PipelineRequest = class extends Readable5 {
|
||||
var PipelineRequest = class extends Readable6 {
|
||||
constructor() {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = null;
|
||||
@@ -41320,7 +41320,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
var PipelineResponse = class extends Readable5 {
|
||||
var PipelineResponse = class extends Readable6 {
|
||||
constructor(resume) {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = resume;
|
||||
@@ -45937,7 +45937,7 @@ var require_cache3 = __commonJS({
|
||||
"node_modules/undici/lib/interceptor/cache.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var assert4 = require("node:assert");
|
||||
var { Readable: Readable5 } = require("node:stream");
|
||||
var { Readable: Readable6 } = require("node:stream");
|
||||
var util7 = require_util9();
|
||||
var CacheHandler = require_cache_handler();
|
||||
var MemoryCacheStore = require_memory_cache_store();
|
||||
@@ -46033,7 +46033,7 @@ var require_cache3 = __commonJS({
|
||||
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler));
|
||||
}
|
||||
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.readableDidRead, "stream should not be readableDidRead");
|
||||
const controller = {
|
||||
@@ -49156,7 +49156,7 @@ var require_fetch2 = __commonJS({
|
||||
subresourceSet
|
||||
} = require_constants10();
|
||||
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 { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2();
|
||||
var { getGlobalDispatcher } = require_global4();
|
||||
@@ -50118,7 +50118,7 @@ var require_fetch2 = __commonJS({
|
||||
const headersList = new HeadersList();
|
||||
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders);
|
||||
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 decoders = [];
|
||||
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
|
||||
@@ -55017,13 +55017,13 @@ var require_semver5 = __commonJS({
|
||||
return true;
|
||||
}
|
||||
rangeTmp = new Range(comp26.value, options);
|
||||
return satisfies4(this.value, rangeTmp, options);
|
||||
return satisfies5(this.value, rangeTmp, options);
|
||||
} else if (comp26.operator === "") {
|
||||
if (comp26.value === "") {
|
||||
return true;
|
||||
}
|
||||
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 sameDirectionDecreasing = (this.operator === "<=" || this.operator === "<") && (comp26.operator === "<=" || comp26.operator === "<");
|
||||
@@ -55350,8 +55350,8 @@ var require_semver5 = __commonJS({
|
||||
}
|
||||
return true;
|
||||
}
|
||||
exports2.satisfies = satisfies4;
|
||||
function satisfies4(version3, range2, options) {
|
||||
exports2.satisfies = satisfies5;
|
||||
function satisfies5(version3, range2, options) {
|
||||
try {
|
||||
range2 = new Range(range2, options);
|
||||
} catch (er) {
|
||||
@@ -55442,8 +55442,8 @@ var require_semver5 = __commonJS({
|
||||
}
|
||||
return null;
|
||||
}
|
||||
exports2.validRange = validRange2;
|
||||
function validRange2(range2, options) {
|
||||
exports2.validRange = validRange3;
|
||||
function validRange3(range2, options) {
|
||||
try {
|
||||
return new Range(range2, options).range || "*";
|
||||
} catch (er) {
|
||||
@@ -55481,7 +55481,7 @@ var require_semver5 = __commonJS({
|
||||
default:
|
||||
throw new TypeError('Must provide a hilo val of "<" or ">"');
|
||||
}
|
||||
if (satisfies4(version3, range2, options)) {
|
||||
if (satisfies5(version3, range2, options)) {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
var import_undici2 = __toESM(require_undici2(), 1);
|
||||
function getProxyAgent() {
|
||||
@@ -96169,21 +96173,15 @@ function formatVariants(entries) {
|
||||
// src/download/manifest.ts
|
||||
var cachedManifestData = /* @__PURE__ */ new Map();
|
||||
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||
const cachedVersions = cachedManifestData.get(manifestUrl);
|
||||
if (cachedVersions !== void 0) {
|
||||
const cachedManifest = cachedManifestData.get(manifestUrl);
|
||||
if (cachedManifest?.complete === true) {
|
||||
debug(`Using cached manifest data from ${manifestUrl}`);
|
||||
return cachedVersions;
|
||||
}
|
||||
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 cachedManifest.versions;
|
||||
}
|
||||
const response = await fetchManifestResponse(manifestUrl);
|
||||
const body2 = await response.text();
|
||||
const versions = parseManifest(body2, manifestUrl);
|
||||
cachedManifestData.set(manifestUrl, versions);
|
||||
cachedManifestData.set(manifestUrl, { complete: true, versions });
|
||||
return versions;
|
||||
}
|
||||
function parseManifest(data, sourceDescription) {
|
||||
@@ -96191,31 +96189,14 @@ function parseManifest(data, sourceDescription) {
|
||||
if (trimmed === "") {
|
||||
throw new Error(`Manifest at ${sourceDescription} is empty.`);
|
||||
}
|
||||
if (trimmed.startsWith("[")) {
|
||||
throw new Error(
|
||||
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
|
||||
);
|
||||
}
|
||||
rejectLegacyManifest(trimmed, sourceDescription);
|
||||
const versions = [];
|
||||
for (const [index, line] of data.split("\n").entries()) {
|
||||
const record = line.trim();
|
||||
if (record === "") {
|
||||
continue;
|
||||
}
|
||||
let parsed;
|
||||
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);
|
||||
versions.push(parseManifestRecord(record, sourceDescription, index + 1));
|
||||
}
|
||||
if (versions.length === 0) {
|
||||
throw new Error(`No manifest data found in ${sourceDescription}.`);
|
||||
@@ -96223,13 +96204,16 @@ function parseManifest(data, sourceDescription) {
|
||||
return versions;
|
||||
}
|
||||
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) {
|
||||
throw new Error("No versions found in manifest data");
|
||||
}
|
||||
debug(`Latest version from manifest: ${latestVersion}`);
|
||||
return latestVersion;
|
||||
}
|
||||
async function getFirstMatchingVersion(predicate) {
|
||||
return (await findManifestVersion((versionData) => predicate(versionData.version)))?.version;
|
||||
}
|
||||
async function getAllVersions(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||
info2(
|
||||
`Getting available versions from ${manifestSource(manifestUrl)} ...`
|
||||
@@ -96238,8 +96222,7 @@ async function getAllVersions(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||
return versions.map((versionData) => versionData.version);
|
||||
}
|
||||
async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||
const versions = await fetchManifest(manifestUrl);
|
||||
const versionData = versions.find(
|
||||
const versionData = manifestUrl === VERSIONS_MANIFEST_URL ? await findManifestVersion((candidate) => candidate.version === version3) : (await fetchManifest(manifestUrl)).find(
|
||||
(candidate) => candidate.version === version3
|
||||
);
|
||||
if (!versionData) {
|
||||
@@ -96266,12 +96249,103 @@ async function getArtifact(version3, arch3, platform2, manifestUrl = VERSIONS_MA
|
||||
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) {
|
||||
if (manifestUrl === VERSIONS_MANIFEST_URL) {
|
||||
return VERSIONS_MANIFEST_URL;
|
||||
}
|
||||
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) {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
@@ -97735,9 +97809,19 @@ var RangeVersionResolver = class {
|
||||
if (context3.parsedSpecifier.kind !== "range") {
|
||||
return void 0;
|
||||
}
|
||||
const availableVersions = await getAllVersions(context3.manifestUrl);
|
||||
debug(`Available versions: ${availableVersions}`);
|
||||
const resolvedVersion = context3.resolutionStrategy === "lowest" ? minSatisfying3(availableVersions, context3.parsedSpecifier.normalized) : maxSatisfying2(availableVersions, context3.parsedSpecifier.normalized);
|
||||
let resolvedVersion;
|
||||
if (context3.resolutionStrategy === "highest" && (context3.manifestUrl === void 0 || context3.manifestUrl === VERSIONS_MANIFEST_URL)) {
|
||||
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) {
|
||||
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}`);
|
||||
}
|
||||
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) {
|
||||
const maxSemver = evaluateVersions(versions, version3);
|
||||
if (maxSemver !== "") {
|
||||
|
||||
+123
-51
@@ -9157,7 +9157,7 @@ var require_readable = __commonJS({
|
||||
"node_modules/@actions/http-client/node_modules/undici/lib/api/readable.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var assert = require("node:assert");
|
||||
var { Readable } = require("node:stream");
|
||||
var { Readable: Readable2 } = require("node:stream");
|
||||
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors();
|
||||
var util = require_util();
|
||||
var { ReadableStreamFrom } = require_util();
|
||||
@@ -9169,7 +9169,7 @@ var require_readable = __commonJS({
|
||||
var kContentLength = /* @__PURE__ */ Symbol("kContentLength");
|
||||
var noop = () => {
|
||||
};
|
||||
var BodyReadable = class extends Readable {
|
||||
var BodyReadable = class extends Readable2 {
|
||||
constructor({
|
||||
resume,
|
||||
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) {
|
||||
"use strict";
|
||||
var assert = require("node:assert");
|
||||
var { Readable } = require_readable();
|
||||
var { Readable: Readable2 } = require_readable();
|
||||
var { InvalidArgumentError, RequestAbortedError } = require_errors();
|
||||
var util = require_util();
|
||||
var { getResolveErrorBodyCallback } = require_util3();
|
||||
@@ -9606,7 +9606,7 @@ var require_api_request = __commonJS({
|
||||
const parsedHeaders = responseHeaders === "raw" ? util.parseHeaders(rawHeaders) : headers;
|
||||
const contentType = parsedHeaders["content-type"];
|
||||
const contentLength = parsedHeaders["content-length"];
|
||||
const res = new Readable({
|
||||
const res = new Readable2({
|
||||
resume,
|
||||
abort,
|
||||
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) {
|
||||
"use strict";
|
||||
var {
|
||||
Readable,
|
||||
Readable: Readable2,
|
||||
Duplex,
|
||||
PassThrough
|
||||
} = require("node:stream");
|
||||
@@ -9935,7 +9935,7 @@ var require_api_pipeline = __commonJS({
|
||||
var { addSignal, removeSignal } = require_abort_signal();
|
||||
var assert = require("node:assert");
|
||||
var kResume = /* @__PURE__ */ Symbol("resume");
|
||||
var PipelineRequest = class extends Readable {
|
||||
var PipelineRequest = class extends Readable2 {
|
||||
constructor() {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = null;
|
||||
@@ -9952,7 +9952,7 @@ var require_api_pipeline = __commonJS({
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
var PipelineResponse = class extends Readable {
|
||||
var PipelineResponse = class extends Readable2 {
|
||||
constructor(resume) {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = resume;
|
||||
@@ -13277,7 +13277,7 @@ var require_fetch = __commonJS({
|
||||
subresourceSet
|
||||
} = require_constants3();
|
||||
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 { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url();
|
||||
var { getGlobalDispatcher } = require_global2();
|
||||
@@ -14178,7 +14178,7 @@ var require_fetch = __commonJS({
|
||||
headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true);
|
||||
}
|
||||
location = headersList.get("location", true);
|
||||
this.body = new Readable({ read: resume });
|
||||
this.body = new Readable2({ read: resume });
|
||||
const decoders = [];
|
||||
const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status);
|
||||
if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) {
|
||||
@@ -31244,7 +31244,7 @@ var require_readable2 = __commonJS({
|
||||
"use strict";
|
||||
var assert = require("node:assert");
|
||||
var { addAbortListener } = require("node:events");
|
||||
var { Readable } = require("node:stream");
|
||||
var { Readable: Readable2 } = require("node:stream");
|
||||
var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors2();
|
||||
var util = require_util9();
|
||||
var { ReadableStreamFrom } = require_util9();
|
||||
@@ -31258,7 +31258,7 @@ var require_readable2 = __commonJS({
|
||||
var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead");
|
||||
var noop = () => {
|
||||
};
|
||||
var BodyReadable = class extends Readable {
|
||||
var BodyReadable = class extends Readable2 {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {(this: Readable, size: number) => void} opts.resume
|
||||
@@ -31647,7 +31647,7 @@ var require_api_request2 = __commonJS({
|
||||
"use strict";
|
||||
var assert = require("node:assert");
|
||||
var { AsyncResource } = require("node:async_hooks");
|
||||
var { Readable } = require_readable2();
|
||||
var { Readable: Readable2 } = require_readable2();
|
||||
var { InvalidArgumentError, RequestAbortedError } = require_errors2();
|
||||
var util = require_util9();
|
||||
function noop() {
|
||||
@@ -31731,7 +31731,7 @@ var require_api_request2 = __commonJS({
|
||||
const parsedHeaders = headers;
|
||||
const contentType = parsedHeaders?.["content-type"];
|
||||
const contentLength = parsedHeaders?.["content-length"];
|
||||
const res = new Readable({
|
||||
const res = new Readable2({
|
||||
resume: () => controller.resume(),
|
||||
abort: (reason) => controller.abort(reason),
|
||||
contentType,
|
||||
@@ -32107,7 +32107,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
"node_modules/undici/lib/api/api-pipeline.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var {
|
||||
Readable,
|
||||
Readable: Readable2,
|
||||
Duplex,
|
||||
PassThrough
|
||||
} = require("node:stream");
|
||||
@@ -32124,7 +32124,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
function noop() {
|
||||
}
|
||||
var kResume = /* @__PURE__ */ Symbol("resume");
|
||||
var PipelineRequest = class extends Readable {
|
||||
var PipelineRequest = class extends Readable2 {
|
||||
constructor() {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = null;
|
||||
@@ -32142,7 +32142,7 @@ var require_api_pipeline2 = __commonJS({
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
var PipelineResponse = class extends Readable {
|
||||
var PipelineResponse = class extends Readable2 {
|
||||
constructor(resume) {
|
||||
super({ autoDestroy: true });
|
||||
this[kResume] = resume;
|
||||
@@ -36759,7 +36759,7 @@ var require_cache3 = __commonJS({
|
||||
"node_modules/undici/lib/interceptor/cache.js"(exports2, module2) {
|
||||
"use strict";
|
||||
var assert = require("node:assert");
|
||||
var { Readable } = require("node:stream");
|
||||
var { Readable: Readable2 } = require("node:stream");
|
||||
var util = require_util9();
|
||||
var CacheHandler = require_cache_handler();
|
||||
var MemoryCacheStore = require_memory_cache_store();
|
||||
@@ -36855,7 +36855,7 @@ var require_cache3 = __commonJS({
|
||||
return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler));
|
||||
}
|
||||
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.readableDidRead, "stream should not be readableDidRead");
|
||||
const controller = {
|
||||
@@ -39978,7 +39978,7 @@ var require_fetch2 = __commonJS({
|
||||
subresourceSet
|
||||
} = require_constants8();
|
||||
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 { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url2();
|
||||
var { getGlobalDispatcher } = require_global4();
|
||||
@@ -40940,7 +40940,7 @@ var require_fetch2 = __commonJS({
|
||||
const headersList = new HeadersList();
|
||||
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders);
|
||||
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 decoders = [];
|
||||
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);
|
||||
}
|
||||
|
||||
// src/download/manifest.ts
|
||||
var import_node_readline = require("node:readline");
|
||||
var import_node_stream = require("node:stream");
|
||||
|
||||
// src/utils/constants.ts
|
||||
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
|
||||
var cachedManifestData = /* @__PURE__ */ new Map();
|
||||
async function fetchManifest(manifestUrl = VERSIONS_MANIFEST_URL) {
|
||||
const cachedVersions = cachedManifestData.get(manifestUrl);
|
||||
if (cachedVersions !== void 0) {
|
||||
const cachedManifest = cachedManifestData.get(manifestUrl);
|
||||
if (cachedManifest?.complete === true) {
|
||||
debug(`Using cached manifest data from ${manifestUrl}`);
|
||||
return cachedVersions;
|
||||
}
|
||||
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 cachedManifest.versions;
|
||||
}
|
||||
const response = await fetchManifestResponse(manifestUrl);
|
||||
const body = await response.text();
|
||||
const versions = parseManifest(body, manifestUrl);
|
||||
cachedManifestData.set(manifestUrl, versions);
|
||||
cachedManifestData.set(manifestUrl, { complete: true, versions });
|
||||
return versions;
|
||||
}
|
||||
function parseManifest(data, sourceDescription) {
|
||||
@@ -50767,31 +50765,14 @@ function parseManifest(data, sourceDescription) {
|
||||
if (trimmed === "") {
|
||||
throw new Error(`Manifest at ${sourceDescription} is empty.`);
|
||||
}
|
||||
if (trimmed.startsWith("[")) {
|
||||
throw new Error(
|
||||
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`
|
||||
);
|
||||
}
|
||||
rejectLegacyManifest(trimmed, sourceDescription);
|
||||
const versions = [];
|
||||
for (const [index, line] of data.split("\n").entries()) {
|
||||
const record = line.trim();
|
||||
if (record === "") {
|
||||
continue;
|
||||
}
|
||||
let parsed;
|
||||
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);
|
||||
versions.push(parseManifestRecord(record, sourceDescription, index + 1));
|
||||
}
|
||||
if (versions.length === 0) {
|
||||
throw new Error(`No manifest data found in ${sourceDescription}.`);
|
||||
@@ -50799,13 +50780,104 @@ function parseManifest(data, sourceDescription) {
|
||||
return versions;
|
||||
}
|
||||
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) {
|
||||
throw new Error("No versions found in manifest data");
|
||||
}
|
||||
debug(`Latest version from manifest: ${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) {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
|
||||
+147
-39
@@ -1,3 +1,5 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import { Readable } from "node:stream";
|
||||
import * as core from "@actions/core";
|
||||
import { VERSIONS_MANIFEST_URL } from "../utils/constants";
|
||||
import { fetch } from "../utils/fetch";
|
||||
@@ -23,28 +25,26 @@ export interface ArtifactResult {
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
const cachedManifestData = new Map<string, ManifestVersion[]>();
|
||||
interface CachedManifest {
|
||||
complete: boolean;
|
||||
versions: ManifestVersion[];
|
||||
}
|
||||
|
||||
const cachedManifestData = new Map<string, CachedManifest>();
|
||||
|
||||
export async function fetchManifest(
|
||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||
): Promise<ManifestVersion[]> {
|
||||
const cachedVersions = cachedManifestData.get(manifestUrl);
|
||||
if (cachedVersions !== undefined) {
|
||||
const cachedManifest = cachedManifestData.get(manifestUrl);
|
||||
if (cachedManifest?.complete === true) {
|
||||
core.debug(`Using cached manifest data from ${manifestUrl}`);
|
||||
return cachedVersions;
|
||||
}
|
||||
|
||||
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 cachedManifest.versions;
|
||||
}
|
||||
|
||||
const response = await fetchManifestResponse(manifestUrl);
|
||||
const body = await response.text();
|
||||
const versions = parseManifest(body, manifestUrl);
|
||||
cachedManifestData.set(manifestUrl, versions);
|
||||
cachedManifestData.set(manifestUrl, { complete: true, versions });
|
||||
return versions;
|
||||
}
|
||||
|
||||
@@ -57,11 +57,7 @@ export function parseManifest(
|
||||
throw new Error(`Manifest at ${sourceDescription} is empty.`);
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("[")) {
|
||||
throw new Error(
|
||||
`Legacy JSON array manifests are no longer supported in ${sourceDescription}. Use the astral-sh/versions manifest format instead.`,
|
||||
);
|
||||
}
|
||||
rejectLegacyManifest(trimmed, sourceDescription);
|
||||
|
||||
const versions: ManifestVersion[] = [];
|
||||
|
||||
@@ -71,22 +67,7 @@ export function parseManifest(
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
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);
|
||||
versions.push(parseManifestRecord(record, sourceDescription, index + 1));
|
||||
}
|
||||
|
||||
if (versions.length === 0) {
|
||||
@@ -99,7 +80,10 @@ export function parseManifest(
|
||||
export async function getLatestVersion(
|
||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||
): 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) {
|
||||
throw new Error("No versions found in manifest data");
|
||||
@@ -109,6 +93,16 @@ export async function getLatestVersion(
|
||||
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(
|
||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||
): Promise<string[]> {
|
||||
@@ -125,10 +119,12 @@ export async function getArtifact(
|
||||
platform: string,
|
||||
manifestUrl: string = VERSIONS_MANIFEST_URL,
|
||||
): Promise<ArtifactResult | undefined> {
|
||||
const versions = await fetchManifest(manifestUrl);
|
||||
const versionData = versions.find(
|
||||
(candidate) => candidate.version === version,
|
||||
);
|
||||
const versionData =
|
||||
manifestUrl === VERSIONS_MANIFEST_URL
|
||||
? await findManifestVersion((candidate) => candidate.version === version)
|
||||
: (await fetchManifest(manifestUrl)).find(
|
||||
(candidate) => candidate.version === version,
|
||||
);
|
||||
if (!versionData) {
|
||||
core.debug(`Version ${version} not found in manifest ${manifestUrl}`);
|
||||
return undefined;
|
||||
@@ -169,6 +165,87 @@ export function clearManifestCache(manifestUrl?: string): void {
|
||||
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 {
|
||||
if (manifestUrl === VERSIONS_MANIFEST_URL) {
|
||||
return VERSIONS_MANIFEST_URL;
|
||||
@@ -177,6 +254,37 @@ function manifestSource(manifestUrl: string): string {
|
||||
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 {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
|
||||
+55
-8
@@ -2,7 +2,12 @@ import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import * as pep440 from "@renovatebot/pep440";
|
||||
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 * as log from "../utils/logging";
|
||||
import {
|
||||
@@ -82,13 +87,26 @@ class RangeVersionResolver implements ConcreteVersionResolver {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const availableVersions = await getAllVersions(context.manifestUrl);
|
||||
core.debug(`Available versions: ${availableVersions}`);
|
||||
|
||||
const resolvedVersion =
|
||||
context.resolutionStrategy === "lowest"
|
||||
? minSatisfying(availableVersions, context.parsedSpecifier.normalized)
|
||||
: maxSatisfying(availableVersions, context.parsedSpecifier.normalized);
|
||||
let resolvedVersion: string | undefined;
|
||||
if (
|
||||
context.resolutionStrategy === "highest" &&
|
||||
(context.manifestUrl === undefined ||
|
||||
context.manifestUrl === VERSIONS_MANIFEST_URL)
|
||||
) {
|
||||
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) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
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(
|
||||
versions: string[],
|
||||
version: string,
|
||||
|
||||
Reference in New Issue
Block a user