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 * 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 () => {
+150
View File
@@ -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", () => {
Generated Vendored
+204 -97
View File
@@ -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;
}
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}`);
const resolvedVersion = context3.resolutionStrategy === "lowest" ? minSatisfying3(availableVersions, context3.parsedSpecifier.normalized) : maxSatisfying2(availableVersions, context3.parsedSpecifier.normalized);
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
View File
@@ -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;
+145 -37
View File
@@ -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,8 +119,10 @@ export async function getArtifact(
platform: string,
manifestUrl: string = VERSIONS_MANIFEST_URL,
): Promise<ArtifactResult | undefined> {
const versions = await fetchManifest(manifestUrl);
const versionData = versions.find(
const versionData =
manifestUrl === VERSIONS_MANIFEST_URL
? await findManifestVersion((candidate) => candidate.version === version)
: (await fetchManifest(manifestUrl)).find(
(candidate) => candidate.version === version,
);
if (!versionData) {
@@ -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;
+51 -4
View File
@@ -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;
}
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}`);
const resolvedVersion =
resolvedVersion =
context.resolutionStrategy === "lowest"
? minSatisfying(availableVersions, context.parsedSpecifier.normalized)
: maxSatisfying(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,