"""Proxy a GitHub release to Cloudflare R2. Example usage: uv run --env-file .env -- main.py https://github.com/astral-sh/uv-wheelnext/releases/tag/0.8.3 """ # /// script # requires-python = ">=3.13" # dependencies = [ # "requests", # ] # /// import argparse import logging import os import re import subprocess import sys from pathlib import Path import requests def parse_release_url(url: str) -> tuple[str, str, str]: """Parse a GitHub release URL into components. For example, given https://github.com/astral-sh/uv/releases/tag/0.6.14, this function will return `("astral-sh", "uv", "0.6.14")`. """ pattern = r"https://github\.com/([^/]+)/([^/]+)/releases/tag/([\w.-]+)" match = re.match(pattern, url) if not match: raise ValueError("Invalid GitHub release URL") owner, repo, version = match.groups() return owner, repo, version def download_release_assets(url: str, output_dir: Path) -> None: """Download all assets from a GitHub release.""" owner, repo, version = parse_release_url(url) # Create the output directory. output_dir.mkdir(parents=True, exist_ok=True) # Extract the release payload from the GitHub API. api_url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{version}" headers = {} if "GITHUB_TOKEN" in os.environ: headers["Authorization"] = f"Bearer {os.environ['GITHUB_TOKEN']}" response = requests.get(api_url, headers=headers) response.raise_for_status() release = response.json() if not release["assets"]: print("No assets found") return # Download each asset to disk for asset in release["assets"]: asset_name = asset["name"] output_path = output_dir / asset_name # Skip if file already exists if output_path.exists(): print(f"Skipping {asset_name} (already exists)") continue # Get download URL using the API URL. download_url = asset["url"] headers = { "Accept": "application/octet-stream", } if "GITHUB_TOKEN" in os.environ: headers["Authorization"] = f"Bearer {os.environ['GITHUB_TOKEN']}" response = requests.get(download_url, headers=headers, allow_redirects=True) response.raise_for_status() # Write the asset to disk. output_path.write_bytes(response.content) print(f"Saved to {output_path}") def upload_to_r2(local_dir: Path, *, wheelnext_version: str | None) -> None: """Upload files to Cloudflare R2 using wrangler.""" bucket = "uv-wheelnext-releases" # Upload each file for file_path in local_dir.iterdir(): if file_path.is_file(): if wheelnext_version: r2_path = f"{wheelnext_version}/{file_path.name}" else: r2_path = file_path.name print(f"Uploading {file_path.name} to {r2_path}...") try: result = subprocess.run( [ "npx", "wrangler", "r2", "object", "put", "--remote", f"{bucket}/{r2_path}", "--file", str(file_path), "--content-type", "application/octet-stream", ], check=True, capture_output=True, ) print(f"Command output:\n{result.stdout.decode()}") if result.stderr: print(f"Command stderr:\n{result.stderr.decode()}") print(f"Uploaded to {bucket}/{r2_path}") except subprocess.CalledProcessError as e: print(f"Error uploading {file_path.name}: {e.stderr.decode()} {e.stdout.decode()}") sys.exit(1) def main() -> None: logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", ) parser = argparse.ArgumentParser( description="Download GitHub release assets and upload to Cloudflare R2" ) parser.add_argument( "url", help="GitHub release URL (e.g., https://github.com/astral-sh/uv/releases/tag/0.6.14)", ) parser.add_argument( "--wheelnext-version", type=str, help="Wheelnext version", ) args = parser.parse_args() # Extract the version from the URL. _, _, uv_version = parse_release_url(args.url) # Create the downloads directory. downloads_dir = Path.cwd() / "downloads" / uv_version downloads_dir.mkdir(parents=True, exist_ok=True) # Download release assets. print("Downloading release assets...") download_release_assets(args.url, downloads_dir) # Upload to R2. print("Uploading to Cloudflare R2...") upload_to_r2(downloads_dir, wheelnext_version=args.wheelnext_version) print("All done!") if __name__ == "__main__": main()