Spaces:
Running
Running
| """Read-only verification that CI passed for an exact public GitHub commit.""" | |
| from __future__ import annotations | |
| import argparse | |
| from collections.abc import Callable | |
| import json | |
| import re | |
| import sys | |
| from typing import Any | |
| from urllib.parse import urlencode | |
| from urllib.request import Request, urlopen | |
| REPOSITORY = "kuotunyu/doc-inspector" | |
| WORKFLOW_FILE = "ci.yml" | |
| BRANCH = "main" | |
| API_VERSION = "2026-03-10" | |
| SHA_PATTERN = re.compile(r"^[0-9a-fA-F]{40}$") | |
| def normalize_sha(value: str) -> str: | |
| candidate = value.strip() | |
| if not SHA_PATTERN.fullmatch(candidate): | |
| raise ValueError("expected SHA 必須是完整的 40 位十六進位 Git commit SHA。") | |
| return candidate.lower() | |
| def fetch_ci_report( | |
| expected_sha: str, | |
| *, | |
| opener: Callable[..., Any] = urlopen, | |
| timeout_seconds: float = 20, | |
| ) -> dict[str, object]: | |
| """Return whether the push workflow succeeded for exactly expected_sha.""" | |
| sha = normalize_sha(expected_sha) | |
| if timeout_seconds <= 0: | |
| raise ValueError("timeout_seconds 必須大於 0。") | |
| query = urlencode( | |
| { | |
| "branch": BRANCH, | |
| "event": "push", | |
| "head_sha": sha, | |
| "per_page": 1, | |
| } | |
| ) | |
| request = Request( | |
| ( | |
| f"https://api.github.com/repos/{REPOSITORY}/actions/workflows/" | |
| f"{WORKFLOW_FILE}/runs?{query}" | |
| ), | |
| headers={ | |
| "Accept": "application/vnd.github+json", | |
| "Accept-Encoding": "identity", | |
| "User-Agent": "doc-inspector-ci-check/1.0", | |
| "X-GitHub-Api-Version": API_VERSION, | |
| }, | |
| method="GET", | |
| ) | |
| with opener(request, timeout=timeout_seconds) as response: | |
| status_code = int(response.getcode()) | |
| payload = response.read() | |
| if status_code != 200: | |
| raise RuntimeError(f"GitHub Actions API 回傳 HTTP {status_code}。") | |
| decoded = json.loads(payload.decode("utf-8")) | |
| if not isinstance(decoded, dict): | |
| raise ValueError("GitHub Actions API 回應不是物件。") | |
| runs = decoded.get("workflow_runs") | |
| if not isinstance(runs, list): | |
| raise ValueError("GitHub Actions API 回應缺少 workflow_runs 清單。") | |
| if any(not isinstance(run, dict) for run in runs): | |
| raise ValueError("GitHub Actions API 的 workflow_runs 含有無效項目。") | |
| run = runs[0] if runs else None | |
| returned_sha = run.get("head_sha") if run else None | |
| sha_matches = ( | |
| isinstance(returned_sha, str) and returned_sha.casefold() == sha.casefold() | |
| ) | |
| run_status = run.get("status") if run else None | |
| conclusion = run.get("conclusion") if run else None | |
| ci_passed = ( | |
| sha_matches and run_status == "completed" and conclusion == "success" | |
| ) | |
| return { | |
| "verified": True, | |
| "repository": REPOSITORY, | |
| "workflow_file": WORKFLOW_FILE, | |
| "branch": BRANCH, | |
| "expected_sha": sha, | |
| "run_found": run is not None, | |
| "sha_matches": sha_matches, | |
| "status": run_status, | |
| "conclusion": conclusion, | |
| "run_number": run.get("run_number") if run else None, | |
| "run_url": run.get("html_url") if run else None, | |
| "ci_passed": ci_passed, | |
| "uses_authentication": False, | |
| "reads_env_truth": False, | |
| "performs_writes": False, | |
| } | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser( | |
| description="確認指定 GitHub commit 的 push CI 已完成且成功;不會寫入遠端。", | |
| ) | |
| parser.add_argument( | |
| "--expected-sha", | |
| required=True, | |
| help="由維護者在推送後以 git rev-parse HEAD 取得的完整 commit SHA", | |
| ) | |
| parser.add_argument("--timeout-seconds", type=float, default=20) | |
| return parser | |
| def main(argv: list[str] | None = None) -> int: | |
| if hasattr(sys.stdout, "reconfigure"): | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| args = build_parser().parse_args(argv) | |
| try: | |
| report = fetch_ci_report( | |
| args.expected_sha, | |
| timeout_seconds=args.timeout_seconds, | |
| ) | |
| except (OSError, RuntimeError, ValueError, UnicodeDecodeError) as exc: | |
| report = { | |
| "verified": False, | |
| "repository": REPOSITORY, | |
| "workflow_file": WORKFLOW_FILE, | |
| "ci_passed": False, | |
| "error_type": type(exc).__name__, | |
| "error": str(exc), | |
| "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["ci_passed"] else 1 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |