mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-03-13 21:05:57 +00:00
Use astral-sh/versions as primary version provider (#802)
Some checks failed
test / test-act (push) Failing after 18s
Release Drafter / ✏️ Draft release (push) Waiting to run
test / test-tool-install (macos-14) (push) Waiting to run
test / test-tool-install (macos-latest) (push) Waiting to run
test / test-tool-install (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-14, macos-14) (push) Waiting to run
test / test-default-version (macos-14) (push) Waiting to run
test / test-default-version (macos-latest) (push) Waiting to run
test / test-default-version (windows-latest) (push) Waiting to run
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Waiting to run
test / test-python-version (macos-latest) (push) Waiting to run
test / test-activate-environment (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (windows-2022, windows-2022) (push) Waiting to run
test / test-python-version (windows-latest) (push) Waiting to run
test / test-activate-environment (macos-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-15, macos-15) (push) Waiting to run
test / test-activate-environment-custom-path (macos-latest) (push) Waiting to run
test / test-activate-environment-custom-path (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (ubuntu-24.04, ubuntu-24.04) (push) Waiting to run
test / test-cache-key-os-version (windows-2025, windows-2025) (push) Waiting to run
test / test-setup-cache (auto, windows-latest) (push) Waiting to run
test / test-setup-cache (false, windows-latest) (push) Waiting to run
test / test-setup-cache (true, windows-latest) (push) Waiting to run
test / test-restore-cache (auto, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (auto, windows-latest) (push) Blocked by required conditions
test / test-restore-cache (false, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (false, windows-latest) (push) Blocked by required conditions
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Waiting to run
test / test-python-install-dir (map[expected-python-dir:D:\a\_temp\uv-python-dir os:windows-latest]) (push) Waiting to run
test / all-tests-passed (push) Blocked by required conditions
test / test-restore-cache (true, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (true, windows-latest) (push) Blocked by required conditions
CodeQL / Analyze (TypeScript) (push) Failing after 6s
test / lint (push) Failing after 6s
test / test-default-version (ubuntu-latest) (push) Failing after 6s
test / test-uv-no-modify-path (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.0 resolution-strategy:lowest version-input:>=0.1.0,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.45 resolution-strategy:highest version-input:>=0.1,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.0 version-input:0.3.0]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.2 version-input:0.3.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3.x]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.30 version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-latest-version (>=0.8) (push) Failing after 12s
test / test-latest-version (latest) (push) Failing after 13s
test / test-from-working-directory-version (map[expected-version:0.5.14 working-directory:__tests__/fixtures/pyproject-toml-project]) (push) Failing after 18s
test / test-from-working-directory-version (map[expected-version:0.5.15 working-directory:__tests__/fixtures/uv-toml-project]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.5.15 version-file:__tests__/fixtures/.tool-versions]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.6.17 version-file:__tests__/fixtures/uv-in-requirements-txt-project/requirements.txt]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.8.3 version-file:__tests__/fixtures/uv-in-requirements-hash-txt-project/requirements.txt]) (push) Failing after 18s
test / test-malformed-pyproject-file-fallback (push) Failing after 17s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 18s
test / test-with-explicit-token (push) Failing after 17s
test / test-uvx (push) Failing after 19s
test / test-tool-install (ubuntu-latest) (push) Failing after 20s
test / test-python-version (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment-custom-path (ubuntu-latest) (push) Failing after 20s
test / test-cache-key-os-version (ubuntu-22.04, ubuntu-22.04) (push) Failing after 18s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 18s
test / test-restore-cache-requirements-txt (push) Has been skipped
test / test-setup-cache-dependency-glob (push) Failing after 18s
test / test-setup-cache-requirements-txt (push) Failing after 18s
test / test-restore-cache-dependency-glob (push) Has been skipped
test / test-setup-cache-save-cache-false (push) Failing after 18s
test / test-restore-cache-save-cache-false (push) Has been skipped
test / test-setup-cache-restore-cache-false (push) Failing after 17s
test / test-restore-cache-restore-cache-false (push) Has been skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 17s
test / test-cache-local-cache-disabled (push) Failing after 17s
test / test-cache-local-cache-disabled-but-explicit-path (push) Failing after 17s
test / test-no-python-version (push) Failing after 17s
test / test-debian-unstable (push) Failing after 31s
test / test-musl (push) Failing after 30s
test / test-custom-manifest-file (push) Failing after 18s
test / test-absolute-path (push) Failing after 18s
test / test-relative-path (push) Failing after 18s
test / test-cache-prune-force (push) Failing after 18s
test / test-cache-dir-from-file (push) Failing after 18s
test / test-cache-python-missing-managed-install-dir (push) Failing after 19s
test / test-cache-python-installs (push) Failing after 19s
test / test-python-install-dir (map[expected-python-dir:/home/runner/work/_temp/uv-python-dir os:ubuntu-latest]) (push) Failing after 19s
test / test-restore-python-installs (push) Has been skipped
test / validate-typings (push) Failing after 21s
Some checks failed
test / test-act (push) Failing after 18s
Release Drafter / ✏️ Draft release (push) Waiting to run
test / test-tool-install (macos-14) (push) Waiting to run
test / test-tool-install (macos-latest) (push) Waiting to run
test / test-tool-install (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-14, macos-14) (push) Waiting to run
test / test-default-version (macos-14) (push) Waiting to run
test / test-default-version (macos-latest) (push) Waiting to run
test / test-default-version (windows-latest) (push) Waiting to run
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Waiting to run
test / test-python-version (macos-latest) (push) Waiting to run
test / test-activate-environment (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (windows-2022, windows-2022) (push) Waiting to run
test / test-python-version (windows-latest) (push) Waiting to run
test / test-activate-environment (macos-latest) (push) Waiting to run
test / test-cache-key-os-version (macos-15, macos-15) (push) Waiting to run
test / test-activate-environment-custom-path (macos-latest) (push) Waiting to run
test / test-activate-environment-custom-path (windows-latest) (push) Waiting to run
test / test-cache-key-os-version (ubuntu-24.04, ubuntu-24.04) (push) Waiting to run
test / test-cache-key-os-version (windows-2025, windows-2025) (push) Waiting to run
test / test-setup-cache (auto, windows-latest) (push) Waiting to run
test / test-setup-cache (false, windows-latest) (push) Waiting to run
test / test-setup-cache (true, windows-latest) (push) Waiting to run
test / test-restore-cache (auto, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (auto, windows-latest) (push) Blocked by required conditions
test / test-restore-cache (false, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (false, windows-latest) (push) Blocked by required conditions
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Waiting to run
test / test-python-install-dir (map[expected-python-dir:D:\a\_temp\uv-python-dir os:windows-latest]) (push) Waiting to run
test / all-tests-passed (push) Blocked by required conditions
test / test-restore-cache (true, ubuntu-latest) (push) Blocked by required conditions
test / test-restore-cache (true, windows-latest) (push) Blocked by required conditions
CodeQL / Analyze (TypeScript) (push) Failing after 6s
test / lint (push) Failing after 6s
test / test-default-version (ubuntu-latest) (push) Failing after 6s
test / test-uv-no-modify-path (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.0 resolution-strategy:lowest version-input:>=0.1.0,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.1.45 resolution-strategy:highest version-input:>=0.1,<0.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.0 version-input:0.3.0]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.2 version-input:0.3.2]) (push) Failing after 10s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3.x]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.3.5 version-input:0.3]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.25 resolution-strategy:lowest version-input:>=0.4.25]) (push) Failing after 12s
test / test-specific-version (map[expected-version:0.4.30 version-input:>=0.4.25,<0.5]) (push) Failing after 12s
test / test-latest-version (>=0.8) (push) Failing after 12s
test / test-latest-version (latest) (push) Failing after 13s
test / test-from-working-directory-version (map[expected-version:0.5.14 working-directory:__tests__/fixtures/pyproject-toml-project]) (push) Failing after 18s
test / test-from-working-directory-version (map[expected-version:0.5.15 working-directory:__tests__/fixtures/uv-toml-project]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.5.15 version-file:__tests__/fixtures/.tool-versions]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.6.17 version-file:__tests__/fixtures/uv-in-requirements-txt-project/requirements.txt]) (push) Failing after 18s
test / test-version-file-version (map[expected-version:0.8.3 version-file:__tests__/fixtures/uv-in-requirements-hash-txt-project/requirements.txt]) (push) Failing after 18s
test / test-malformed-pyproject-file-fallback (push) Failing after 17s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 18s
test / test-with-explicit-token (push) Failing after 17s
test / test-uvx (push) Failing after 19s
test / test-tool-install (ubuntu-latest) (push) Failing after 20s
test / test-python-version (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment (ubuntu-latest) (push) Failing after 20s
test / test-activate-environment-custom-path (ubuntu-latest) (push) Failing after 20s
test / test-cache-key-os-version (ubuntu-22.04, ubuntu-22.04) (push) Failing after 18s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 18s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 18s
test / test-restore-cache-requirements-txt (push) Has been skipped
test / test-setup-cache-dependency-glob (push) Failing after 18s
test / test-setup-cache-requirements-txt (push) Failing after 18s
test / test-restore-cache-dependency-glob (push) Has been skipped
test / test-setup-cache-save-cache-false (push) Failing after 18s
test / test-restore-cache-save-cache-false (push) Has been skipped
test / test-setup-cache-restore-cache-false (push) Failing after 17s
test / test-restore-cache-restore-cache-false (push) Has been skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 17s
test / test-cache-local-cache-disabled (push) Failing after 17s
test / test-cache-local-cache-disabled-but-explicit-path (push) Failing after 17s
test / test-no-python-version (push) Failing after 17s
test / test-debian-unstable (push) Failing after 31s
test / test-musl (push) Failing after 30s
test / test-custom-manifest-file (push) Failing after 18s
test / test-absolute-path (push) Failing after 18s
test / test-relative-path (push) Failing after 18s
test / test-cache-prune-force (push) Failing after 18s
test / test-cache-dir-from-file (push) Failing after 18s
test / test-cache-python-missing-managed-install-dir (push) Failing after 19s
test / test-cache-python-installs (push) Failing after 19s
test / test-python-install-dir (map[expected-python-dir:/home/runner/work/_temp/uv-python-dir os:ubuntu-latest]) (push) Failing after 19s
test / test-restore-python-installs (push) Has been skipped
test / validate-typings (push) Failing after 21s
Closes: #777 Closes: #325
This commit is contained in:
committed by
GitHub
parent
0f6ec07aaf
commit
e06108dd0a
@@ -6,33 +6,35 @@ import type { Architecture, Platform } from "../../utils/platforms";
|
||||
import { KNOWN_CHECKSUMS } from "./known-checksums";
|
||||
|
||||
export async function validateChecksum(
|
||||
checkSum: string | undefined,
|
||||
checksum: string | undefined,
|
||||
downloadPath: string,
|
||||
arch: Architecture,
|
||||
platform: Platform,
|
||||
version: string,
|
||||
): Promise<void> {
|
||||
let isValid: boolean | undefined;
|
||||
if (checkSum !== undefined && checkSum !== "") {
|
||||
isValid = await validateFileCheckSum(downloadPath, checkSum);
|
||||
} else {
|
||||
core.debug("Checksum not provided. Checking known checksums.");
|
||||
const key = `${arch}-${platform}-${version}`;
|
||||
if (key in KNOWN_CHECKSUMS) {
|
||||
const knownChecksum = KNOWN_CHECKSUMS[`${arch}-${platform}-${version}`];
|
||||
core.debug(`Checking checksum for ${arch}-${platform}-${version}.`);
|
||||
isValid = await validateFileCheckSum(downloadPath, knownChecksum);
|
||||
} else {
|
||||
core.debug(`No known checksum found for ${key}.`);
|
||||
}
|
||||
const key = `${arch}-${platform}-${version}`;
|
||||
const hasProvidedChecksum = checksum !== undefined && checksum !== "";
|
||||
const checksumToUse = hasProvidedChecksum ? checksum : KNOWN_CHECKSUMS[key];
|
||||
|
||||
if (checksumToUse === undefined) {
|
||||
core.debug(`No checksum found for ${key}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValid === false) {
|
||||
throw new Error(`Checksum for ${downloadPath} did not match ${checkSum}.`);
|
||||
}
|
||||
if (isValid === true) {
|
||||
core.debug(`Checksum for ${downloadPath} is valid.`);
|
||||
const checksumSource = hasProvidedChecksum
|
||||
? "provided checksum"
|
||||
: `KNOWN_CHECKSUMS entry for ${key}`;
|
||||
|
||||
core.debug(`Validating checksum using ${checksumSource}.`);
|
||||
const isValid = await validateFileCheckSum(downloadPath, checksumToUse);
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
`Checksum for ${downloadPath} did not match ${checksumToUse}.`,
|
||||
);
|
||||
}
|
||||
|
||||
core.debug(`Checksum for ${downloadPath} is valid.`);
|
||||
}
|
||||
|
||||
async function validateFileCheckSum(
|
||||
|
||||
@@ -1,59 +1,34 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import { KNOWN_CHECKSUMS } from "./known-checksums";
|
||||
|
||||
export interface ChecksumEntry {
|
||||
key: string;
|
||||
checksum: string;
|
||||
}
|
||||
|
||||
export async function updateChecksums(
|
||||
filePath: string,
|
||||
downloadUrls: string[],
|
||||
checksumEntries: ChecksumEntry[],
|
||||
): Promise<void> {
|
||||
await fs.rm(filePath);
|
||||
await fs.appendFile(
|
||||
filePath,
|
||||
"// AUTOGENERATED_DO_NOT_EDIT\nexport const KNOWN_CHECKSUMS: { [key: string]: string } = {\n",
|
||||
);
|
||||
let firstLine = true;
|
||||
for (const downloadUrl of downloadUrls) {
|
||||
const key = getKey(downloadUrl);
|
||||
if (key === undefined) {
|
||||
const deduplicatedEntries = new Map<string, string>();
|
||||
|
||||
for (const entry of checksumEntries) {
|
||||
if (deduplicatedEntries.has(entry.key)) {
|
||||
continue;
|
||||
}
|
||||
const checksum = await getOrDownloadChecksum(key, downloadUrl);
|
||||
if (!firstLine) {
|
||||
await fs.appendFile(filePath, ",\n");
|
||||
}
|
||||
await fs.appendFile(filePath, ` "${key}":\n "${checksum}"`);
|
||||
firstLine = false;
|
||||
}
|
||||
await fs.appendFile(filePath, ",\n};\n");
|
||||
}
|
||||
|
||||
function getKey(downloadUrl: string): string | undefined {
|
||||
// https://github.com/astral-sh/uv/releases/download/0.3.2/uv-aarch64-apple-darwin.tar.gz.sha256
|
||||
const parts = downloadUrl.split("/");
|
||||
const fileName = parts[parts.length - 1];
|
||||
if (fileName.startsWith("source")) {
|
||||
return undefined;
|
||||
deduplicatedEntries.set(entry.key, entry.checksum);
|
||||
}
|
||||
const name = fileName.split(".")[0].split("uv-")[1];
|
||||
const version = parts[parts.length - 2];
|
||||
return `${name}-${version}`;
|
||||
}
|
||||
|
||||
async function getOrDownloadChecksum(
|
||||
key: string,
|
||||
downloadUrl: string,
|
||||
): Promise<string> {
|
||||
let checksum = "";
|
||||
if (key in KNOWN_CHECKSUMS) {
|
||||
checksum = KNOWN_CHECKSUMS[key];
|
||||
} else {
|
||||
const content = await downloadAssetContent(downloadUrl);
|
||||
checksum = content.split(" ")[0].trim();
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
const body = [...deduplicatedEntries.entries()]
|
||||
.map(([key, checksum]) => ` "${key}":\n "${checksum}"`)
|
||||
.join(",\n");
|
||||
|
||||
async function downloadAssetContent(downloadUrl: string): Promise<string> {
|
||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||
const content = await fs.readFile(downloadPath, "utf8");
|
||||
return content;
|
||||
const content =
|
||||
"// AUTOGENERATED_DO_NOT_EDIT\n" +
|
||||
"export const KNOWN_CHECKSUMS: { [key: string]: string } = {\n" +
|
||||
body +
|
||||
(body === "" ? "" : ",\n") +
|
||||
"};\n";
|
||||
|
||||
await fs.writeFile(filePath, content);
|
||||
}
|
||||
|
||||
@@ -2,20 +2,21 @@ import { promises as fs } from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import type { Endpoints } from "@octokit/types";
|
||||
import * as pep440 from "@renovatebot/pep440";
|
||||
import * as semver from "semver";
|
||||
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
|
||||
import { Octokit } from "../utils/octokit";
|
||||
import { TOOL_CACHE_NAME, VERSIONS_NDJSON_URL } from "../utils/constants";
|
||||
import type { Architecture, Platform } from "../utils/platforms";
|
||||
import { validateChecksum } from "./checksum/checksum";
|
||||
import {
|
||||
getDownloadUrl,
|
||||
getAllVersions as getAllManifestVersions,
|
||||
getLatestKnownVersion as getLatestVersionInManifest,
|
||||
getManifestArtifact,
|
||||
} from "./version-manifest";
|
||||
|
||||
type Release =
|
||||
Endpoints["GET /repos/{owner}/{repo}/releases"]["response"]["data"][number];
|
||||
import {
|
||||
getAllVersions as getAllVersionsFromNdjson,
|
||||
getArtifact as getArtifactFromNdjson,
|
||||
getLatestVersion as getLatestVersionFromNdjson,
|
||||
} from "./versions-client";
|
||||
|
||||
export function tryGetFromToolCache(
|
||||
arch: Architecture,
|
||||
@@ -32,19 +33,26 @@ export function tryGetFromToolCache(
|
||||
return { installedPath, version: resolvedVersion };
|
||||
}
|
||||
|
||||
export async function downloadVersionFromGithub(
|
||||
export async function downloadVersionFromNdjson(
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
checkSum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ version: string; cachedToolDir: string }> {
|
||||
const artifact = `uv-${arch}-${platform}`;
|
||||
const extension = getExtension(platform);
|
||||
const downloadUrl = `https://github.com/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`;
|
||||
const artifact = await getArtifactFromNdjson(version, arch, platform);
|
||||
|
||||
if (!artifact) {
|
||||
throw new Error(
|
||||
`Could not find artifact for version ${version}, arch ${arch}, platform ${platform} in ${VERSIONS_NDJSON_URL} .`,
|
||||
);
|
||||
}
|
||||
|
||||
// For the default astral-sh/versions source, checksum validation relies on
|
||||
// user input or the built-in KNOWN_CHECKSUMS table, not NDJSON sha256 values.
|
||||
return await downloadVersion(
|
||||
downloadUrl,
|
||||
artifact,
|
||||
artifact.url,
|
||||
`uv-${arch}-${platform}`,
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
@@ -54,38 +62,32 @@ export async function downloadVersionFromGithub(
|
||||
}
|
||||
|
||||
export async function downloadVersionFromManifest(
|
||||
manifestUrl: string | undefined,
|
||||
manifestUrl: string,
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
checkSum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ version: string; cachedToolDir: string }> {
|
||||
const downloadUrl = await getDownloadUrl(
|
||||
const artifact = await getManifestArtifact(
|
||||
manifestUrl,
|
||||
version,
|
||||
arch,
|
||||
platform,
|
||||
);
|
||||
if (!downloadUrl) {
|
||||
core.info(
|
||||
`manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}. Falling back to GitHub releases.`,
|
||||
);
|
||||
return await downloadVersionFromGithub(
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
checkSum,
|
||||
githubToken,
|
||||
if (!artifact) {
|
||||
throw new Error(
|
||||
`manifest-file does not contain version ${version}, arch ${arch}, platform ${platform}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return await downloadVersion(
|
||||
downloadUrl,
|
||||
artifact.downloadUrl,
|
||||
`uv-${arch}-${platform}`,
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
checkSum,
|
||||
resolveChecksum(checkSum, artifact.checksum),
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
@@ -96,7 +98,7 @@ async function downloadVersion(
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
checkSum: string | undefined,
|
||||
checksum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ version: string; cachedToolDir: string }> {
|
||||
core.info(`Downloading uv from "${downloadUrl}" ...`);
|
||||
@@ -105,14 +107,14 @@ async function downloadVersion(
|
||||
undefined,
|
||||
githubToken,
|
||||
);
|
||||
await validateChecksum(checkSum, downloadPath, arch, platform, version);
|
||||
await validateChecksum(checksum, downloadPath, arch, platform, version);
|
||||
|
||||
let uvDir: string;
|
||||
if (platform === "pc-windows-msvc") {
|
||||
// On windows extracting the zip does not create an intermediate directory
|
||||
// On windows extracting the zip does not create an intermediate directory.
|
||||
try {
|
||||
// Try tar first as it's much faster, but only bsdtar supports zip files,
|
||||
// so this my fail if another tar, like gnu tar, ends up being used.
|
||||
// so this may fail if another tar, like gnu tar, ends up being used.
|
||||
uvDir = await tc.extractTar(downloadPath, undefined, "x");
|
||||
} catch (err) {
|
||||
core.info(
|
||||
@@ -127,6 +129,7 @@ async function downloadVersion(
|
||||
const extractedDir = await tc.extractTar(downloadPath);
|
||||
uvDir = path.join(extractedDir, artifactName);
|
||||
}
|
||||
|
||||
const cachedToolDir = await tc.cacheDir(
|
||||
uvDir,
|
||||
TOOL_CACHE_NAME,
|
||||
@@ -136,14 +139,22 @@ async function downloadVersion(
|
||||
return { cachedToolDir, version: version };
|
||||
}
|
||||
|
||||
function resolveChecksum(
|
||||
checkSum: string | undefined,
|
||||
manifestChecksum?: string,
|
||||
): string | undefined {
|
||||
return checkSum !== undefined && checkSum !== ""
|
||||
? checkSum
|
||||
: manifestChecksum;
|
||||
}
|
||||
|
||||
function getExtension(platform: Platform): string {
|
||||
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||
}
|
||||
|
||||
export async function resolveVersion(
|
||||
versionInput: string,
|
||||
manifestFile: string | undefined,
|
||||
githubToken: string,
|
||||
manifestUrl: string | undefined,
|
||||
resolutionStrategy: "highest" | "lowest" = "highest",
|
||||
): Promise<string> {
|
||||
core.debug(`Resolving version: ${versionInput}`);
|
||||
@@ -155,15 +166,15 @@ export async function resolveVersion(
|
||||
if (resolveVersionSpecifierToLatest) {
|
||||
core.info("Found minimum version specifier, using latest version");
|
||||
}
|
||||
if (manifestFile) {
|
||||
if (manifestUrl !== undefined) {
|
||||
version =
|
||||
versionInput === "latest" || resolveVersionSpecifierToLatest
|
||||
? await getLatestVersionInManifest(manifestFile)
|
||||
? await getLatestVersionInManifest(manifestUrl)
|
||||
: versionInput;
|
||||
} else {
|
||||
version =
|
||||
versionInput === "latest" || resolveVersionSpecifierToLatest
|
||||
? await getLatestVersion(githubToken)
|
||||
? await getLatestVersionFromNdjson()
|
||||
: versionInput;
|
||||
}
|
||||
if (tc.isExplicitVersion(version)) {
|
||||
@@ -175,91 +186,33 @@ export async function resolveVersion(
|
||||
}
|
||||
return version;
|
||||
}
|
||||
const availableVersions = await getAvailableVersions(githubToken);
|
||||
|
||||
const availableVersions = await getAvailableVersions(manifestUrl);
|
||||
core.debug(`Available versions: ${availableVersions}`);
|
||||
const resolvedVersion =
|
||||
resolutionStrategy === "lowest"
|
||||
? minSatisfying(availableVersions, version)
|
||||
: maxSatisfying(availableVersions, version);
|
||||
|
||||
if (resolvedVersion === undefined) {
|
||||
throw new Error(`No version found for ${version}`);
|
||||
}
|
||||
|
||||
return resolvedVersion;
|
||||
}
|
||||
|
||||
async function getAvailableVersions(githubToken: string): Promise<string[]> {
|
||||
core.info("Getting available versions from GitHub API...");
|
||||
try {
|
||||
const octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
});
|
||||
return await getReleaseTagNames(octokit);
|
||||
} catch (err) {
|
||||
if ((err as Error).message.includes("Bad credentials")) {
|
||||
core.info(
|
||||
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
|
||||
);
|
||||
const octokit = new Octokit();
|
||||
return await getReleaseTagNames(octokit);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function getReleaseTagNames(octokit: Octokit): Promise<string[]> {
|
||||
const response: Release[] = await octokit.paginate(
|
||||
octokit.rest.repos.listReleases,
|
||||
{
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
},
|
||||
);
|
||||
const releaseTagNames = response.map((release) => release.tag_name);
|
||||
if (releaseTagNames.length === 0) {
|
||||
throw Error(
|
||||
"Github API request failed while getting releases. Check the GitHub status page for outages. Try again later.",
|
||||
async function getAvailableVersions(
|
||||
manifestUrl: string | undefined,
|
||||
): Promise<string[]> {
|
||||
if (manifestUrl !== undefined) {
|
||||
core.info(
|
||||
`Getting available versions from manifest-file ${manifestUrl} ...`,
|
||||
);
|
||||
}
|
||||
return releaseTagNames;
|
||||
}
|
||||
|
||||
async function getLatestVersion(githubToken: string) {
|
||||
core.info("Getting latest version from GitHub API...");
|
||||
const octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
});
|
||||
|
||||
let latestRelease: { tag_name: string } | undefined;
|
||||
try {
|
||||
latestRelease = await getLatestRelease(octokit);
|
||||
} catch (err) {
|
||||
if ((err as Error).message.includes("Bad credentials")) {
|
||||
core.info(
|
||||
"No (valid) GitHub token provided. Falling back to anonymous. Requests might be rate limited.",
|
||||
);
|
||||
const octokit = new Octokit();
|
||||
latestRelease = await getLatestRelease(octokit);
|
||||
} else {
|
||||
core.error(
|
||||
"Github API request failed while getting latest release. Check the GitHub status page for outages. Try again later.",
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
return await getAllManifestVersions(manifestUrl);
|
||||
}
|
||||
|
||||
if (!latestRelease) {
|
||||
throw new Error("Could not determine latest release.");
|
||||
}
|
||||
core.debug(`Latest version: ${latestRelease.tag_name}`);
|
||||
return latestRelease.tag_name;
|
||||
}
|
||||
|
||||
async function getLatestRelease(octokit: Octokit) {
|
||||
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
});
|
||||
return latestRelease;
|
||||
core.info(`Getting available versions from ${VERSIONS_NDJSON_URL} ...`);
|
||||
return await getAllVersionsFromNdjson();
|
||||
}
|
||||
|
||||
function maxSatisfying(
|
||||
|
||||
80
src/download/legacy-version-manifest.ts
Normal file
80
src/download/legacy-version-manifest.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export interface ManifestEntry {
|
||||
arch: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
downloadUrl: string;
|
||||
checksum?: string;
|
||||
variant?: string;
|
||||
archiveFormat?: string;
|
||||
}
|
||||
|
||||
interface LegacyManifestEntry {
|
||||
arch: string;
|
||||
platform: string;
|
||||
version: string;
|
||||
downloadUrl: string;
|
||||
checksum?: string;
|
||||
}
|
||||
|
||||
const warnedLegacyManifestUrls = new Set<string>();
|
||||
|
||||
export function parseLegacyManifestEntries(
|
||||
parsedEntries: unknown[],
|
||||
manifestUrl: string,
|
||||
): ManifestEntry[] {
|
||||
warnAboutLegacyManifestFormat(manifestUrl);
|
||||
|
||||
return parsedEntries.map((entry, index) => {
|
||||
if (!isLegacyManifestEntry(entry)) {
|
||||
throw new Error(
|
||||
`Invalid legacy manifest-file entry at index ${index} in ${manifestUrl}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
arch: entry.arch,
|
||||
checksum: entry.checksum,
|
||||
downloadUrl: entry.downloadUrl,
|
||||
platform: entry.platform,
|
||||
version: entry.version,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function clearLegacyManifestWarnings(): void {
|
||||
warnedLegacyManifestUrls.clear();
|
||||
}
|
||||
|
||||
function warnAboutLegacyManifestFormat(manifestUrl: string): void {
|
||||
if (warnedLegacyManifestUrls.has(manifestUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
warnedLegacyManifestUrls.add(manifestUrl);
|
||||
core.warning(
|
||||
`manifest-file ${manifestUrl} uses the legacy JSON array format, which is deprecated. Please migrate to the astral-sh/versions NDJSON format before the next major release.`,
|
||||
);
|
||||
}
|
||||
|
||||
function isLegacyManifestEntry(value: unknown): value is LegacyManifestEntry {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checksumIsValid =
|
||||
typeof value.checksum === "string" || value.checksum === undefined;
|
||||
|
||||
return (
|
||||
typeof value.arch === "string" &&
|
||||
checksumIsValid &&
|
||||
typeof value.downloadUrl === "string" &&
|
||||
typeof value.platform === "string" &&
|
||||
typeof value.version === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
39
src/download/variant-selection.ts
Normal file
39
src/download/variant-selection.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
interface VariantAwareEntry {
|
||||
variant?: string;
|
||||
}
|
||||
|
||||
export function selectDefaultVariant<T extends VariantAwareEntry>(
|
||||
entries: T[],
|
||||
duplicateEntryDescription: string,
|
||||
): T {
|
||||
const firstEntry = entries[0];
|
||||
if (firstEntry === undefined) {
|
||||
throw new Error("selectDefaultVariant requires at least one candidate.");
|
||||
}
|
||||
|
||||
if (entries.length === 1) {
|
||||
return firstEntry;
|
||||
}
|
||||
|
||||
const defaultEntries = entries.filter((entry) =>
|
||||
isDefaultVariant(entry.variant),
|
||||
);
|
||||
if (defaultEntries.length === 1) {
|
||||
return defaultEntries[0];
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`${duplicateEntryDescription} with variants ${formatVariants(entries)}. setup-uv currently requires a single default variant for duplicate platform entries.`,
|
||||
);
|
||||
}
|
||||
|
||||
function isDefaultVariant(variant: string | undefined): boolean {
|
||||
return variant === undefined || variant === "default";
|
||||
}
|
||||
|
||||
function formatVariants<T extends VariantAwareEntry>(entries: T[]): string {
|
||||
return entries
|
||||
.map((entry) => entry.variant ?? "default")
|
||||
.sort((left, right) => left.localeCompare(right))
|
||||
.join(", ");
|
||||
}
|
||||
@@ -1,91 +1,169 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import * as semver from "semver";
|
||||
import { fetch } from "../utils/fetch";
|
||||
import {
|
||||
clearLegacyManifestWarnings,
|
||||
type ManifestEntry,
|
||||
parseLegacyManifestEntries,
|
||||
} from "./legacy-version-manifest";
|
||||
import { selectDefaultVariant } from "./variant-selection";
|
||||
import { type NdjsonVersion, parseVersionData } from "./versions-client";
|
||||
|
||||
const localManifestFile = join(__dirname, "..", "..", "version-manifest.json");
|
||||
|
||||
interface ManifestEntry {
|
||||
version: string;
|
||||
artifactName: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
export interface ManifestArtifact {
|
||||
downloadUrl: string;
|
||||
checksum?: string;
|
||||
archiveFormat?: string;
|
||||
}
|
||||
|
||||
const cachedManifestEntries = new Map<string, ManifestEntry[]>();
|
||||
|
||||
export async function getLatestKnownVersion(
|
||||
manifestUrl: string | undefined,
|
||||
manifestUrl: string,
|
||||
): Promise<string> {
|
||||
const manifestEntries = await getManifestEntries(manifestUrl);
|
||||
return manifestEntries.reduce((a, b) =>
|
||||
semver.gt(a.version, b.version) ? a : b,
|
||||
).version;
|
||||
const versions = await getAllVersions(manifestUrl);
|
||||
const latestVersion = versions.reduce((latest, current) =>
|
||||
semver.gt(current, latest) ? current : latest,
|
||||
);
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
export async function getDownloadUrl(
|
||||
manifestUrl: string | undefined,
|
||||
export async function getAllVersions(manifestUrl: string): Promise<string[]> {
|
||||
const manifestEntries = await getManifestEntries(manifestUrl);
|
||||
return [...new Set(manifestEntries.map((entry) => entry.version))];
|
||||
}
|
||||
|
||||
export async function getManifestArtifact(
|
||||
manifestUrl: string,
|
||||
version: string,
|
||||
arch: string,
|
||||
platform: string,
|
||||
): Promise<string | undefined> {
|
||||
): Promise<ManifestArtifact | undefined> {
|
||||
const manifestEntries = await getManifestEntries(manifestUrl);
|
||||
const entry = manifestEntries.find(
|
||||
(entry) =>
|
||||
entry.version === version &&
|
||||
entry.arch === arch &&
|
||||
entry.platform === platform,
|
||||
const entry = selectManifestEntry(
|
||||
manifestEntries,
|
||||
manifestUrl,
|
||||
version,
|
||||
arch,
|
||||
platform,
|
||||
);
|
||||
return entry ? entry.downloadUrl : undefined;
|
||||
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
archiveFormat: entry.archiveFormat,
|
||||
checksum: entry.checksum,
|
||||
downloadUrl: entry.downloadUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearManifestCache(): void {
|
||||
cachedManifestEntries.clear();
|
||||
clearLegacyManifestWarnings();
|
||||
}
|
||||
|
||||
async function getManifestEntries(
|
||||
manifestUrl: string | undefined,
|
||||
): Promise<ManifestEntry[]> {
|
||||
let data: string;
|
||||
if (manifestUrl !== undefined) {
|
||||
core.info(`Fetching manifest-file from: ${manifestUrl}`);
|
||||
const response = await fetch(manifestUrl, {});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch manifest-file: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
data = await response.text();
|
||||
} else {
|
||||
core.info("manifest-file not provided, reading from local file.");
|
||||
const fileContent = await fs.readFile(localManifestFile);
|
||||
data = fileContent.toString();
|
||||
}
|
||||
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
export async function updateVersionManifest(
|
||||
manifestUrl: string,
|
||||
downloadUrls: string[],
|
||||
): Promise<void> {
|
||||
const manifest: ManifestEntry[] = [];
|
||||
|
||||
for (const downloadUrl of downloadUrls) {
|
||||
const urlParts = downloadUrl.split("/");
|
||||
const version = urlParts[urlParts.length - 2];
|
||||
const artifactName = urlParts[urlParts.length - 1];
|
||||
if (!artifactName.startsWith("uv-")) {
|
||||
continue;
|
||||
}
|
||||
if (artifactName.startsWith("uv-installer")) {
|
||||
continue;
|
||||
}
|
||||
const artifactParts = artifactName.split(".")[0].split("-");
|
||||
manifest.push({
|
||||
arch: artifactParts[1],
|
||||
artifactName: artifactName,
|
||||
downloadUrl: downloadUrl,
|
||||
platform: artifactName.split(`uv-${artifactParts[1]}-`)[1].split(".")[0],
|
||||
version: version,
|
||||
});
|
||||
): Promise<ManifestEntry[]> {
|
||||
const cachedEntries = cachedManifestEntries.get(manifestUrl);
|
||||
if (cachedEntries !== undefined) {
|
||||
core.debug(`Using cached manifest-file from: ${manifestUrl}`);
|
||||
return cachedEntries;
|
||||
}
|
||||
|
||||
core.info(`Fetching manifest-file from: ${manifestUrl}`);
|
||||
const response = await fetch(manifestUrl, {});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch manifest-file: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.text();
|
||||
const parsedEntries = parseManifestEntries(data, manifestUrl);
|
||||
cachedManifestEntries.set(manifestUrl, parsedEntries);
|
||||
|
||||
return parsedEntries;
|
||||
}
|
||||
|
||||
function parseManifestEntries(
|
||||
data: string,
|
||||
manifestUrl: string,
|
||||
): ManifestEntry[] {
|
||||
const trimmed = data.trim();
|
||||
if (trimmed === "") {
|
||||
throw new Error(`manifest-file at ${manifestUrl} is empty.`);
|
||||
}
|
||||
|
||||
const parsedAsJson = tryParseJson(trimmed);
|
||||
if (Array.isArray(parsedAsJson)) {
|
||||
return parseLegacyManifestEntries(parsedAsJson, manifestUrl);
|
||||
}
|
||||
|
||||
const versions = parseVersionData(trimmed, manifestUrl);
|
||||
return mapNdjsonVersionsToManifestEntries(versions, manifestUrl);
|
||||
}
|
||||
|
||||
function mapNdjsonVersionsToManifestEntries(
|
||||
versions: NdjsonVersion[],
|
||||
manifestUrl: string,
|
||||
): ManifestEntry[] {
|
||||
const manifestEntries: ManifestEntry[] = [];
|
||||
|
||||
for (const versionData of versions) {
|
||||
for (const artifact of versionData.artifacts) {
|
||||
const [arch, ...platformParts] = artifact.platform.split("-");
|
||||
if (arch === undefined || platformParts.length === 0) {
|
||||
throw new Error(
|
||||
`Invalid artifact platform '${artifact.platform}' in manifest-file ${manifestUrl}.`,
|
||||
);
|
||||
}
|
||||
|
||||
manifestEntries.push({
|
||||
arch,
|
||||
archiveFormat: artifact.archive_format,
|
||||
checksum: artifact.sha256,
|
||||
downloadUrl: artifact.url,
|
||||
platform: platformParts.join("-"),
|
||||
variant: artifact.variant,
|
||||
version: versionData.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return manifestEntries;
|
||||
}
|
||||
|
||||
function selectManifestEntry(
|
||||
manifestEntries: ManifestEntry[],
|
||||
manifestUrl: string,
|
||||
version: string,
|
||||
arch: string,
|
||||
platform: string,
|
||||
): ManifestEntry | undefined {
|
||||
const matches = manifestEntries.filter(
|
||||
(candidate) =>
|
||||
candidate.version === version &&
|
||||
candidate.arch === arch &&
|
||||
candidate.platform === platform,
|
||||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectDefaultVariant(
|
||||
matches,
|
||||
`manifest-file ${manifestUrl} contains multiple artifacts for version ${version}, arch ${arch}, platform ${platform}`,
|
||||
);
|
||||
}
|
||||
|
||||
function tryParseJson(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
core.debug(`Updating manifest-file: ${JSON.stringify(manifest)}`);
|
||||
await fs.writeFile(manifestUrl, JSON.stringify(manifest));
|
||||
}
|
||||
|
||||
191
src/download/versions-client.ts
Normal file
191
src/download/versions-client.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as core from "@actions/core";
|
||||
import { VERSIONS_NDJSON_URL } from "../utils/constants";
|
||||
import { fetch } from "../utils/fetch";
|
||||
import { selectDefaultVariant } from "./variant-selection";
|
||||
|
||||
export interface NdjsonArtifact {
|
||||
platform: string;
|
||||
variant?: string;
|
||||
url: string;
|
||||
archive_format: string;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface NdjsonVersion {
|
||||
version: string;
|
||||
artifacts: NdjsonArtifact[];
|
||||
}
|
||||
|
||||
export interface ArtifactResult {
|
||||
url: string;
|
||||
sha256: string;
|
||||
archiveFormat: string;
|
||||
}
|
||||
|
||||
const cachedVersionData = new Map<string, NdjsonVersion[]>();
|
||||
|
||||
export async function fetchVersionData(
|
||||
url: string = VERSIONS_NDJSON_URL,
|
||||
): Promise<NdjsonVersion[]> {
|
||||
const cachedVersions = cachedVersionData.get(url);
|
||||
if (cachedVersions !== undefined) {
|
||||
core.debug(`Using cached NDJSON version data from ${url}`);
|
||||
return cachedVersions;
|
||||
}
|
||||
|
||||
core.info(`Fetching version data from ${url} ...`);
|
||||
const response = await fetch(url, {});
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch version data: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const body = await response.text();
|
||||
const versions = parseVersionData(body, url);
|
||||
cachedVersionData.set(url, versions);
|
||||
return versions;
|
||||
}
|
||||
|
||||
export function parseVersionData(
|
||||
data: string,
|
||||
sourceDescription: string,
|
||||
): NdjsonVersion[] {
|
||||
const versions: NdjsonVersion[] = [];
|
||||
|
||||
for (const [index, line] of data.split("\n").entries()) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(trimmed);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse version data from ${sourceDescription} at line ${index + 1}: ${(error as Error).message}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNdjsonVersion(parsed)) {
|
||||
throw new Error(
|
||||
`Invalid NDJSON record in ${sourceDescription} at line ${index + 1}.`,
|
||||
);
|
||||
}
|
||||
|
||||
versions.push(parsed);
|
||||
}
|
||||
|
||||
if (versions.length === 0) {
|
||||
throw new Error(`No version data found in ${sourceDescription}.`);
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
export async function getLatestVersion(): Promise<string> {
|
||||
const versions = await fetchVersionData();
|
||||
const latestVersion = versions[0]?.version;
|
||||
if (!latestVersion) {
|
||||
throw new Error("No versions found in NDJSON data");
|
||||
}
|
||||
|
||||
core.debug(`Latest version from NDJSON: ${latestVersion}`);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
export async function getAllVersions(): Promise<string[]> {
|
||||
const versions = await fetchVersionData();
|
||||
return versions.map((versionData) => versionData.version);
|
||||
}
|
||||
|
||||
export async function getArtifact(
|
||||
version: string,
|
||||
arch: string,
|
||||
platform: string,
|
||||
): Promise<ArtifactResult | undefined> {
|
||||
const versions = await fetchVersionData();
|
||||
const versionData = versions.find(
|
||||
(candidate) => candidate.version === version,
|
||||
);
|
||||
if (!versionData) {
|
||||
core.debug(`Version ${version} not found in NDJSON data`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetPlatform = `${arch}-${platform}`;
|
||||
const matchingArtifacts = versionData.artifacts.filter(
|
||||
(candidate) => candidate.platform === targetPlatform,
|
||||
);
|
||||
|
||||
if (matchingArtifacts.length === 0) {
|
||||
core.debug(
|
||||
`Artifact for ${targetPlatform} not found in version ${version}. Available platforms: ${versionData.artifacts
|
||||
.map((candidate) => candidate.platform)
|
||||
.join(", ")}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const artifact = selectArtifact(matchingArtifacts, version, targetPlatform);
|
||||
|
||||
return {
|
||||
archiveFormat: artifact.archive_format,
|
||||
sha256: artifact.sha256,
|
||||
url: artifact.url,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearCache(url?: string): void {
|
||||
if (url === undefined) {
|
||||
cachedVersionData.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
cachedVersionData.delete(url);
|
||||
}
|
||||
|
||||
function selectArtifact(
|
||||
artifacts: NdjsonArtifact[],
|
||||
version: string,
|
||||
targetPlatform: string,
|
||||
): NdjsonArtifact {
|
||||
return selectDefaultVariant(
|
||||
artifacts,
|
||||
`Multiple artifacts found for ${targetPlatform} in version ${version}`,
|
||||
);
|
||||
}
|
||||
|
||||
function isNdjsonVersion(value: unknown): value is NdjsonVersion {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value.version !== "string" || !Array.isArray(value.artifacts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.artifacts.every(isNdjsonArtifact);
|
||||
}
|
||||
|
||||
function isNdjsonArtifact(value: unknown): value is NdjsonArtifact {
|
||||
if (!isRecord(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const variantIsValid =
|
||||
typeof value.variant === "string" || value.variant === undefined;
|
||||
|
||||
return (
|
||||
typeof value.archive_format === "string" &&
|
||||
typeof value.platform === "string" &&
|
||||
typeof value.sha256 === "string" &&
|
||||
typeof value.url === "string" &&
|
||||
variantIsValid
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import * as exec from "@actions/exec";
|
||||
import { restoreCache } from "./cache/restore-cache";
|
||||
import {
|
||||
downloadVersionFromManifest,
|
||||
downloadVersionFromNdjson,
|
||||
resolveVersion,
|
||||
tryGetFromToolCache,
|
||||
} from "./download/download-version";
|
||||
@@ -139,14 +140,23 @@ async function setupUv(
|
||||
};
|
||||
}
|
||||
|
||||
const downloadVersionResult = await downloadVersionFromManifest(
|
||||
manifestFile,
|
||||
platform,
|
||||
arch,
|
||||
resolvedVersion,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
const downloadVersionResult =
|
||||
manifestFile !== undefined
|
||||
? await downloadVersionFromManifest(
|
||||
manifestFile,
|
||||
platform,
|
||||
arch,
|
||||
resolvedVersion,
|
||||
checkSum,
|
||||
githubToken,
|
||||
)
|
||||
: await downloadVersionFromNdjson(
|
||||
platform,
|
||||
arch,
|
||||
resolvedVersion,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
|
||||
return {
|
||||
uvDir: downloadVersionResult.cachedToolDir,
|
||||
@@ -158,12 +168,7 @@ async function determineVersion(
|
||||
manifestFile: string | undefined,
|
||||
): Promise<string> {
|
||||
if (versionInput !== "") {
|
||||
return await resolveVersion(
|
||||
versionInput,
|
||||
manifestFile,
|
||||
githubToken,
|
||||
resolutionStrategy,
|
||||
);
|
||||
return await resolveVersion(versionInput, manifestFile, resolutionStrategy);
|
||||
}
|
||||
if (versionFileInput !== "") {
|
||||
const versionFromFile = getUvVersionFromFile(versionFileInput);
|
||||
@@ -175,7 +180,6 @@ async function determineVersion(
|
||||
return await resolveVersion(
|
||||
versionFromFile,
|
||||
manifestFile,
|
||||
githubToken,
|
||||
resolutionStrategy,
|
||||
);
|
||||
}
|
||||
@@ -193,7 +197,6 @@ async function determineVersion(
|
||||
return await resolveVersion(
|
||||
versionFromUvToml || versionFromPyproject || "latest",
|
||||
manifestFile,
|
||||
githubToken,
|
||||
resolutionStrategy,
|
||||
);
|
||||
}
|
||||
|
||||
81
src/update-known-checksums.ts
Normal file
81
src/update-known-checksums.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as semver from "semver";
|
||||
import { KNOWN_CHECKSUMS } from "./download/checksum/known-checksums";
|
||||
import {
|
||||
type ChecksumEntry,
|
||||
updateChecksums,
|
||||
} from "./download/checksum/update-known-checksums";
|
||||
import {
|
||||
fetchVersionData,
|
||||
getLatestVersion,
|
||||
type NdjsonVersion,
|
||||
} from "./download/versions-client";
|
||||
|
||||
const VERSION_IN_CHECKSUM_KEY_PATTERN =
|
||||
/-(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/;
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const checksumFilePath = process.argv.slice(2)[0];
|
||||
if (!checksumFilePath) {
|
||||
throw new Error(
|
||||
"Missing checksum file path. Usage: node dist/update-known-checksums/index.js <checksum-file-path>",
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestVersion();
|
||||
const latestKnownVersion = getLatestKnownVersionFromChecksums();
|
||||
|
||||
if (semver.lte(latestVersion, latestKnownVersion)) {
|
||||
core.info(
|
||||
`Latest release (${latestVersion}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const versions = await fetchVersionData();
|
||||
const checksumEntries = extractChecksumsFromNdjson(versions);
|
||||
await updateChecksums(checksumFilePath, checksumEntries);
|
||||
|
||||
core.setOutput("latest-version", latestVersion);
|
||||
}
|
||||
|
||||
function getLatestKnownVersionFromChecksums(): string {
|
||||
const versions = new Set<string>();
|
||||
|
||||
for (const key of Object.keys(KNOWN_CHECKSUMS)) {
|
||||
const version = extractVersionFromChecksumKey(key);
|
||||
if (version !== undefined) {
|
||||
versions.add(version);
|
||||
}
|
||||
}
|
||||
|
||||
const latestVersion = [...versions].sort(semver.rcompare)[0];
|
||||
if (!latestVersion) {
|
||||
throw new Error("Could not determine latest known version from checksums.");
|
||||
}
|
||||
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
function extractVersionFromChecksumKey(key: string): string | undefined {
|
||||
return key.match(VERSION_IN_CHECKSUM_KEY_PATTERN)?.[1];
|
||||
}
|
||||
|
||||
function extractChecksumsFromNdjson(
|
||||
versions: NdjsonVersion[],
|
||||
): ChecksumEntry[] {
|
||||
const checksums: ChecksumEntry[] = [];
|
||||
|
||||
for (const version of versions) {
|
||||
for (const artifact of version.artifacts) {
|
||||
checksums.push({
|
||||
checksum: artifact.sha256,
|
||||
key: `${artifact.platform}-${version.version}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return checksums;
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,63 +0,0 @@
|
||||
import * as core from "@actions/core";
|
||||
import type { Endpoints } from "@octokit/types";
|
||||
import * as semver from "semver";
|
||||
import { updateChecksums } from "./download/checksum/update-known-checksums";
|
||||
import {
|
||||
getLatestKnownVersion,
|
||||
updateVersionManifest,
|
||||
} from "./download/version-manifest";
|
||||
import { OWNER, REPO } from "./utils/constants";
|
||||
import { Octokit } from "./utils/octokit";
|
||||
|
||||
type Release =
|
||||
Endpoints["GET /repos/{owner}/{repo}/releases"]["response"]["data"][number];
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const checksumFilePath = process.argv.slice(2)[0];
|
||||
const versionsManifestFile = process.argv.slice(2)[1];
|
||||
const githubToken = process.argv.slice(2)[2];
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: githubToken,
|
||||
});
|
||||
|
||||
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
});
|
||||
|
||||
const latestKnownVersion = await getLatestKnownVersion(undefined);
|
||||
|
||||
if (semver.lte(latestRelease.tag_name, latestKnownVersion)) {
|
||||
core.info(
|
||||
`Latest release (${latestRelease.tag_name}) is not newer than the latest known version (${latestKnownVersion}). Skipping update.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const releases: Release[] = await octokit.paginate(
|
||||
octokit.rest.repos.listReleases,
|
||||
{
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
},
|
||||
);
|
||||
const checksumDownloadUrls: string[] = releases.flatMap((release) =>
|
||||
release.assets
|
||||
.filter((asset) => asset.name.endsWith(".sha256"))
|
||||
.map((asset) => asset.browser_download_url),
|
||||
);
|
||||
await updateChecksums(checksumFilePath, checksumDownloadUrls);
|
||||
|
||||
const artifactDownloadUrls: string[] = releases.flatMap((release) =>
|
||||
release.assets
|
||||
.filter((asset) => !asset.name.endsWith(".sha256"))
|
||||
.map((asset) => asset.browser_download_url),
|
||||
);
|
||||
|
||||
await updateVersionManifest(versionsManifestFile, artifactDownloadUrls);
|
||||
|
||||
core.setOutput("latest-version", latestRelease.tag_name);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,5 +1,5 @@
|
||||
export const REPO = "uv";
|
||||
export const OWNER = "astral-sh";
|
||||
export const TOOL_CACHE_NAME = "uv";
|
||||
export const STATE_UV_PATH = "uv-path";
|
||||
export const STATE_UV_VERSION = "uv-version";
|
||||
export const VERSIONS_NDJSON_URL =
|
||||
"https://raw.githubusercontent.com/astral-sh/versions/main/v1/uv.ndjson";
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { OctokitOptions } from "@octokit/core";
|
||||
import { Octokit as Core } from "@octokit/core";
|
||||
import {
|
||||
type PaginateInterface,
|
||||
paginateRest,
|
||||
} from "@octokit/plugin-paginate-rest";
|
||||
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import { fetch as customFetch } from "./fetch";
|
||||
|
||||
export type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
|
||||
|
||||
const DEFAULTS = {
|
||||
baseUrl: "https://api.github.com",
|
||||
userAgent: "setup-uv",
|
||||
};
|
||||
|
||||
const OctokitWithPlugins = Core.plugin(paginateRest, legacyRestEndpointMethods);
|
||||
|
||||
export const Octokit = OctokitWithPlugins.defaults(function buildDefaults(
|
||||
options: OctokitOptions,
|
||||
): OctokitOptions {
|
||||
return {
|
||||
...DEFAULTS,
|
||||
...options,
|
||||
request: {
|
||||
fetch: customFetch,
|
||||
...options.request,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export type Octokit = InstanceType<typeof OctokitWithPlugins> & {
|
||||
paginate: PaginateInterface;
|
||||
};
|
||||
Reference in New Issue
Block a user