doc-inspector / scripts /check_github_contributors.py
steven0226's picture
deploy: 建立乾淨的已驗收版本
9d60e8e verified
Raw
History Blame Contribute Delete
4.2 kB
"""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())