执行摘要
- 一句话:新增 PR 评论命令网关,授权加 CI 标签与重跑任务
- 推荐动作:值得精读,尤其关注三点:①
COMMAND_REGISTRY 作为 handler、policy key、capability 的唯一事实来源,把评论到动作的映射压缩到一处;② capability 隔离的 GitHub App token 铸造逻辑(issues 与 actions 互斥);③ 精确 default-deny ACL 与严格 JSON 解析(_strict_object 拒绝重复键)的组合,防止策略文件被绕过。对 CI 维护者,tests/ci/test/test_comment_ci_command.py 展示了如何用 FakeAPI 断言不应发生的调用,是同类网关测试的范例。对普通贡献者,阅读 docs/ci/01-label.md 的 Manage CI from PR comments 一节即可。
功能与动机
Miles 的 CI 是 label 驱动的,但没有仓库写权限的贡献者无法从 PR 讨论中请求一个已批准的 CI 标签;旧的 target-string 路由器把每条评论都当作标签操作,新增非标签命令需要同步修改 workflow、解析器、策略、分发器和 token 选择。维护者需要一个可审查的访问层,委托固定命令而不授予仓库级变更权限,正如 PR body 所述:"Workflow owners need one reviewable access layer that delegates fixed commands without granting repository-wide mutation rights."
实现拆解
- 命令解析与注册表(新增
.github/workflows/scripts/comment_ci_command.py,约 870 行):把评论解析成精确命令,定义 AddLabel、ClearLabels、RerunFailedCI 类型,COMMAND_REGISTRY 静态注册表将命令与 handler、policy key、token capability 绑定为唯一事实来源;_strict_object 与 _reject_json_constant 提供严格 JSON 解析,拒绝重复键和非标准数字,防止策略文件被绕过;_validate_permissions 与 _validate_user_ids 只接受 write/admin,无关评论在读取策略前即退出(capability = none),不会调用 GitHub API 或铸造 token。
- ACL 策略(新增
.github/workflows/policies/comment-command-access.json):version 2 的精确 default-deny ACL。add_label_access 组(仓库 write/admin 或显式 user_ids)可执行 add_label;repo_write_access 组(仅 live write/admin)可执行 clear_labels 和 rerun_failed_ci。allowed_labels 白名单列出 20 个 run-ci-* 标签与 bypass-fastfail,新增 KNOWN_LABELS 条目不会自动暴露为可评论标签。
- Workflow 编排(新增
.github/workflows/comment-ci-command.yml,97 行):监听 issue_comment.created,默认关闭(vars.CI_COMMAND_APP_ENABLED == 'true' 才运行)。第一阶段用 github.token 做 preflight 授权并输出 capability(none/issues/actions);issues 能力时用 GitHub App token(permission-issues: write、permission-pull-requests: read)执行加/清标签;actions 能力走独立 job,用 permission-actions: write 铸造 token 后重跑失败任务,并以 concurrency: queue: max 保证同一 PR 的重跑串行、不替换旧请求。两次 checkout 都使用稀疏模式,只取 handler 与 policy 文件,且以 github.sha(基础分支)为准,避免 PR 内容污染信任边界。
- 测试(新增
tests/ci/test/test_comment_ci_command.py,1422 行,144 个用例):用 FakeAPI 模拟 GitHub API,覆盖权限校验、fork head、user_ids 精确授权、未知评论早退、clear-labels 只清持久控制标签、rerun 绑定当前 PR head 等工作流契约;通过 register_cpu_ci 挂到 stage-a-cpu CPU 套件,保证在常规 CI 中执行。
- 文档(更新
docs/ci/01-label.md):新增 "Manage CI from PR comments" 小节,说明命令的精确语法(整条评论只能有一条命令,允许多余首尾空白、不允许参数/正文/第二条命令)、default-deny 语义、user_ids 只授权加标签、maintain 角色按 legacy write 处理等。
关键文件:
tests/ci/test/test_comment_ci_command.py(模块 CI 命令;类别 test;类型 test-coverage;符号 load_module, FakeAPI, init, get_pull): 新增 1422 行测试(144 个用例),用 FakeAPI 完整模拟 GitHub API 调用序列,覆盖授权、拒绝、fork、user_ids、并发契约,是本次变更行为契约的锁定者。
.github/workflows/scripts/comment_ci_command.py(模块 CI 命令;类别 infra;类型 infrastructure;符号 CommentCommandError, AddLabel, ClearLabels, RerunFailedCI): 命令网关的核心实现(870 行):负责严格解析评论、校验策略、按 COMMAND_REGISTRY 路由到 AddLabel/ClearLabels/RerunFailedCI handler,并输出 capability 供 workflow 铸造不同权限的 token。
.github/workflows/comment-ci-command.yml(模块 CI 网关;类别 infra;类型 infrastructure): 新增 workflow 编排:issue_comment 触发、默认禁用、preflight 授权后按 capability 分叉,issues 与 actions 使用互斥的 App token 范围;rerun 以 concurrency queue:max 保证 per-PR 串行。
.github/workflows/policies/comment-command-access.json(模块 CI 策略;类别 infra;类型 infrastructure): 精确 default-deny ACL:add_label_access/repo_write_access 两组权限与 allowed_labels 白名单,是“谁能做什么”的唯一数据来源。
docs/ci/01-label.md(模块 CI 文档;类别 docs;类型 documentation): 面向贡献者与维护者的使用说明与安全语义文档,新增 Manage CI from PR comments 一节。
关键符号:load_json, _strict_object, _reject_json_constant, _validate_permissions, _validate_user_ids, AddLabel, ClearLabels, RerunFailedCI, FakeAPI.get_pull, FakeAPI.get_permission, FakeAPI.add_label, FakeAPI.remove_label, FakeAPI.list_workflow_runs, FakeAPI.rerun_failed_jobs, FakeAPI.list_pulls_for_head
关键源码片段
tests/ci/test/test_comment_ci_command.py
新增 1422 行测试(144 个用例),用 FakeAPI 完整模拟 GitHub API 调用序列,覆盖授权、拒绝、fork、user_ids、并发契约,是本次变更行为契约的锁定者。
import importlib.util
import json
import urllib.error
import urllib.parse
from pathlib import Path
import pytest
from tests.ci.ci_register import register_cpu_ci
from tests.ci.labels import KNOWN_LABELS
register_cpu_ci(est_time=1, suite="stage-a-cpu", labels=[])
ROOT = Path(__file__).parents[3]
HANDLER_PATH = ROOT / ".github/workflows/scripts/comment_ci_command.py"
POLICY_PATH = ROOT / ".github/workflows/policies/comment-command-access.json"
WORKFLOW_PATH = ROOT / ".github/workflows/comment-ci-command.yml"
def load_module(name, path):
# 直接按文件路径加载脚本 , 避免测试与真实 workflow 使用不同入口。
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
HANDLER = load_module("comment_ci_command", HANDLER_PATH)
ACTOR_ID = 1234 # 固定测试参与者数字 ID, 对应 GitHub 的稳定 user.id。
HEAD_SHA = "a" * 40
HEAD_REF = "feature/test"
WRITE_PERMISSIONS = frozenset({"write", "admin"})
# FakeAPI 记录全部 API 调用 , 让测试既能断言结果 , 也能断言“不应发生”的调用 , 例如无关评论绝不应触发 get_permission 或加标签。
class FakeAPI:
def __init__(self, pull, *, permission="write", permission_actor_id=ACTOR_ID):
self.pull = pull
self.permission = {
"permission": permission,
"user": {"id": permission_actor_id},
}
self.calls = []
self.get_calls = []
self.permission_calls = []
self.add_calls = []
self.remove_calls = []
# 为每个可重跑 workflow 准备独立的运行记录 , 便于验证 rerun 只命中当前 head。
self.workflow_runs = {workflow_file: [] for workflow_file, _ in HANDLER.RERUN_WORKFLOWS}
self.list_run_calls = []
self.rerun_calls = []
self.list_pull_calls = []
self.head_pulls = [pull]
def get_pull(self, pull_number):
self.calls.append(("get_pull", pull_number))
self.get_calls.append(pull_number)
return self.pull
def get_permission(self, actor_login):
self.calls.append(("get_permission", actor_login))
self.permission_calls.append(actor_login)
return self.permission
def add_label(self, pull_number, label):
self.calls.append(("add_label", pull_number, label))
self.add_calls.append((pull_number, label))
return [*self.pull["labels"], {"name": label}]
def remove_label(self, pull_number, label):
self.calls.append(("remove_label", pull_number, label))
self.remove_calls.append((pull_number, label))
self.pull["labels"] = [item for item in self.pull["labels"] if item["name"] != label]
return self.pull["labels"]
def list_workflow_runs(self, workflow_file, head_sha):
self.calls.append(("list_workflow_runs", workflow_file, head_sha))
self.list_run_calls.append((workflow_file, head_sha))
return self.workflow_runs[workflow_file]
def rerun_failed_jobs(self, run_id):
self.calls.append(("rerun_failed_jobs", run_id))
self.rerun_calls.append(run_id)
def list_pulls_for_head(self, owner_login, head_ref):
# 用于把 head ref 解析回当前 open PR, 保证 rerun 只作用在精确 head 上。
self.calls.append(("list_pulls_for_head", owner_login, head_ref))
self.list_pull_calls.append((owner_login, head_ref))
return self.head_pulls
评论区精华
该 PR 没有产生任何 review 评论,唯一的审核者 yushengsu-thu 直接 APPROVED,说明核心安全设计在作者自述与内部复核后获得认可。设计权衡主要写在 PR body 中:
风险与影响
- 风险:
- 信任边界依赖基础分支:workflow 以
github.sha 稀疏检出 handler 与 policy,PR 无法注入,但拥有基础分支写权限者仍可改动 comment_ci_command.py 扩大权限或外发 App token;该模型等价于仓库写权限信任,需在 App 注册与 rollout 时确认事件仅来自默认分支。
- 令牌泄漏面:App token 按 capability 互斥铸造,但 issues 分支的 token 会通过环境变量传递给脚本,若开启 workflow 的 step debug,
CI_COMMAND_API_TOKEN 可能进入日志;建议关闭调试并最小化 App 权限(pull-requests 仅 read)。
- 行为回归风险:
clear-labels 只删除 CLEAR_EXACT_LABELS 中两个持久控制标签,rerun-failed-ci 只重跑三个 PR workflow 且绑定 head SHA;若未来新增 workflow 未同步到 RERUN_WORKFLOWS,重跑会静默遗漏。
- 排队限制:GitHub 对
queue: max 并发保留最多 100 个待执行 job,同一 PR 重跑频繁时可能挤占全局配额并导致 5 分钟 timeout。
- ACL 手工维护:
comment-command-access.json 的 allowed_labels 与 KNOWN_LABELS 需要人工同步,漏加会导致贡献者请求被默认拒绝(安全方向正确,但体验会受影响)。
- 影响:
- 贡献者:可在 PR 评论里 /run-ci-short 等请求 CI 标签,免去找维护者打标签的等待;无写权限者仍然不能清标签或重跑。
- 维护者:通过 /clear-labels、/rerun-failed-ci 可自助处理持久控制标签和失败重跑;通过 JSON
user_ids 可逐个授信外部协作者,无需授予仓库写权限。
- 系统:新增仓库级 GitHub App 与两组配置变量/密钥;workflow 默认禁用,配置完成前不影响现有 label 驱动 CI。
- 团队:CI 交互从 label 路由演进为命令网关,后续扩命令(如 test-case)只需在注册表加一项,不必跨文件同步;影响范围集中在 CI/权限边界,不涉及训练、rollout、dashboard 等核心模块。
- 风险标记:默认禁用需 App 配置, token 能力互斥依赖 workflow 逻辑, 真实 GitHub App 集成未验证, 重跑排队受 100 上限约束, ACL 手工维护
关联脉络
- PR #2651 docs: fix stale paths, flags, env vars and metric names: 同属 CI/文档治理线,持续校准 docs/ 与真实 CI 行为;间接关联,未改相同文件。
- PR #2654 docs: name the Slack channel #miles-rl: 修改了 docs/ci/contributor-guide.md,同属 docs/ci 目录维护;间接关联。
参与讨论