mirror astral-sh/setup-uv @ d0cc045d04ccac9d8b7881df0226f9e82c39688e
CodeQL / Analyze (TypeScript) (push) Failing after 1s
test / lint (push) Failing after 1s
test / test-default-version (ubuntu-latest) (push) Failing after 0s
test / test-specific-version (0.3) (push) Failing after 0s
test / test-specific-version (0.3.0) (push) Failing after 1s
test / test-specific-version (0.3.2) (push) Failing after 0s
test / test-specific-version (0.3.x) (push) Failing after 0s
test / test-specific-version (>=0.3.0) (push) Failing after 1s
test / test-semver-range (ubuntu-latest) (push) Failing after 1s
test / test-pep440-version (push) Failing after 0s
test / test-pyproject-file-version (push) Failing after 0s
test / test-malformed-pyproject-file-fallback (push) Failing after 0s
test / test-version-file-version (push) Failing after 0s
test / test-uv-file-version (push) Failing after 0s
test / test-version-file-hash-version (push) Failing after 0s
test / test-tool-versions-file-version (push) Failing after 0s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 1s
test / test-with-explicit-token (push) Failing after 0s
test / test-uvx (push) Failing after 1s
test / test-tool-install (ubuntu-latest) (push) Failing after 0s
test / test-python-version (ubuntu-latest) (push) Failing after 0s
test / test-activate-environment (ubuntu-latest) (push) Failing after 0s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 0s
test / test-musl (push) Failing after 0s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 0s
test / test-setup-cache-requirements-txt (push) Failing after 0s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 0s
test / test-restore-cache-requirements-txt (push) Skipped
test / test-setup-cache-save-cache-false (push) Failing after 0s
test / test-setup-cache-dependency-glob (push) Failing after 0s
test / test-restore-cache-dependency-glob (push) Skipped
test / test-restore-cache-save-cache-false (push) Skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 1s
test / test-setup-cache-restore-cache-false (push) Failing after 0s
test / test-restore-cache-restore-cache-false (push) Skipped
test / test-no-python-version (push) Failing after 0s
test / test-absolute-path (push) Failing after 0s
test / test-custom-manifest-file (push) Failing after 0s
test / test-relative-path (push) Failing after 0s
test / test-default-version (macos-14) (push) Canceled after 0s
test / test-default-version (macos-latest) (push) Canceled after 0s
test / test-default-version (windows-latest) (push) Canceled after 0s
test / test-semver-range (selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Canceled after 0s
test / test-tool-install (macos-14) (push) Canceled after 0s
test / test-tool-install (macos-latest) (push) Canceled after 0s
test / test-tool-install (windows-latest) (push) Canceled after 0s
test / test-tilde-expansion-tool-dirs (push) Canceled after 0s
test / test-python-version (macos-latest) (push) Canceled after 0s
test / test-python-version (windows-latest) (push) Canceled after 0s
test / test-activate-environment (macos-latest) (push) Canceled after 0s
test / test-activate-environment (windows-latest) (push) Canceled after 0s
test / test-setup-cache (auto, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (auto, windows-latest) (push) Canceled after 0s
test / test-setup-cache (false, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (false, windows-latest) (push) Canceled after 0s
test / test-setup-cache (true, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (true, windows-latest) (push) Canceled after 0s
test / test-restore-cache (auto, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (auto, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (auto, windows-latest) (push) Canceled after 0s
test / test-restore-cache (false, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (false, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (false, windows-latest) (push) Canceled after 0s
test / test-restore-cache (true, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (true, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (true, windows-latest) (push) Canceled after 0s
test / test-cache-local (map[expected-cache-dir:/home/ubuntu/.cache/uv os:selfhosted-ubuntu-arm64]) (push) Canceled after 0s
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Canceled after 0s
test / test-setup-cache-local (push) Canceled after 0s
test / test-restore-cache-local (push) Canceled after 0s
test / test-tilde-expansion-cache-local-path (push) Canceled after 0s
test / test-tilde-expansion-cache-dependency-glob (push) Canceled after 0s
test / cleanup-tilde-expansion-tests (push) Canceled after 0s
test / all-tests-passed (push) Canceled after 0s
Release Drafter / ✏️ Draft release (push) Canceled after 0s
CodeQL / Analyze (TypeScript) (push) Failing after 1s
test / lint (push) Failing after 1s
test / test-default-version (ubuntu-latest) (push) Failing after 0s
test / test-specific-version (0.3) (push) Failing after 0s
test / test-specific-version (0.3.0) (push) Failing after 1s
test / test-specific-version (0.3.2) (push) Failing after 0s
test / test-specific-version (0.3.x) (push) Failing after 0s
test / test-specific-version (>=0.3.0) (push) Failing after 1s
test / test-semver-range (ubuntu-latest) (push) Failing after 1s
test / test-pep440-version (push) Failing after 0s
test / test-pyproject-file-version (push) Failing after 0s
test / test-malformed-pyproject-file-fallback (push) Failing after 0s
test / test-version-file-version (push) Failing after 0s
test / test-uv-file-version (push) Failing after 0s
test / test-version-file-hash-version (push) Failing after 0s
test / test-tool-versions-file-version (push) Failing after 0s
test / test-checksum (map[checksum:4d9279ad5ca596b1e2d703901d508430eb07564dc4d8837de9e2fca9c90f8ecd os:ubuntu-latest]) (push) Failing after 1s
test / test-with-explicit-token (push) Failing after 0s
test / test-uvx (push) Failing after 1s
test / test-tool-install (ubuntu-latest) (push) Failing after 0s
test / test-python-version (ubuntu-latest) (push) Failing after 0s
test / test-activate-environment (ubuntu-latest) (push) Failing after 0s
test / test-setup-cache (auto, ubuntu-latest) (push) Failing after 0s
test / test-musl (push) Failing after 0s
test / test-setup-cache (true, ubuntu-latest) (push) Failing after 0s
test / test-setup-cache-requirements-txt (push) Failing after 0s
test / test-setup-cache (false, ubuntu-latest) (push) Failing after 0s
test / test-restore-cache-requirements-txt (push) Skipped
test / test-setup-cache-save-cache-false (push) Failing after 0s
test / test-setup-cache-dependency-glob (push) Failing after 0s
test / test-restore-cache-dependency-glob (push) Skipped
test / test-restore-cache-save-cache-false (push) Skipped
test / test-cache-local (map[expected-cache-dir:/home/runner/work/_temp/setup-uv-cache os:ubuntu-latest]) (push) Failing after 1s
test / test-setup-cache-restore-cache-false (push) Failing after 0s
test / test-restore-cache-restore-cache-false (push) Skipped
test / test-no-python-version (push) Failing after 0s
test / test-absolute-path (push) Failing after 0s
test / test-custom-manifest-file (push) Failing after 0s
test / test-relative-path (push) Failing after 0s
test / test-default-version (macos-14) (push) Canceled after 0s
test / test-default-version (macos-latest) (push) Canceled after 0s
test / test-default-version (windows-latest) (push) Canceled after 0s
test / test-semver-range (selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-checksum (map[checksum:a70cbfbf3bb5c08b2f84963b4f12c94e08fbb2468ba418a3bfe1066fbe9e7218 os:macos-latest]) (push) Canceled after 0s
test / test-tool-install (macos-14) (push) Canceled after 0s
test / test-tool-install (macos-latest) (push) Canceled after 0s
test / test-tool-install (windows-latest) (push) Canceled after 0s
test / test-tilde-expansion-tool-dirs (push) Canceled after 0s
test / test-python-version (macos-latest) (push) Canceled after 0s
test / test-python-version (windows-latest) (push) Canceled after 0s
test / test-activate-environment (macos-latest) (push) Canceled after 0s
test / test-activate-environment (windows-latest) (push) Canceled after 0s
test / test-setup-cache (auto, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (auto, windows-latest) (push) Canceled after 0s
test / test-setup-cache (false, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (false, windows-latest) (push) Canceled after 0s
test / test-setup-cache (true, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-setup-cache (true, windows-latest) (push) Canceled after 0s
test / test-restore-cache (auto, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (auto, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (auto, windows-latest) (push) Canceled after 0s
test / test-restore-cache (false, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (false, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (false, windows-latest) (push) Canceled after 0s
test / test-restore-cache (true, selfhosted-ubuntu-arm64) (push) Canceled after 0s
test / test-restore-cache (true, ubuntu-latest) (push) Canceled after 0s
test / test-restore-cache (true, windows-latest) (push) Canceled after 0s
test / test-cache-local (map[expected-cache-dir:/home/ubuntu/.cache/uv os:selfhosted-ubuntu-arm64]) (push) Canceled after 0s
test / test-cache-local (map[expected-cache-dir:D:\a\_temp\setup-uv-cache os:windows-latest]) (push) Canceled after 0s
test / test-setup-cache-local (push) Canceled after 0s
test / test-restore-cache-local (push) Canceled after 0s
test / test-tilde-expansion-cache-local-path (push) Canceled after 0s
test / test-tilde-expansion-cache-dependency-glob (push) Canceled after 0s
test / cleanup-tilde-expansion-tests (push) Canceled after 0s
test / all-tests-passed (push) Canceled after 0s
Release Drafter / ✏️ Draft release (push) Canceled after 0s
This commit is contained in:
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { hashFiles } from "../hash/hash-files";
|
||||
import {
|
||||
cacheDependencyGlob,
|
||||
cacheLocalPath,
|
||||
cacheSuffix,
|
||||
pruneCache,
|
||||
pythonVersion as pythonVersionInput,
|
||||
restoreCache as shouldRestoreCache,
|
||||
workingDirectory,
|
||||
} from "../utils/inputs";
|
||||
import { getArch, getPlatform } from "../utils/platforms";
|
||||
|
||||
export const STATE_CACHE_KEY = "cache-key";
|
||||
export const STATE_CACHE_MATCHED_KEY = "cache-matched-key";
|
||||
const CACHE_VERSION = "1";
|
||||
|
||||
export async function restoreCache(): Promise<void> {
|
||||
const cacheKey = await computeKeys();
|
||||
core.saveState(STATE_CACHE_KEY, cacheKey);
|
||||
|
||||
if (!shouldRestoreCache) {
|
||||
core.info("restore-cache is false. Skipping restore cache step.");
|
||||
return;
|
||||
}
|
||||
|
||||
let matchedKey: string | undefined;
|
||||
core.info(
|
||||
`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`,
|
||||
);
|
||||
try {
|
||||
matchedKey = await cache.restoreCache([cacheLocalPath], cacheKey);
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
core.warning(message);
|
||||
core.setOutput("cache-hit", false);
|
||||
return;
|
||||
}
|
||||
|
||||
handleMatchResult(matchedKey, cacheKey);
|
||||
}
|
||||
|
||||
async function computeKeys(): Promise<string> {
|
||||
let cacheDependencyPathHash = "-";
|
||||
if (cacheDependencyGlob !== "") {
|
||||
core.info(
|
||||
`Searching files using cache dependency glob: ${cacheDependencyGlob.split("\n").join(",")}`,
|
||||
);
|
||||
cacheDependencyPathHash += await hashFiles(cacheDependencyGlob, true);
|
||||
if (cacheDependencyPathHash === "-") {
|
||||
core.warning(
|
||||
`No file matched to [${cacheDependencyGlob.split("\n").join(",")}]. The cache will never get invalidated. Make sure you have checked out the target repository and configured the cache-dependency-glob input correctly.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (cacheDependencyPathHash === "-") {
|
||||
cacheDependencyPathHash = "-no-dependency-glob";
|
||||
}
|
||||
const suffix = cacheSuffix ? `-${cacheSuffix}` : "";
|
||||
const pythonVersion = await getPythonVersion();
|
||||
const platform = await getPlatform();
|
||||
const pruned = pruneCache ? "-pruned" : "";
|
||||
return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`;
|
||||
}
|
||||
|
||||
async function getPythonVersion(): Promise<string> {
|
||||
if (pythonVersionInput !== "") {
|
||||
return pythonVersionInput;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const options: exec.ExecOptions = {
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => {
|
||||
output += data.toString();
|
||||
},
|
||||
},
|
||||
silent: !core.isDebug(),
|
||||
};
|
||||
|
||||
try {
|
||||
const execArgs = ["python", "find", "--directory", workingDirectory];
|
||||
await exec.exec("uv", execArgs, options);
|
||||
const pythonPath = output.trim();
|
||||
|
||||
output = "";
|
||||
await exec.exec(pythonPath, ["--version"], options);
|
||||
// output is like "Python 3.8.10"
|
||||
return output.split(" ")[1].trim();
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
core.debug(`Failed to get python version from uv. Error: ${err.message}`);
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function handleMatchResult(
|
||||
matchedKey: string | undefined,
|
||||
primaryKey: string,
|
||||
): void {
|
||||
if (!matchedKey) {
|
||||
core.info(`No GitHub Actions cache found for key: ${primaryKey}`);
|
||||
core.setOutput("cache-hit", false);
|
||||
return;
|
||||
}
|
||||
|
||||
core.saveState(STATE_CACHE_MATCHED_KEY, matchedKey);
|
||||
core.info(
|
||||
`uv cache restored from GitHub Actions cache with key: ${matchedKey}`,
|
||||
);
|
||||
core.setOutput("cache-hit", true);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import type { Architecture, Platform } from "../../utils/platforms";
|
||||
import { KNOWN_CHECKSUMS } from "./known-checksums";
|
||||
|
||||
export async function validateChecksum(
|
||||
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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid === false) {
|
||||
throw new Error(`Checksum for ${downloadPath} did not match ${checkSum}.`);
|
||||
}
|
||||
if (isValid === true) {
|
||||
core.debug(`Checksum for ${downloadPath} is valid.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateFileCheckSum(
|
||||
filePath: string,
|
||||
expected: string,
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash("sha256");
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on("error", (err) => reject(err));
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("end", () => {
|
||||
const actual = hash.digest("hex");
|
||||
resolve(actual === expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function isknownVersion(version: string): boolean {
|
||||
const pattern = new RegExp(`^.*-.*-${version}$`);
|
||||
return Object.keys(KNOWN_CHECKSUMS).some((key) => pattern.test(key));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import * as tc from "@actions/tool-cache";
|
||||
import { KNOWN_CHECKSUMS } from "./known-checksums";
|
||||
export async function updateChecksums(
|
||||
filePath: string,
|
||||
downloadUrls: string[],
|
||||
): 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) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
async function downloadAssetContent(downloadUrl: string): Promise<string> {
|
||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||
const content = await fs.readFile(downloadPath, "utf8");
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
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 * as pep440 from "@renovatebot/pep440";
|
||||
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
|
||||
import { Octokit } from "../utils/octokit";
|
||||
import type { Architecture, Platform } from "../utils/platforms";
|
||||
import { validateChecksum } from "./checksum/checksum";
|
||||
import {
|
||||
getDownloadUrl,
|
||||
getLatestKnownVersion as getLatestVersionInManifest,
|
||||
} from "./version-manifest";
|
||||
|
||||
export function tryGetFromToolCache(
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
): { version: string; installedPath: string | undefined } {
|
||||
core.debug(`Trying to get uv from tool cache for ${version}...`);
|
||||
const cachedVersions = tc.findAllVersions(TOOL_CACHE_NAME, arch);
|
||||
core.debug(`Cached versions: ${cachedVersions}`);
|
||||
let resolvedVersion = tc.evaluateVersions(cachedVersions, version);
|
||||
if (resolvedVersion === "") {
|
||||
resolvedVersion = version;
|
||||
}
|
||||
const installedPath = tc.find(TOOL_CACHE_NAME, resolvedVersion, arch);
|
||||
return { installedPath, version: resolvedVersion };
|
||||
}
|
||||
|
||||
export async function downloadVersionFromGithub(
|
||||
serverUrl: string,
|
||||
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 = `${serverUrl}/${OWNER}/${REPO}/releases/download/${version}/${artifact}${extension}`;
|
||||
return await downloadVersion(
|
||||
downloadUrl,
|
||||
artifact,
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadVersionFromManifest(
|
||||
manifestUrl: string | undefined,
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
checkSum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ version: string; cachedToolDir: string }> {
|
||||
const downloadUrl = await getDownloadUrl(
|
||||
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(
|
||||
"https://github.com",
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
return await downloadVersion(
|
||||
downloadUrl,
|
||||
`uv-${arch}-${platform}`,
|
||||
platform,
|
||||
arch,
|
||||
version,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadVersion(
|
||||
downloadUrl: string,
|
||||
artifactName: string,
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
version: string,
|
||||
checkSum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ version: string; cachedToolDir: string }> {
|
||||
core.info(`Downloading uv from "${downloadUrl}" ...`);
|
||||
const downloadPath = await tc.downloadTool(
|
||||
downloadUrl,
|
||||
undefined,
|
||||
githubToken,
|
||||
);
|
||||
await validateChecksum(checkSum, downloadPath, arch, platform, version);
|
||||
|
||||
let uvDir: string;
|
||||
const extension = getExtension(platform);
|
||||
if (platform === "pc-windows-msvc") {
|
||||
const fullPathWithExtension = `${downloadPath}${extension}`;
|
||||
await fs.copyFile(downloadPath, fullPathWithExtension);
|
||||
uvDir = await tc.extractZip(fullPathWithExtension);
|
||||
// On windows extracting the zip does not create an intermediate directory
|
||||
} else {
|
||||
const extractedDir = await tc.extractTar(downloadPath);
|
||||
uvDir = path.join(extractedDir, artifactName);
|
||||
}
|
||||
const cachedToolDir = await tc.cacheDir(
|
||||
uvDir,
|
||||
TOOL_CACHE_NAME,
|
||||
version,
|
||||
arch,
|
||||
);
|
||||
return { cachedToolDir, version: version };
|
||||
}
|
||||
|
||||
function getExtension(platform: Platform): string {
|
||||
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
|
||||
}
|
||||
|
||||
export async function resolveVersion(
|
||||
versionInput: string,
|
||||
manifestFile: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<string> {
|
||||
core.debug(`Resolving version: ${versionInput}`);
|
||||
let version: string;
|
||||
if (manifestFile) {
|
||||
version =
|
||||
versionInput === "latest"
|
||||
? await getLatestVersionInManifest(manifestFile)
|
||||
: versionInput;
|
||||
} else {
|
||||
version =
|
||||
versionInput === "latest"
|
||||
? await getLatestVersion(githubToken)
|
||||
: versionInput;
|
||||
}
|
||||
if (tc.isExplicitVersion(version)) {
|
||||
core.debug(`Version ${version} is an explicit version.`);
|
||||
return version;
|
||||
}
|
||||
const availableVersions = await getAvailableVersions(githubToken);
|
||||
core.debug(`Available versions: ${availableVersions}`);
|
||||
const resolvedVersion = 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: InstanceType<typeof Octokit>,
|
||||
): Promise<string[]> {
|
||||
const response = 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.",
|
||||
);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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: InstanceType<typeof Octokit>) {
|
||||
const { data: latestRelease } = await octokit.rest.repos.getLatestRelease({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
});
|
||||
return latestRelease;
|
||||
}
|
||||
|
||||
function maxSatisfying(
|
||||
versions: string[],
|
||||
version: string,
|
||||
): string | undefined {
|
||||
const maxSemver = tc.evaluateVersions(versions, version);
|
||||
if (maxSemver !== "") {
|
||||
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||
return maxSemver;
|
||||
}
|
||||
const maxPep440 = pep440.maxSatisfying(versions, version);
|
||||
if (maxPep440 !== null) {
|
||||
core.debug(
|
||||
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
|
||||
);
|
||||
return maxPep440;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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";
|
||||
|
||||
const localManifestFile = join(__dirname, "..", "..", "version-manifest.json");
|
||||
|
||||
interface ManifestEntry {
|
||||
version: string;
|
||||
artifactName: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
downloadUrl: string;
|
||||
}
|
||||
|
||||
export async function getLatestKnownVersion(
|
||||
manifestUrl: string | undefined,
|
||||
): Promise<string> {
|
||||
const manifestEntries = await getManifestEntries(manifestUrl);
|
||||
return manifestEntries.reduce((a, b) =>
|
||||
semver.gt(a.version, b.version) ? a : b,
|
||||
).version;
|
||||
}
|
||||
|
||||
export async function getDownloadUrl(
|
||||
manifestUrl: string | undefined,
|
||||
version: string,
|
||||
arch: string,
|
||||
platform: string,
|
||||
): Promise<string | undefined> {
|
||||
const manifestEntries = await getManifestEntries(manifestUrl);
|
||||
const entry = manifestEntries.find(
|
||||
(entry) =>
|
||||
entry.version === version &&
|
||||
entry.arch === arch &&
|
||||
entry.platform === platform,
|
||||
);
|
||||
return entry ? entry.downloadUrl : undefined;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
core.debug(`Updating manifest-file: ${JSON.stringify(manifest)}`);
|
||||
await fs.writeFile(manifestUrl, JSON.stringify(manifest));
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as crypto from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as stream from "node:stream";
|
||||
import * as util from "node:util";
|
||||
import * as core from "@actions/core";
|
||||
import { create } from "@actions/glob";
|
||||
|
||||
/**
|
||||
* Hashes files matching the given glob pattern.
|
||||
*
|
||||
* Copied from https://github.com/actions/toolkit/blob/20ed2908f19538e9dfb66d8083f1171c0a50a87c/packages/glob/src/internal-hash-files.ts#L9-L49
|
||||
* But supports hashing files outside the GITHUB_WORKSPACE.
|
||||
* @param pattern The glob pattern to match files.
|
||||
* @param verbose Whether to log the files being hashed.
|
||||
*/
|
||||
export async function hashFiles(
|
||||
pattern: string,
|
||||
verbose = false,
|
||||
): Promise<string> {
|
||||
const globber = await create(pattern);
|
||||
let hasMatch = false;
|
||||
const writeDelegate = verbose ? core.info : core.debug;
|
||||
const result = crypto.createHash("sha256");
|
||||
let count = 0;
|
||||
for await (const file of globber.globGenerator()) {
|
||||
writeDelegate(file);
|
||||
if (fs.statSync(file).isDirectory()) {
|
||||
writeDelegate(`Skip directory '${file}'.`);
|
||||
continue;
|
||||
}
|
||||
const hash = crypto.createHash("sha256");
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
await pipeline(fs.createReadStream(file), hash);
|
||||
result.write(hash.digest());
|
||||
count++;
|
||||
if (!hasMatch) {
|
||||
hasMatch = true;
|
||||
}
|
||||
}
|
||||
result.end();
|
||||
|
||||
if (hasMatch) {
|
||||
writeDelegate(`Found ${count} files to hash.`);
|
||||
return result.digest("hex");
|
||||
}
|
||||
writeDelegate("No matches found for glob");
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import {
|
||||
STATE_CACHE_KEY,
|
||||
STATE_CACHE_MATCHED_KEY,
|
||||
} from "./cache/restore-cache";
|
||||
import {
|
||||
cacheLocalPath,
|
||||
enableCache,
|
||||
ignoreNothingToCache,
|
||||
pruneCache as shouldPruneCache,
|
||||
saveCache as shouldSaveCache,
|
||||
} from "./utils/inputs";
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
try {
|
||||
if (enableCache) {
|
||||
if (shouldSaveCache) {
|
||||
await saveCache();
|
||||
} else {
|
||||
core.info("save-cache is false. Skipping save cache step.");
|
||||
}
|
||||
// node will stay alive if any promises are not resolved,
|
||||
// which is a possibility if HTTP requests are dangling
|
||||
// due to retries or timeouts. We know that if we got here
|
||||
// that all promises that we care about have successfully
|
||||
// resolved, so simply exit with success.
|
||||
process.exit(0);
|
||||
}
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
core.setFailed(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCache(): Promise<void> {
|
||||
const cacheKey = core.getState(STATE_CACHE_KEY);
|
||||
const matchedKey = core.getState(STATE_CACHE_MATCHED_KEY);
|
||||
|
||||
if (!cacheKey) {
|
||||
core.warning("Error retrieving cache key from state.");
|
||||
return;
|
||||
}
|
||||
if (matchedKey === cacheKey) {
|
||||
core.info(`Cache hit occurred on key ${cacheKey}, not saving cache.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldPruneCache) {
|
||||
await pruneCache();
|
||||
}
|
||||
|
||||
core.info(`Saving cache path: ${cacheLocalPath}`);
|
||||
if (!fs.existsSync(cacheLocalPath) && !ignoreNothingToCache) {
|
||||
throw new Error(
|
||||
`Cache path ${cacheLocalPath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
await cache.saveCache([cacheLocalPath], cacheKey);
|
||||
core.info(`cache saved with the key: ${cacheKey}`);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
e.message ===
|
||||
"Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved."
|
||||
) {
|
||||
core.info(
|
||||
"No cacheable paths were found. Ignoring because ignore-nothing-to-save is enabled.",
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function pruneCache(): Promise<void> {
|
||||
const options: exec.ExecOptions = {
|
||||
silent: false,
|
||||
};
|
||||
const execArgs = ["cache", "prune", "--ci"];
|
||||
|
||||
core.info("Pruning cache...");
|
||||
await exec.exec("uv", execArgs, options);
|
||||
}
|
||||
|
||||
run();
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import { restoreCache } from "./cache/restore-cache";
|
||||
import {
|
||||
downloadVersionFromGithub,
|
||||
downloadVersionFromManifest,
|
||||
resolveVersion,
|
||||
tryGetFromToolCache,
|
||||
} from "./download/download-version";
|
||||
import {
|
||||
activateEnvironment as activateEnvironmentInput,
|
||||
addProblemMatchers,
|
||||
cacheLocalPath,
|
||||
checkSum,
|
||||
enableCache,
|
||||
githubToken,
|
||||
ignoreEmptyWorkdir,
|
||||
manifestFile,
|
||||
pythonVersion,
|
||||
serverUrl,
|
||||
toolBinDir,
|
||||
toolDir,
|
||||
versionFile as versionFileInput,
|
||||
version as versionInput,
|
||||
workingDirectory,
|
||||
} from "./utils/inputs";
|
||||
import {
|
||||
type Architecture,
|
||||
getArch,
|
||||
getPlatform,
|
||||
type Platform,
|
||||
} from "./utils/platforms";
|
||||
import { getUvVersionFromFile } from "./version/resolve";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
detectEmptyWorkdir();
|
||||
const platform = await getPlatform();
|
||||
const arch = getArch();
|
||||
|
||||
try {
|
||||
if (platform === undefined) {
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
if (arch === undefined) {
|
||||
throw new Error(`Unsupported architecture: ${process.arch}`);
|
||||
}
|
||||
const setupResult = await setupUv(platform, arch, checkSum, githubToken);
|
||||
|
||||
addToolBinToPath();
|
||||
addUvToPathAndOutput(setupResult.uvDir);
|
||||
setToolDir();
|
||||
setupPython();
|
||||
await activateEnvironment();
|
||||
addMatchers();
|
||||
setCacheDir(cacheLocalPath);
|
||||
|
||||
core.setOutput("uv-version", setupResult.version);
|
||||
core.info(`Successfully installed uv version ${setupResult.version}`);
|
||||
|
||||
if (enableCache) {
|
||||
await restoreCache();
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
core.setFailed((err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function detectEmptyWorkdir(): void {
|
||||
if (fs.readdirSync(".").length === 0) {
|
||||
if (ignoreEmptyWorkdir) {
|
||||
core.info(
|
||||
"Empty workdir detected. Ignoring because ignore-empty-workdir is enabled",
|
||||
);
|
||||
} else {
|
||||
core.warning(
|
||||
"Empty workdir detected. This may cause unexpected behavior. You can enable ignore-empty-workdir to mute this warning.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupUv(
|
||||
platform: Platform,
|
||||
arch: Architecture,
|
||||
checkSum: string | undefined,
|
||||
githubToken: string,
|
||||
): Promise<{ uvDir: string; version: string }> {
|
||||
const resolvedVersion = await determineVersion(manifestFile);
|
||||
const toolCacheResult = tryGetFromToolCache(arch, resolvedVersion);
|
||||
if (toolCacheResult.installedPath) {
|
||||
core.info(`Found uv in tool-cache for ${toolCacheResult.version}`);
|
||||
return {
|
||||
uvDir: toolCacheResult.installedPath,
|
||||
version: toolCacheResult.version,
|
||||
};
|
||||
}
|
||||
|
||||
let downloadVersionResult: { version: string; cachedToolDir: string };
|
||||
if (serverUrl !== "https://github.com") {
|
||||
core.warning(
|
||||
"The input server-url is deprecated. Please use manifest-file instead.",
|
||||
);
|
||||
downloadVersionResult = await downloadVersionFromGithub(
|
||||
serverUrl,
|
||||
platform,
|
||||
arch,
|
||||
resolvedVersion,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
} else {
|
||||
downloadVersionResult = await downloadVersionFromManifest(
|
||||
manifestFile,
|
||||
platform,
|
||||
arch,
|
||||
resolvedVersion,
|
||||
checkSum,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
uvDir: downloadVersionResult.cachedToolDir,
|
||||
version: downloadVersionResult.version,
|
||||
};
|
||||
}
|
||||
|
||||
async function determineVersion(
|
||||
manifestFile: string | undefined,
|
||||
): Promise<string> {
|
||||
if (versionInput !== "") {
|
||||
return await resolveVersion(versionInput, manifestFile, githubToken);
|
||||
}
|
||||
if (versionFileInput !== "") {
|
||||
const versionFromFile = getUvVersionFromFile(versionFileInput);
|
||||
if (versionFromFile === undefined) {
|
||||
throw new Error(
|
||||
`Could not determine uv version from file: ${versionFileInput}`,
|
||||
);
|
||||
}
|
||||
return await resolveVersion(versionFromFile, manifestFile, githubToken);
|
||||
}
|
||||
const versionFromUvToml = getUvVersionFromFile(
|
||||
`${workingDirectory}${path.sep}uv.toml`,
|
||||
);
|
||||
const versionFromPyproject = getUvVersionFromFile(
|
||||
`${workingDirectory}${path.sep}pyproject.toml`,
|
||||
);
|
||||
if (versionFromUvToml === undefined && versionFromPyproject === undefined) {
|
||||
core.info(
|
||||
"Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.",
|
||||
);
|
||||
}
|
||||
return await resolveVersion(
|
||||
versionFromUvToml || versionFromPyproject || "latest",
|
||||
manifestFile,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
function addUvToPathAndOutput(cachedPath: string): void {
|
||||
core.setOutput("uv-path", `${cachedPath}${path.sep}uv`);
|
||||
core.setOutput("uvx-path", `${cachedPath}${path.sep}uvx`);
|
||||
core.addPath(cachedPath);
|
||||
core.info(`Added ${cachedPath} to the path`);
|
||||
}
|
||||
|
||||
function addToolBinToPath(): void {
|
||||
if (toolBinDir !== undefined) {
|
||||
core.exportVariable("UV_TOOL_BIN_DIR", toolBinDir);
|
||||
core.info(`Set UV_TOOL_BIN_DIR to ${toolBinDir}`);
|
||||
core.addPath(toolBinDir);
|
||||
core.info(`Added ${toolBinDir} to the path`);
|
||||
} else {
|
||||
if (process.env.XDG_BIN_HOME !== undefined) {
|
||||
core.addPath(process.env.XDG_BIN_HOME);
|
||||
core.info(`Added ${process.env.XDG_BIN_HOME} to the path`);
|
||||
} else if (process.env.XDG_DATA_HOME !== undefined) {
|
||||
core.addPath(`${process.env.XDG_DATA_HOME}/../bin`);
|
||||
core.info(`Added ${process.env.XDG_DATA_HOME}/../bin to the path`);
|
||||
} else {
|
||||
core.addPath(`${process.env.HOME}/.local/bin`);
|
||||
core.info(`Added ${process.env.HOME}/.local/bin to the path`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setToolDir(): void {
|
||||
if (toolDir !== undefined) {
|
||||
core.exportVariable("UV_TOOL_DIR", toolDir);
|
||||
core.info(`Set UV_TOOL_DIR to ${toolDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
function setupPython(): void {
|
||||
if (pythonVersion !== "") {
|
||||
core.exportVariable("UV_PYTHON", pythonVersion);
|
||||
core.info(`Set UV_PYTHON to ${pythonVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function activateEnvironment(): Promise<void> {
|
||||
if (activateEnvironmentInput) {
|
||||
const execArgs = ["venv", ".venv", "--directory", workingDirectory];
|
||||
|
||||
core.info("Activating python venv...");
|
||||
await exec.exec("uv", execArgs);
|
||||
|
||||
let venvBinPath = `${workingDirectory}${path.sep}.venv${path.sep}bin`;
|
||||
if (process.platform === "win32") {
|
||||
venvBinPath = `${workingDirectory}${path.sep}.venv${path.sep}Scripts`;
|
||||
}
|
||||
core.addPath(path.resolve(venvBinPath));
|
||||
core.exportVariable(
|
||||
"VIRTUAL_ENV",
|
||||
path.resolve(`${workingDirectory}${path.sep}.venv`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setCacheDir(cacheLocalPath: string): void {
|
||||
core.exportVariable("UV_CACHE_DIR", cacheLocalPath);
|
||||
core.info(`Set UV_CACHE_DIR to ${cacheLocalPath}`);
|
||||
}
|
||||
|
||||
function addMatchers(): void {
|
||||
if (addProblemMatchers) {
|
||||
const matchersPath = path.join(__dirname, `..${path.sep}..`, ".github");
|
||||
core.info(`##[add-matcher]${path.join(matchersPath, "python.json")}`);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as core from "@actions/core";
|
||||
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";
|
||||
|
||||
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 = 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();
|
||||
@@ -0,0 +1,3 @@
|
||||
export const REPO = "uv";
|
||||
export const OWNER = "astral-sh";
|
||||
export const TOOL_CACHE_NAME = "uv";
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ProxyAgent, type RequestInit, fetch as undiciFetch } from "undici";
|
||||
|
||||
export function getProxyAgent() {
|
||||
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
||||
if (httpProxy) {
|
||||
return new ProxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
||||
if (httpsProxy) {
|
||||
return new ProxyAgent(httpsProxy);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const fetch = async (url: string, opts: RequestInit) =>
|
||||
await undiciFetch(url, {
|
||||
dispatcher: getProxyAgent(),
|
||||
...opts,
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import path from "node:path";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export const workingDirectory = core.getInput("working-directory");
|
||||
export const version = core.getInput("version");
|
||||
export const versionFile = getVersionFile();
|
||||
export const pythonVersion = core.getInput("python-version");
|
||||
export const activateEnvironment = core.getBooleanInput("activate-environment");
|
||||
export const checkSum = core.getInput("checksum");
|
||||
export const enableCache = getEnableCache();
|
||||
export const restoreCache = core.getInput("restore-cache") === "true";
|
||||
export const saveCache = core.getInput("save-cache") === "true";
|
||||
export const cacheSuffix = core.getInput("cache-suffix") || "";
|
||||
export const cacheLocalPath = getCacheLocalPath();
|
||||
export const cacheDependencyGlob = getCacheDependencyGlob();
|
||||
export const pruneCache = core.getInput("prune-cache") === "true";
|
||||
export const ignoreNothingToCache =
|
||||
core.getInput("ignore-nothing-to-cache") === "true";
|
||||
export const ignoreEmptyWorkdir =
|
||||
core.getInput("ignore-empty-workdir") === "true";
|
||||
export const toolBinDir = getToolBinDir();
|
||||
export const toolDir = getToolDir();
|
||||
export const serverUrl = core.getInput("server-url");
|
||||
export const githubToken = core.getInput("github-token");
|
||||
export const manifestFile = getManifestFile();
|
||||
export const addProblemMatchers =
|
||||
core.getInput("add-problem-matchers") === "true";
|
||||
|
||||
function getVersionFile(): string {
|
||||
const versionFileInput = core.getInput("version-file");
|
||||
if (versionFileInput !== "") {
|
||||
const tildeExpanded = expandTilde(versionFileInput);
|
||||
return resolveRelativePath(tildeExpanded);
|
||||
}
|
||||
return versionFileInput;
|
||||
}
|
||||
|
||||
function getEnableCache(): boolean {
|
||||
const enableCacheInput = core.getInput("enable-cache");
|
||||
if (enableCacheInput === "auto") {
|
||||
return process.env.RUNNER_ENVIRONMENT === "github-hosted";
|
||||
}
|
||||
return enableCacheInput === "true";
|
||||
}
|
||||
|
||||
function getToolBinDir(): string | undefined {
|
||||
const toolBinDirInput = core.getInput("tool-bin-dir");
|
||||
if (toolBinDirInput !== "") {
|
||||
const tildeExpanded = expandTilde(toolBinDirInput);
|
||||
return resolveRelativePath(tildeExpanded);
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
if (process.env.RUNNER_TEMP !== undefined) {
|
||||
return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-bin-dir`;
|
||||
}
|
||||
throw Error(
|
||||
"Could not determine UV_TOOL_BIN_DIR. Please make sure RUNNER_TEMP is set or provide the tool-bin-dir input",
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getToolDir(): string | undefined {
|
||||
const toolDirInput = core.getInput("tool-dir");
|
||||
if (toolDirInput !== "") {
|
||||
const tildeExpanded = expandTilde(toolDirInput);
|
||||
return resolveRelativePath(tildeExpanded);
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
if (process.env.RUNNER_TEMP !== undefined) {
|
||||
return `${process.env.RUNNER_TEMP}${path.sep}uv-tool-dir`;
|
||||
}
|
||||
throw Error(
|
||||
"Could not determine UV_TOOL_DIR. Please make sure RUNNER_TEMP is set or provide the tool-dir input",
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getCacheLocalPath(): string {
|
||||
const cacheLocalPathInput = core.getInput("cache-local-path");
|
||||
if (cacheLocalPathInput !== "") {
|
||||
const tildeExpanded = expandTilde(cacheLocalPathInput);
|
||||
return resolveRelativePath(tildeExpanded);
|
||||
}
|
||||
if (process.env.RUNNER_ENVIRONMENT === "github-hosted") {
|
||||
if (process.env.RUNNER_TEMP !== undefined) {
|
||||
return `${process.env.RUNNER_TEMP}${path.sep}setup-uv-cache`;
|
||||
}
|
||||
throw Error(
|
||||
"Could not determine UV_CACHE_DIR. Please make sure RUNNER_TEMP is set or provide the cache-local-path input",
|
||||
);
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return `${process.env.APPDATA}${path.sep}uv${path.sep}cache`;
|
||||
}
|
||||
return `${process.env.HOME}${path.sep}.cache${path.sep}uv`;
|
||||
}
|
||||
|
||||
function getCacheDependencyGlob(): string {
|
||||
const cacheDependencyGlobInput = core.getInput("cache-dependency-glob");
|
||||
if (cacheDependencyGlobInput !== "") {
|
||||
return cacheDependencyGlobInput
|
||||
.split("\n")
|
||||
.map((part) => part.trim())
|
||||
.map((part) => expandTilde(part))
|
||||
.map((part) => resolveRelativePath(part))
|
||||
.join("\n");
|
||||
}
|
||||
return cacheDependencyGlobInput;
|
||||
}
|
||||
|
||||
function expandTilde(input: string): string {
|
||||
if (input.startsWith("~")) {
|
||||
return `${process.env.HOME}${input.substring(1)}`;
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
function resolveRelativePath(inputPath: string): string {
|
||||
const hasNegation = inputPath.startsWith("!");
|
||||
const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath;
|
||||
|
||||
const resolvedPath = path.resolve(workingDirectory, pathWithoutNegation);
|
||||
|
||||
core.debug(
|
||||
`Resolving relative path ${inputPath} to ${hasNegation ? "!" : ""}${resolvedPath}`,
|
||||
);
|
||||
return hasNegation ? `!${resolvedPath}` : resolvedPath;
|
||||
}
|
||||
|
||||
function getManifestFile(): string | undefined {
|
||||
const manifestFileInput = core.getInput("manifest-file");
|
||||
if (manifestFileInput !== "") {
|
||||
return manifestFileInput;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Octokit as Core } from "@octokit/core";
|
||||
import type {
|
||||
Constructor,
|
||||
OctokitOptions,
|
||||
} from "@octokit/core/dist-types/types";
|
||||
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",
|
||||
};
|
||||
|
||||
export const Octokit: typeof Core &
|
||||
Constructor<
|
||||
{
|
||||
paginate: PaginateInterface;
|
||||
} & ReturnType<typeof legacyRestEndpointMethods>
|
||||
> = Core.plugin(paginateRest, legacyRestEndpointMethods).defaults(
|
||||
function buildDefaults(options: OctokitOptions): OctokitOptions {
|
||||
return {
|
||||
...DEFAULTS,
|
||||
...options,
|
||||
request: {
|
||||
fetch: customFetch,
|
||||
...options.request,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export type Octokit = InstanceType<typeof Octokit>;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
export type Platform =
|
||||
| "unknown-linux-gnu"
|
||||
| "unknown-linux-musl"
|
||||
| "unknown-linux-musleabihf"
|
||||
| "apple-darwin"
|
||||
| "pc-windows-msvc";
|
||||
export type Architecture =
|
||||
| "i686"
|
||||
| "x86_64"
|
||||
| "aarch64"
|
||||
| "s390x"
|
||||
| "powerpc64le";
|
||||
|
||||
export function getArch(): Architecture | undefined {
|
||||
const arch = process.arch;
|
||||
const archMapping: { [key: string]: Architecture } = {
|
||||
arm64: "aarch64",
|
||||
ia32: "i686",
|
||||
ppc64: "powerpc64le",
|
||||
s390x: "s390x",
|
||||
x64: "x86_64",
|
||||
};
|
||||
|
||||
if (arch in archMapping) {
|
||||
return archMapping[arch];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlatform(): Promise<Platform | undefined> {
|
||||
const processPlatform = process.platform;
|
||||
const platformMapping: { [key: string]: Platform } = {
|
||||
darwin: "apple-darwin",
|
||||
linux: "unknown-linux-gnu",
|
||||
win32: "pc-windows-msvc",
|
||||
};
|
||||
|
||||
if (processPlatform in platformMapping) {
|
||||
const platform = platformMapping[processPlatform];
|
||||
if (platform === "unknown-linux-gnu") {
|
||||
const isMusl = await isMuslOs();
|
||||
return isMusl ? "unknown-linux-musl" : platform;
|
||||
}
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
async function isMuslOs(): Promise<boolean> {
|
||||
let stdOutput = "";
|
||||
let errOutput = "";
|
||||
const options: exec.ExecOptions = {
|
||||
ignoreReturnCode: true,
|
||||
listeners: {
|
||||
stderr: (data: Buffer) => {
|
||||
errOutput += data.toString();
|
||||
},
|
||||
stdout: (data: Buffer) => {
|
||||
stdOutput += data.toString();
|
||||
},
|
||||
},
|
||||
silent: !core.isDebug(),
|
||||
};
|
||||
|
||||
try {
|
||||
const execArgs = ["--version"];
|
||||
await exec.exec("ldd", execArgs, options);
|
||||
return stdOutput.includes("musl") || errOutput.includes("musl");
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
core.warning(
|
||||
`Failed to determine glibc or musl. Falling back to glibc. Error: ${err.message}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import fs from "node:fs";
|
||||
import * as toml from "smol-toml";
|
||||
|
||||
export function getRequiredVersionFromConfigFile(
|
||||
filePath: string,
|
||||
): string | undefined {
|
||||
if (!filePath.endsWith(".toml")) {
|
||||
return undefined;
|
||||
}
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
|
||||
if (filePath.endsWith("pyproject.toml")) {
|
||||
const tomlContent = toml.parse(fileContent) as {
|
||||
tool?: { uv?: { "required-version"?: string } };
|
||||
};
|
||||
return tomlContent?.tool?.uv?.["required-version"];
|
||||
}
|
||||
const tomlContent = toml.parse(fileContent) as {
|
||||
"required-version"?: string;
|
||||
};
|
||||
return tomlContent["required-version"];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from "node:fs";
|
||||
import * as toml from "smol-toml";
|
||||
|
||||
export function getUvVersionFromRequirementsFile(
|
||||
filePath: string,
|
||||
): string | undefined {
|
||||
const fileContent = fs.readFileSync(filePath, "utf-8");
|
||||
if (filePath.endsWith(".txt")) {
|
||||
return getUvVersionFromAllDependencies(fileContent.split("\n"));
|
||||
}
|
||||
const dependencies = parsePyprojectDependencies(fileContent);
|
||||
return getUvVersionFromAllDependencies(dependencies);
|
||||
}
|
||||
function getUvVersionFromAllDependencies(
|
||||
allDependencies: string[],
|
||||
): string | undefined {
|
||||
return allDependencies
|
||||
.find((dep: string) => dep.match(/^uv[=<>~!]/))
|
||||
?.match(/^uv([=<>~!]+\S*)/)?.[1]
|
||||
.trim();
|
||||
}
|
||||
|
||||
interface Pyproject {
|
||||
project?: {
|
||||
dependencies?: string[];
|
||||
"optional-dependencies"?: Record<string, string[]>;
|
||||
};
|
||||
"dependency-groups"?: Record<string, Array<string | object>>;
|
||||
}
|
||||
|
||||
function parsePyprojectDependencies(pyprojectContent: string): string[] {
|
||||
const pyproject: Pyproject = toml.parse(pyprojectContent);
|
||||
const dependencies: string[] = pyproject?.project?.dependencies || [];
|
||||
const optionalDependencies: string[] = Object.values(
|
||||
pyproject?.project?.["optional-dependencies"] || {},
|
||||
).flat();
|
||||
const devDependencies: string[] = Object.values(
|
||||
pyproject?.["dependency-groups"] || {},
|
||||
)
|
||||
.flat()
|
||||
.filter((item: string | object) => typeof item === "string");
|
||||
return dependencies.concat(optionalDependencies, devDependencies);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import fs from "node:fs";
|
||||
import * as core from "@actions/core";
|
||||
import { getRequiredVersionFromConfigFile } from "./config-file";
|
||||
import { getUvVersionFromRequirementsFile } from "./requirements-file";
|
||||
import { getUvVersionFromToolVersions } from "./tool-versions-file";
|
||||
|
||||
export function getUvVersionFromFile(filePath: string): string | undefined {
|
||||
core.info(`Trying to find version for uv in: ${filePath}`);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
core.info(`Could not find file: ${filePath}`);
|
||||
return undefined;
|
||||
}
|
||||
let uvVersion: string | undefined;
|
||||
try {
|
||||
uvVersion = getUvVersionFromToolVersions(filePath);
|
||||
if (uvVersion === undefined) {
|
||||
uvVersion = getRequiredVersionFromConfigFile(filePath);
|
||||
}
|
||||
if (uvVersion === undefined) {
|
||||
uvVersion = getUvVersionFromRequirementsFile(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
core.warning(`Error while parsing ${filePath}: ${message}`);
|
||||
return undefined;
|
||||
}
|
||||
if (uvVersion?.startsWith("==")) {
|
||||
uvVersion = uvVersion.slice(2);
|
||||
}
|
||||
if (uvVersion !== undefined) {
|
||||
core.info(`Found version for uv in ${filePath}: ${uvVersion}`);
|
||||
}
|
||||
return uvVersion;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from "node:fs";
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export function getUvVersionFromToolVersions(
|
||||
filePath: string,
|
||||
): string | undefined {
|
||||
if (!filePath.endsWith(".tool-versions")) {
|
||||
return undefined;
|
||||
}
|
||||
const fileContents = fs.readFileSync(filePath, "utf8");
|
||||
const lines = fileContents.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
// Skip commented lines
|
||||
if (line.trim().startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const match = line.match(/^\s*uv\s*v?\s*(?<version>[^\s]+)\s*$/);
|
||||
if (match) {
|
||||
const matchedVersion = match.groups?.version.trim();
|
||||
if (matchedVersion?.startsWith("ref")) {
|
||||
core.warning(
|
||||
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return matchedVersion;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user