154 lines
4.9 KiB
Python
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
|