headroom/scripts/refresh_tool_hashes.py

75 lines
2.5 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""Refresh SHA-256 pins for externally fetched tool binaries (WEB-03).
Fetches every asset URL in ``headroom/tools.json``, computes its SHA-256, and
writes the digests back into the registry. Run it locally after bumping a tool
version, or let the ``tools-hash-refresh`` CI workflow run it.
python scripts/refresh_tool_hashes.py # populate/update pins
python scripts/refresh_tool_hashes.py --check # exit 1 if any pin drifts
Only ``https://`` URLs are accepted; a plaintext URL is a hard error.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
import urllib.request
from pathlib import Path
REGISTRY = Path(__file__).resolve().parent.parent / "headroom" / "tools.json"
def _fetch_sha256(url: str) -> str:
req = urllib.request.Request(url, headers={"User-Agent": "headroom-tools-refresh/1"})
digest = hashlib.sha256()
with urllib.request.urlopen(req, timeout=120) as resp: # noqa: S310 - https enforced below
for chunk in iter(lambda: resp.read(1024 * 64), b""):
digest.update(chunk)
return digest.hexdigest()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="fail if any pin is missing/stale")
args = parser.parse_args()
data = json.loads(REGISTRY.read_text())
seen: dict[str, str] = {}
drift: list[str] = []
for tool_name, tool in data.get("tools", {}).items():
for platform, asset in tool.get("assets", {}).items():
url = asset.get("url")
if not url:
continue
if not url.startswith("https://"):
print(f"ERROR: {tool_name}/{platform}: non-https url {url!r}", file=sys.stderr)
return 2
if url not in seen:
print(f"fetching {tool_name}/{platform}", file=sys.stderr)
seen[url] = _fetch_sha256(url)
digest = seen[url]
if asset.get("sha256") != digest:
drift.append(f"{tool_name}/{platform}")
if not args.check:
asset["sha256"] = digest
if args.check:
if drift:
print("stale pins: " + ", ".join(drift), file=sys.stderr)
return 1
print("all tool pins up to date")
return 0
REGISTRY.write_text(json.dumps(data, indent=2) + "\n")
print(f"updated {len(drift)} pin(s) in {REGISTRY}")
return 0
if __name__ == "__main__":
raise SystemExit(main())