Add input manifest-file (#454)

Adds capability to maintain custom uv builds or to override the default
sources
This commit is contained in:
Kevin Stillhammer
2025-06-18 22:33:20 +02:00
committed by GitHub
parent 7bbb36f434
commit 60cc2b4585
14 changed files with 493 additions and 126 deletions

View File

@ -7,6 +7,7 @@ import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
import { validateChecksum } from "./checksum/checksum";
import { Octokit } from "../utils/octokit";
import { getDownloadUrl } from "./version-manifest";
export function tryGetFromToolCache(
arch: Architecture,
@ -23,7 +24,7 @@ export function tryGetFromToolCache(
return { version: resolvedVersion, installedPath };
}
export async function downloadVersion(
export async function downloadVersionFromGithub(
serverUrl: string,
platform: Platform,
arch: Architecture,
@ -31,29 +32,77 @@ export async function downloadVersion(
checkSum: string | undefined,
githubToken: string,
): Promise<{ version: string; cachedToolDir: string }> {
const resolvedVersion = await resolveVersion(version, githubToken);
const artifact = `uv-${arch}-${platform}`;
let extension = ".tar.gz";
if (platform === "pc-windows-msvc") {
extension = ".zip";
}
const downloadUrl = `${serverUrl}/${OWNER}/${REPO}/releases/download/${resolvedVersion}/${artifact}${extension}`;
core.info(`Downloading uv from "${downloadUrl}" ...`);
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.warning(
`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,
resolvedVersion,
);
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);
@ -61,15 +110,19 @@ export async function downloadVersion(
// On windows extracting the zip does not create an intermediate directory
} else {
const extractedDir = await tc.extractTar(downloadPath);
uvDir = path.join(extractedDir, artifact);
uvDir = path.join(extractedDir, artifactName);
}
const cachedToolDir = await tc.cacheDir(
uvDir,
TOOL_CACHE_NAME,
resolvedVersion,
version,
arch,
);
return { version: resolvedVersion, cachedToolDir };
return { version: version, cachedToolDir };
}
function getExtension(platform: Platform): string {
return platform === "pc-windows-msvc" ? ".zip" : ".tar.gz";
}
export async function resolveVersion(

View File

@ -1,8 +1,9 @@
import { promises as fs } from "node:fs";
import * as core from "@actions/core";
import * as semver from "semver";
import { fetch } from "../utils/fetch";
interface VersionManifestEntry {
interface ManifestEntry {
version: string;
artifactName: string;
arch: string;
@ -11,22 +12,57 @@ interface VersionManifestEntry {
}
export async function getLatestKnownVersion(
versionManifestFile: string,
manifestUrl: string | undefined,
): Promise<string> {
const data = await fs.readFile(versionManifestFile);
const versionManifestEntries: VersionManifestEntry[] = JSON.parse(
data.toString(),
);
return versionManifestEntries.reduce((a, b) =>
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("version-manifest.json");
data = fileContent.toString();
}
return JSON.parse(data);
}
export async function updateVersionManifest(
versionManifestFile: string,
manifestUrl: string,
downloadUrls: string[],
): Promise<void> {
const versionManifest: VersionManifestEntry[] = [];
const manifest: ManifestEntry[] = [];
for (const downloadUrl of downloadUrls) {
const urlParts = downloadUrl.split("/");
@ -39,7 +75,7 @@ export async function updateVersionManifest(
continue;
}
const artifactParts = artifactName.split(".")[0].split("-");
versionManifest.push({
manifest.push({
version: version,
artifactName: artifactName,
arch: artifactParts[1],
@ -47,6 +83,6 @@ export async function updateVersionManifest(
downloadUrl: downloadUrl,
});
}
core.debug(`Updating version manifest: ${JSON.stringify(versionManifest)}`);
await fs.writeFile(versionManifestFile, JSON.stringify(versionManifest));
core.debug(`Updating manifest-file: ${JSON.stringify(manifest)}`);
await fs.writeFile(manifestUrl, JSON.stringify(manifest));
}

View File

@ -1,9 +1,10 @@
import * as core from "@actions/core";
import * as path from "node:path";
import {
downloadVersion,
tryGetFromToolCache,
resolveVersion,
downloadVersionFromGithub,
downloadVersionFromManifest,
} from "./download/download-version";
import { restoreCache } from "./cache/restore-cache";
@ -26,6 +27,7 @@ import {
version as versionInput,
workingDirectory,
serverUrl,
manifestFile,
} from "./utils/inputs";
import * as exec from "@actions/exec";
import fs from "node:fs";
@ -95,14 +97,29 @@ async function setupUv(
};
}
const downloadVersionResult = await downloadVersion(
serverUrl,
platform,
arch,
resolvedVersion,
checkSum,
githubToken,
);
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,

View File

@ -12,7 +12,7 @@ import {
async function run(): Promise<void> {
const checksumFilePath = process.argv.slice(2)[0];
const versionsManifestFilePath = process.argv.slice(2)[1];
const versionsManifestFile = process.argv.slice(2)[1];
const githubToken = process.argv.slice(2)[2];
const octokit = new Octokit({
@ -24,9 +24,7 @@ async function run(): Promise<void> {
repo: REPO,
});
const latestKnownVersion = await getLatestKnownVersion(
versionsManifestFilePath,
);
const latestKnownVersion = await getLatestKnownVersion(undefined);
if (semver.lte(latestRelease.tag_name, latestKnownVersion)) {
core.info(
@ -52,7 +50,7 @@ async function run(): Promise<void> {
.map((asset) => asset.browser_download_url),
);
await updateVersionManifest(versionsManifestFilePath, artifactDownloadUrls);
await updateVersionManifest(versionsManifestFile, artifactDownloadUrls);
core.setOutput("latest-version", latestRelease.tag_name);
}

21
src/utils/fetch.ts Normal file
View File

@ -0,0 +1,21 @@
import { fetch as undiciFetch, ProxyAgent, type RequestInit } 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,
});

View File

@ -1,5 +1,6 @@
import * as core from "@actions/core";
import path from "node:path";
import { getManifestFromRepo } from "@actions/tool-cache";
export const version = core.getInput("version");
export const pythonVersion = core.getInput("python-version");
@ -19,6 +20,7 @@ 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();
function getEnableCache(): boolean {
const enableCacheInput = core.getInput("enable-cache");
@ -85,3 +87,11 @@ function expandTilde(input: string): string {
}
return input;
}
function getManifestFile(): string | undefined {
const manifestFileInput = core.getInput("manifest-file");
if (manifestFileInput !== "") {
return manifestFileInput;
}
return undefined;
}

View File

@ -8,7 +8,7 @@ import {
type PaginateInterface,
} from "@octokit/plugin-paginate-rest";
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
import { fetch as undiciFetch, ProxyAgent, type RequestInit } from "undici";
import { fetch as customFetch } from "./fetch";
export type { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods";
@ -17,26 +17,6 @@ const DEFAULTS = {
userAgent: "setup-uv",
};
export function getProxyAgent() {
const httpProxy = process.env.HTTP_PROXY || process.env.http_prox;
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 customFetch = async (url: string, opts: RequestInit) =>
await undiciFetch(url, {
dispatcher: getProxyAgent(),
...opts,
});
export const Octokit: typeof Core &
Constructor<
{