#!/usr/bin/env python3 """验证 GitHub Actions 产出的 eval-dispatch artifact 请求。 评论只是索引。真正的授权记录是绑定到精确 workflow run、run attempt 和 dispatch comment ID 的不可变 artifact。任何缺失、冲突或异常都必须拒绝, 不能让单条不可信评论终止轮询进程。校验成功时返回稳定的 idempotency_key;调用方必须在触发评测前持久化、原子占用该 key,重复 占用只能 no-op。本脚本无状态,不以 reaction 代替持久去重。 """ from __future__ import annotations import hashlib import io import json import re import stat import subprocess import sys import zipfile from typing import Any, Optional, Protocol from eval_comment_parse import parse as parse_eval_comment REPOSITORY = "DingTalk-Real-AI/dingtalk-workspace-cli" REPOSITORY_ID = "1187709537" WORKFLOW_ID = "331725458" WORKFLOW_PATH = ".github/workflows/eval-dispatch.yml" DEFAULT_BRANCH = "main" TRUSTED_BOT_LOGIN = "github-actions[bot]" TRUSTED_APP_SLUG = "github-actions" ARTIFACT_FILENAME = "eval-dispatch-request.json" SCHEMA_VERSION = 1 MAX_COMMENT_BYTES = 64 * 1024 MAX_API_JSON_BYTES = 1024 * 1024 MAX_ARCHIVE_BYTES = 64 * 1024 MAX_MANIFEST_BYTES = 64 * 1024 MAX_PRODUCTS_BYTES = 512 MAX_CASES_REF_BYTES = 1024 _SHA_RE = re.compile(r"^[0-9a-f]{40}$") _SHA256_RE = re.compile(r"^[0-9a-f]{64}$") _DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") _PRODUCTS_RE = re.compile(r"^[a-z0-9][a-z0-9-]*(?:,[a-z0-9][a-z0-9-]*)*$") _REF_CHARSET_RE = re.compile(r"^[A-Za-z0-9._/-]+$") _LOGIN_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$") _MARKER_RE = re.compile(r"") _MARKER_FIELDS = { "schema_version", "repository_id", "workflow_id", "workflow_path", "run_id", "run_attempt", "dispatch_comment_id", "artifact_id", "artifact_digest", } _MANIFEST_FIELDS = { "schema_version", "repository_id", "repository", "workflow_id", "workflow_path", "run_id", "run_attempt", "source_comment_id", "dispatch_comment_id", "actor_id", "actor_login", "pr_number", "pr_head_sha", "products", "cases_ref", "source_body_sha256", "idempotency_key", } class GitHubClient(Protocol): """consumer 所需的最小 GitHub 只读边界。""" def get_run_attempt(self, run_id: str, run_attempt: str) -> Any: ... def get_artifact(self, artifact_id: str) -> Any: ... def download_artifact(self, artifact_id: str) -> bytes: ... def get_comment(self, comment_id: str) -> Any: ... def get_pull_request(self, pr_number: str) -> Any: ... class GitHubCLIClient: """通过已认证的 gh CLI 读取 GitHub REST API。""" def _json(self, endpoint: str) -> Any: result = subprocess.run( ["gh", "api", endpoint], capture_output=True, text=True, timeout=20, ) if result.returncode != 0: raise RuntimeError("GitHub API 请求失败") if len(result.stdout.encode("utf-8")) > MAX_API_JSON_BYTES: raise ValueError("GitHub API JSON 响应过大") return _loads_no_duplicates(result.stdout) def get_run_attempt(self, run_id: str, run_attempt: str) -> Any: return self._json( f"repos/{REPOSITORY}/actions/runs/{run_id}/attempts/{run_attempt}" ) def get_artifact(self, artifact_id: str) -> Any: return self._json(f"repos/{REPOSITORY}/actions/artifacts/{artifact_id}") def download_artifact(self, artifact_id: str) -> bytes: result = subprocess.run( ["gh", "api", f"repos/{REPOSITORY}/actions/artifacts/{artifact_id}/zip"], capture_output=True, timeout=30, ) if result.returncode != 0: raise RuntimeError("下载 GitHub artifact 失败") if len(result.stdout) > MAX_ARCHIVE_BYTES: raise ValueError("artifact 压缩包过大") return result.stdout def get_comment(self, comment_id: str) -> Any: return self._json(f"repos/{REPOSITORY}/issues/comments/{comment_id}") def get_pull_request(self, pr_number: str) -> Any: return self._json(f"repos/{REPOSITORY}/pulls/{pr_number}") def _loads_no_duplicates(raw: str) -> Any: def reject_duplicates(pairs): result = {} for key, value in pairs: if key in result: raise ValueError(f"JSON 字段重复: {key}") result[key] = value return result return json.loads(raw, object_pairs_hook=reject_duplicates) def _is_decimal_string(value: Any, *, max_length: int = 20) -> bool: return ( type(value) is str and 1 <= len(value) <= max_length and value.isascii() and value.isdigit() and value[0] != "0" ) def _as_decimal_string(value: Any) -> Optional[str]: if type(value) is int and value > 0: return str(value) if _is_decimal_string(value): return value return None def _is_valid_cases_ref(value: Any) -> bool: if type(value) is not str or len(value.encode("utf-8")) > MAX_CASES_REF_BYTES: return False if value == "": return True if not _REF_CHARSET_RE.fullmatch(value): return False if value.startswith(("-", "/")) or value.endswith(("/", ".")): return False if ".." in value or "//" in value: return False return all( component and not component.startswith(".") and not component.endswith(".lock") for component in value.split("/") ) def _is_string(value: Any, *, max_bytes: int, allow_empty: bool = False) -> bool: return ( type(value) is str and (allow_empty or bool(value)) and len(value.encode("utf-8")) <= max_bytes ) def validate_comment_author(comment: Any) -> bool: """校验 dispatch 评论由 GitHub Actions App 发出。""" if type(comment) is not dict: return False user = comment.get("user") app = comment.get("performed_via_github_app") return ( type(user) is dict and user.get("login") == TRUSTED_BOT_LOGIN and user.get("type") == "Bot" and type(app) is dict and app.get("slug") == TRUSTED_APP_SLUG ) def _extract_marker(body: Any) -> Optional[dict[str, Any]]: if not _is_string(body, max_bytes=MAX_COMMENT_BYTES): return None if body.count("