feat(ci): PR 评论 /eval 触发内网 MCP 评测的 dispatch workflow

- issue_comment 触发,author_association ∈ OWNER/MEMBER/COLLABORATOR 门控
- 不 checkout、不执行 PR 代码;触发通道与凭证全部经 secrets 注入
- scripts/ci/eval_comment_parse.py 解析 /eval <products> [cases=<ref>](10 个单测)
This commit is contained in:
chichuan
2026-08-10 15:34:01 +08:00
parent 6575301a3a
commit 2d38abe681
3 changed files with 237 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""解析 PR 评论中的 `/eval <products> [cases=<ref>]` 触发命令。
供 .github/workflows/eval-dispatch.yml 调用:评论体经环境变量 COMMENT_BODY
传入(避免 argv 注入),解析结果以 GitHub Actions output(products / cases_ref)
写出;格式非法时把单行错误写入 output `error` 并以非零退出。
产品集显式必填;`cases=<ref>` 为用例仓库版本逃生舱(破坏性变更配对验证)。
"""
import os
import re
import sys
PRODUCT_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
REF_RE = re.compile(r"^[A-Za-z0-9._/-]+$")
USAGE = "用法: /eval <product>[,<product>...] [cases=<ref>];示例: /eval drive,doc cases=feat/x"
def parse(body: str):
lines = [line.strip() for line in (body or "").splitlines() if line.strip()]
if not lines:
raise ValueError(f"评论为空。{USAGE}")
tokens = lines[0].split()
if tokens[0] != "/eval":
raise ValueError(f"首行必须以 /eval 命令开头。{USAGE}")
if len(tokens) < 2:
raise ValueError(f"产品集显式必填。{USAGE}")
products = [p for p in tokens[1].split(",") if p]
if not products:
raise ValueError(f"产品集显式必填。{USAGE}")
for product in products:
if not PRODUCT_RE.match(product):
raise ValueError(f"产品名非法: {product}。{USAGE}")
cases_ref = ""
for extra in tokens[2:]:
if extra.startswith("cases="):
cases_ref = extra[len("cases="):]
if not cases_ref or not REF_RE.match(cases_ref):
raise ValueError(f"cases 引用非法: {extra}。{USAGE}")
else:
raise ValueError(f"未知参数: {extra}。{USAGE}")
return ",".join(products), cases_ref
def _write_outputs(pairs):
lines = [f"{key}={value}" for key, value in pairs]
output_path = os.environ.get("GITHUB_OUTPUT", "")
if output_path:
with open(output_path, "a", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
print("\n".join(lines))
def main() -> int:
try:
products, cases_ref = parse(os.environ.get("COMMENT_BODY", ""))
except ValueError as exc:
_write_outputs([("error", str(exc))])
print(str(exc), file=sys.stderr)
return 1
_write_outputs([("products", products), ("cases_ref", cases_ref)])
return 0
if __name__ == "__main__":
sys.exit(main())
+50
View File
@@ -0,0 +1,50 @@
import pytest
from eval_comment_parse import parse
def test_single_product():
assert parse("/eval drive") == ("drive", "")
def test_multiple_products():
assert parse("/eval drive,doc,edu-app") == ("drive,doc,edu-app", "")
def test_cases_ref_escape_hatch():
assert parse("/eval drive cases=feat/drive-latest") == ("drive", "feat/drive-latest")
def test_extra_lines_after_command_are_ignored():
body = "/eval drive\n\n顺便说明:这个 PR 只动了 drive 的 --latest。"
assert parse(body) == ("drive", "")
def test_missing_products_rejected():
with pytest.raises(ValueError, match="产品集显式必填"):
parse("/eval")
def test_similar_command_prefix_rejected():
with pytest.raises(ValueError, match="/eval 命令开头"):
parse("/evaluate drive")
def test_illegal_product_name_rejected():
with pytest.raises(ValueError, match="产品名非法"):
parse("/eval drive;rm")
def test_unknown_extra_token_rejected():
with pytest.raises(ValueError, match="未知参数"):
parse("/eval drive --force")
def test_illegal_cases_ref_rejected():
with pytest.raises(ValueError, match="cases 引用非法"):
parse("/eval drive cases=$(whoami)")
def test_empty_comment_rejected():
with pytest.raises(ValueError, match="评论为空"):
parse(" \n ")