Spaces:
Running
Running
| """Read-only verification of the public GitHub contributor identity. | |
| The check uses GitHub's public REST endpoint without authentication. It does | |
| not inspect local Git history, read dotenv secrets, or write to the repository. | |
| """ | |
| from __future__ import annotations | |
| from collections.abc import Callable | |
| import json | |
| import sys | |
| from typing import Any | |
| from urllib.parse import urlencode | |
| from urllib.request import Request, urlopen | |
| REPOSITORY = "kuotunyu/doc-inspector" | |
| EXPECTED_LOGIN = "kuotunyu" | |
| API_ROOT = f"https://api.github.com/repos/{REPOSITORY}/contributors" | |
| API_VERSION = "2026-03-10" | |
| PER_PAGE = 100 | |
| MAX_PAGES = 10 | |
| def _request_url(page: int) -> str: | |
| query = urlencode({"anon": "1", "per_page": PER_PAGE, "page": page}) | |
| return f"{API_ROOT}?{query}" | |
| def fetch_contributor_report( | |
| *, | |
| opener: Callable[..., Any] = urlopen, | |
| timeout_seconds: float = 20, | |
| ) -> dict[str, object]: | |
| """Fetch all contributor pages and return a privacy-minimized report.""" | |
| if timeout_seconds <= 0: | |
| raise ValueError("timeout_seconds 必須大於 0。") | |
| entries: list[dict[str, object]] = [] | |
| pages_checked = 0 | |
| for page in range(1, MAX_PAGES + 1): | |
| request = Request( | |
| _request_url(page), | |
| headers={ | |
| "Accept": "application/vnd.github+json", | |
| "Accept-Encoding": "identity", | |
| "User-Agent": "doc-inspector-contributor-check/1.0", | |
| "X-GitHub-Api-Version": API_VERSION, | |
| }, | |
| method="GET", | |
| ) | |
| with opener(request, timeout=timeout_seconds) as response: | |
| status = int(response.getcode()) | |
| payload = response.read() | |
| if status != 200: | |
| raise RuntimeError(f"GitHub Contributors API 回傳 HTTP {status}。") | |
| decoded = json.loads(payload.decode("utf-8")) | |
| if not isinstance(decoded, list): | |
| raise ValueError("GitHub Contributors API 回應不是清單。") | |
| if any(not isinstance(item, dict) for item in decoded): | |
| raise ValueError("GitHub Contributors API 清單含有無效項目。") | |
| page_entries = list(decoded) | |
| entries.extend(page_entries) | |
| pages_checked = page | |
| if len(page_entries) < PER_PAGE: | |
| break | |
| else: | |
| raise RuntimeError("GitHub Contributors API 分頁超過安全上限。") | |
| logins = sorted( | |
| { | |
| login.strip() | |
| for item in entries | |
| if isinstance((login := item.get("login")), str) and login.strip() | |
| }, | |
| key=str.casefold, | |
| ) | |
| anonymous_count = sum( | |
| 1 | |
| for item in entries | |
| if not isinstance(item.get("login"), str) or not item["login"].strip() | |
| ) | |
| sole_contributor = logins == [EXPECTED_LOGIN] and anonymous_count == 0 | |
| return { | |
| "verified": True, | |
| "repository": REPOSITORY, | |
| "expected_login": EXPECTED_LOGIN, | |
| "logins": logins, | |
| "anonymous_contributor_count": anonymous_count, | |
| "contributor_entry_count": len(entries), | |
| "pages_checked": pages_checked, | |
| "sole_contributor": sole_contributor, | |
| "data_may_be_cached_for_hours": True, | |
| "uses_authentication": False, | |
| "reads_env_truth": False, | |
| "performs_writes": False, | |
| } | |
| def main() -> int: | |
| if hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| try: | |
| report = fetch_contributor_report() | |
| except (OSError, RuntimeError, ValueError, UnicodeDecodeError) as exc: | |
| report = { | |
| "verified": False, | |
| "repository": REPOSITORY, | |
| "expected_login": EXPECTED_LOGIN, | |
| "sole_contributor": False, | |
| "error_type": type(exc).__name__, | |
| "error": str(exc), | |
| "data_may_be_cached_for_hours": True, | |
| "uses_authentication": False, | |
| "reads_env_truth": False, | |
| "performs_writes": False, | |
| } | |
| print(json.dumps(report, ensure_ascii=False)) | |
| return 2 | |
| print(json.dumps(report, ensure_ascii=False)) | |
| return 0 if report["sole_contributor"] else 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |