#!/usr/bin/env python3 import json import re import subprocess import sys from pathlib import Path from urllib.request import Request, urlopen from urllib.error import HTTPError GITHUB_API = "https://api.github.com" CODEBERG_API = "https://codeberg.org/api/v1" def run(cmd): p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if p.returncode != 0: raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{p.stderr.strip()}") return p.stdout.strip() def http_get_json(url, token=None): headers = {"Accept": "application/json"} if token: headers["Authorization"] = f"token {token}" req = Request(url, headers=headers) with urlopen(req) as resp: return json.load(resp) def github_latest_release(owner, repo, token=None): url = f"{GITHUB_API}/repos/{owner}/{repo}/releases/latest" return http_get_json(url, token=token) def github_latest_commit(owner, repo, token=None): url = f"{GITHUB_API}/repos/{owner}/{repo}/commits?per_page=1" data = http_get_json(url, token=token) return data[0]["sha"] def codeberg_latest_release(owner, repo, token=None): url = f"{CODEBERG_API}/repos/{owner}/{repo}/releases/latest" return http_get_json(url, token=token) def codeberg_latest_commit(owner, repo, token=None): url = f"{CODEBERG_API}/repos/{owner}/{repo}/commits?limit=1" data = http_get_json(url, token=token) return data[0]["sha"] def nix_hash_to_sri(hash_str): # Convert nix-base32 to SRI return run(["nix", "hash", "to-sri", "--type", "sha256", hash_str]) def prefetch_git(url, rev): out = run(["nix-prefetch-git", "--url", url, "--rev", rev, "--fetch-submodules"]) data = json.loads(out) return nix_hash_to_sri(data["sha256"]) def prefetch_url(url, unpack=False): cmd = ["nix-prefetch-url", url] if unpack: cmd.insert(1, "--unpack") hash_str = run(cmd) return nix_hash_to_sri(hash_str) def is_archive_url(url): return bool(re.search(r"\.(tar\.gz|tar\.xz|tar\.bz2|zip)$", url)) def build_repo_url(location, owner, repo): if location == "github": return f"https://github.com/{owner}/{repo}.git" if location == "codeberg": return f"https://codeberg.org/{owner}/{repo}.git" raise ValueError(f"Unknown repo location: {location}") def build_release_tarball_url(location, owner, repo, tag): if location == "github": return f"https://github.com/{owner}/{repo}/archive/refs/tags/{tag}.tar.gz" if location == "codeberg": return f"https://codeberg.org/{owner}/{repo}/archive/{tag}.tar.gz" raise ValueError(f"Unknown repo location: {location}") def update_entry(name, entry, gh_token=None, cb_token=None): location = entry.get("location") owner = entry.get("owner") repo = entry.get("repo") url = entry.get("url") if url and (location == "url" or location == "archive"): # Direct URL source unpack = is_archive_url(url) new_hash = prefetch_url(url, unpack=unpack) entry["hash"] = new_hash return True if location in ("github", "codeberg"): if entry.get("tag"): # Use latest release tag if location == "github": rel = github_latest_release(owner, repo, token=gh_token) tag = rel["tag_name"] else: rel = codeberg_latest_release(owner, repo, token=cb_token) tag = rel["tag_name"] if tag != entry["tag"]: entry["tag"] = tag tar_url = build_release_tarball_url(location, owner, repo, tag) entry["hash"] = prefetch_url(tar_url, unpack=True) return True if entry.get("rev"): # Use latest commit if location == "github": sha = github_latest_commit(owner, repo, token=gh_token) else: sha = codeberg_latest_commit(owner, repo, token=cb_token) if sha != entry["rev"]: entry["rev"] = sha repo_url = build_repo_url(location, owner, repo) entry["hash"] = prefetch_git(repo_url, sha) return True return False def process_file(path, gh_token=None, cb_token=None): data = json.loads(path.read_text()) changed = False for name, entry in data.items(): try: changed = update_entry(name, entry, gh_token=gh_token, cb_token=cb_token) except HTTPError as e: print(f"[WARN] {path}: {name}: HTTP error {e.code}", file=sys.stderr) except Exception as e: print(f"[WARN] {path}: {name}: {e}", file=sys.stderr) if changed: path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n") return changed def main(root): gh_token = None cb_token = None # Optional tokens from environment # import os # gh_token = os.environ.get("GITHUB_TOKEN") # cb_token = os.environ.get("CODEBERG_TOKEN") root = Path(root) files = list(root.rglob("version*.json")) if not files: print("No version*.json files found") return 1 updated = 0 for f in files: if process_file(f, gh_token=gh_token, cb_token=cb_token): print(f"Updated: {f}") updated += 1 print(f"Done. Updated {updated} file(s).") return 0 if __name__ == "__main__": if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} ") sys.exit(2) sys.exit(main(sys.argv[1]))