Files
kaizoku/provider_updates.py
T

154 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""Check bundled provider JavaScript against the upstream extensions repo."""
import hashlib
import json
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from app_support import AGENT, PROJECT_ROOT
UPSTREAM_REPO_URL = "https://github.com/TheYogMehta/extensions"
UPSTREAM_BRANCH = "main"
UPSTREAM_TREE_URL = (
f"https://api.github.com/repos/TheYogMehta/extensions/git/trees/{UPSTREAM_BRANCH}?recursive=1"
)
UPSTREAM_RAW_BASE = f"https://raw.githubusercontent.com/TheYogMehta/extensions/{UPSTREAM_BRANCH}/"
LOCAL_PROVIDER_ROOT = PROJECT_ROOT / "providers" / "extensions"
UPSTREAM_PROVIDER_PREFIX = "extensions/"
def now_iso():
return datetime.now(timezone.utc).isoformat()
def default_status(status="idle", message="Provider update check has not run yet."):
return {
"status": status,
"message": message,
"checked_at": "",
"upstream_url": UPSTREAM_REPO_URL,
"upstream_branch": UPSTREAM_BRANCH,
"upstream_tree_sha": "",
"checked_files": 0,
"updated_files": [],
"missing_upstream_files": [],
}
def local_provider_js_files(root=LOCAL_PROVIDER_ROOT):
root = Path(root)
if not root.exists():
return {}
files = {}
for path in sorted(root.rglob("*.js")):
if path.is_file():
files[path.relative_to(root).as_posix()] = path
return files
def sha256_bytes(content):
return hashlib.sha256(content).hexdigest()
def fetch_json(url, timeout=10):
request = urllib.request.Request(
url,
headers={
"Accept": "application/vnd.github+json",
"User-Agent": AGENT,
},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def fetch_bytes(url, timeout=10):
request = urllib.request.Request(
url,
headers={
"Accept": "application/octet-stream",
"User-Agent": AGENT,
},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
def upstream_provider_js_paths(timeout=10):
payload = fetch_json(UPSTREAM_TREE_URL, timeout=timeout)
tree = payload.get("tree") if isinstance(payload, dict) else []
paths = set()
for item in tree or []:
path = str((item or {}).get("path") or "")
if (item or {}).get("type") == "blob" and path.startswith(UPSTREAM_PROVIDER_PREFIX) and path.endswith(".js"):
paths.add(path[len(UPSTREAM_PROVIDER_PREFIX) :])
return paths, str(payload.get("sha") or "") if isinstance(payload, dict) else ""
def check_provider_updates(local_root=LOCAL_PROVIDER_ROOT, timeout=10):
checked_at = now_iso()
local_files = local_provider_js_files(local_root)
if not local_files:
status = default_status("error", "No local provider JavaScript files were found.")
status["checked_at"] = checked_at
return status
upstream_paths, upstream_tree_sha = upstream_provider_js_paths(timeout=timeout)
updated_files = []
missing_upstream_files = []
checked_files = 0
for relative_path, local_path in local_files.items():
if relative_path not in upstream_paths:
missing_upstream_files.append(relative_path)
continue
checked_files += 1
local_content = local_path.read_bytes()
remote_url = f"{UPSTREAM_RAW_BASE}{UPSTREAM_PROVIDER_PREFIX}{relative_path}"
remote_content = fetch_bytes(remote_url, timeout=timeout)
local_hash = sha256_bytes(local_content)
remote_hash = sha256_bytes(remote_content)
if local_hash != remote_hash:
updated_files.append(
{
"path": relative_path,
"local_sha256": local_hash,
"upstream_sha256": remote_hash,
"upstream_url": remote_url,
}
)
has_updates = bool(updated_files or missing_upstream_files)
if has_updates:
message = "Provider updates are available from TheYogMehta/extensions."
status_name = "updates_available"
else:
message = "Bundled provider JavaScript is up to date."
status_name = "up_to_date"
return {
"status": status_name,
"message": message,
"checked_at": checked_at,
"upstream_url": UPSTREAM_REPO_URL,
"upstream_branch": UPSTREAM_BRANCH,
"upstream_tree_sha": upstream_tree_sha,
"checked_files": checked_files,
"updated_files": updated_files,
"missing_upstream_files": missing_upstream_files,
}
def error_status(exc):
message = str(exc)
if isinstance(exc, urllib.error.URLError) and getattr(exc, "reason", None):
message = str(exc.reason)
status = default_status("error", f"Provider update check failed: {message}")
status["checked_at"] = now_iso()
return status