Prhub

#2720 fix(ci): grant pull-requests write to comment-gateway feedback jobs

原始 PR 作者 guapisolo 合并时间 2026-08-24 03:27 文件变更 7 提交数 7 评论 4 代码增减 +537 / -107

执行摘要

/rerun-test 反馈升级:同一条评论从 running 原地更新到终态

PR body 明确指出旧反馈机制不可信:评论链接的是几秒钟的权限探测 run 而不是真正分派的测试,没有 in-progress 状态、没有耗时、也不会随测试结果更新。实际运行证据是旧评论链接的 run 32637432938,其 jobs 只测试了反馈权限,而真正的测试结果在另一个 run 中,reviewer 无法从评论直接判断成败。此外 commit de5031c0 记录了权限根因:acknowledge 与 reply 两个 job 调用的 issues API 目标评论永远挂在 PR 上,GitHub 对这类调用按 pull-requests scope 门控,仅有 issues: write 时 run 32630924734 中 dispatch 成功但两个反馈 job 均返回 403。

值得精读。该 PR 虽然只涉及 CI 基础设施,但其设计决策具有可迁移性:状态评论的 ownership 归属(run 自持而不是 gateway 代发)、comment ID 跨 job 传递、always() finalizer 汇总多 job 结果、以及结果映射的 fail-closed 语义,都是构建可靠的 PR 评论机器人的良好范本。尤其推荐关注 _file_run_status_inputs 的严格入参校验与 _file_run_outcome 的终态映射,这两处体现了“宁可报失败也不静默”的工程原则。

讨论亮点

该 PR 没有实质性的 human review 交锋:review 列表中只有 claude[bot] 的自动配置提示,评审意见为空的。设计讨论主要沉淀在 PR body 和 commit 消息中,值得提炼的点:

  • 状态评论的所有权归属:PR body 的 Design Notes 明确采用“run-owned comment”心智模型——默认分支 gateway 只负责授权与分派,状态评论由被分派的 run 自己创建并持有,comment ID 通过 needs 输出流经 resolver / execution 进入 always() finalizer,避免 gateway 与 run 双方抢写同一条评论。
  • 权限 scope 的边界推敲:commit de5031c0 描述中提到,reaction 与状态评论都是 PR-targeted 调用,GitHub 用 pull-requests scope 门控,因此 issues: write 单独不够;finalizer 额外需要 actions: read 才能读取 run_started_at。每个 job 的权限都按最小集精确声明。
  • fail-closed 的结果语义:PR body 的 Review Focus 强调“只有 execution success 才报 passed;无效 status 输入 fail closed”。实现上 _file_run_outcome 把 resolver 失败、无执行 job、取消都显式映射为失败或取消,绝不把“没跑”的 run 粉饰成成功。

实现拆解

  1. 权限修复先行.github/workflows/comment-ci-command.yml 中删除 reply-command job(连带移除 workflow_run_url output),并为 acknowledge job 增加 pull-requests: write。原因是 GitHub 对目标 issue 为 PR 的 issues-API 调用按 pull-requests scope 门控,单独 issues: write 会 403。这是 commit de5031c0 的独立修复,被后续重构吸收。
  2. 状态评论架构取代静态回复.github/workflows/run-ci-file.yml 新增两个 job。announce-file-run 在 run 启动时发布 running 状态评论(含测试文件、run 链接、开始时间、<!-- rerun-test-run:<id> --> 标记),并把 comment_id 作为 job output 传给下游;report-file-runif: always() 挂载在 resolve、CUDA 执行、CPU 执行三个 job 之后,汇总 needs.*.result 映射为终态,读取 run_started_at 计算耗时后 PATCH 回同一个评论 ID。
  3. handler 能力扩展.github/workflows/scripts/comment_ci_command.pycreate_issue_comment 改为返回新建评论的 ID,新增 update_issue_comment(PATCH)与 get_workflow_run(读 start 时间);删除静态 reply_event 路径。新增 _file_run_status_inputs 统一解析 announce / report 两种模式的环境变量并严格校验(测试文件白名单、suite 格式、job result 枚举);_file_run_outcome 定义 fail-closed 的终态映射——只有执行 job 真正 success 才报 passed,resolver 失败、无执行 job、取消均显式报 failed / cancelled。
  4. 测试与文档配套tests/ci/test/test_comment_ci_command.py 新增 FakeAPI 的 update_issue_comment / get_workflow_run,并新增 announce、report、终态映射参数化测试(含非法输入 fail closed);tests/ci/test/test_file_run.py 中的 workflow 不变式测试更新为断言 announce-file-run / report-file-run 的结构(checkout 次数从 2 变为 4、issues: writepull-requests: write 各出现 2 次、report job 含 actions: readif: always())。文档 docs/ci/01-label.mddocs/ci/05-command-identity.md 同步重写反馈流程与 token 权限说明。
文件 模块 状态 重要度
.github/workflows/scripts/comment_ci_command.py 命令网关 modified 7.16
tests/ci/test/test_comment_ci_command.py 命令网关 modified 7.2
.github/workflows/run-ci-file.yml 工作流编排 modified 5.39
.github/workflows/comment-ci-command.yml 工作流编排 modified 4.37
tests/ci/test/test_file_run.py 文件运行 modified 4.15
docs/ci/01-label.md CI 文档 modified 2.74
docs/ci/05-command-identity.md CI 文档 modified 2.02

关键符号

announce_file_run report_file_run _file_run_status_inputs _file_run_outcome _file_run_marker _format_duration _parse_run_timestamp _run_elapsed update_issue_comment get_workflow_run create_issue_comment FileRunStatus reply_event

关键源码片段

.github/workflows/scripts/comment_ci_command.py infrastructure

CI 评论命令处理器的核心脚本,本次重写了 /rerun-test 的反馈路径:删除静态 reply_event,新增 announce / report 双阶段状态评论能力、严格入参校验与耗时计算。

def _file_run_status_inputs(environ):
    """从环境变量解析 announce / report 两种模式的入参。    所有字段都做严格校验:测试文件路径必须匹配注册白名单,suite 必须是
    纯小写短横线名,job result 只能落在 {success, failure, cancelled,
    skipped, ""} 之内。任何异常输入都直接抛错,让 workflow job 失败,
    绝不把脏数据写进 PR 评论。
    """
    mode = environ.get("CI_COMMAND_FILE_RUN_STATUS", "")
    if mode not in FILE_RUN_STATUS_MODES:
        raise CommentCommandError("file run status mode must be announce or report")
    pull_number = _parse_positive_int(environ.get("FILE_RUN_PULL_NUMBER"), "pull request number")
    run_id = _parse_positive_int(environ.get("FILE_RUN_RUN_ID"), "workflow run ID")
    test_file = environ.get("FILE_RUN_TEST_FILE", "")
    if TEST_FILE_PATTERN.fullmatch(test_file) is None:
        raise CommentCommandError("file run test file is invalid")
    suite = environ.get("FILE_RUN_SUITE", "")
    if suite and SUITE_PATTERN.fullmatch(suite) is None:
        raise CommentCommandError("file run suite is invalid")
    results = {
        "resolve": environ.get("FILE_RUN_RESOLVE_RESULT", ""),
        "cuda": environ.get("FILE_RUN_CUDA_RESULT", ""),
        "cpu": environ.get("FILE_RUN_CPU_RESULT", ""),
    }
    for name, value in results.items():
        if value not in FILE_RUN_RESULTS:
            raise CommentCommandError(f"file run {name} result is invalid")
    # CUDA / CPU 两个执行 job 由各自的 if 互斥,未执行的一方为 skipped,
    # 所以真正的执行结果就是那个非 skipped 的值。
    executed = [value for value in (results["cuda"], results["cpu"]) if value not in {"skipped", ""}]
    if len(executed) > 1:
        raise CommentCommandError("file run reported two executing jobs")
    comment_id = None
    if mode == "report":
        comment_id = _parse_positive_int(environ.get("FILE_RUN_COMMENT_ID"), "comment ID")
    return (
        mode,
        FileRunStatus(
            pull_number=pull_number,
            test_file=test_file,
            run_id=run_id,
            suite=suite,
            resolve_result=results["resolve"],
            execute_result=executed[0] if executed else "",
        ),
        comment_id,
    )

评论区精华

反馈 job 403 根因:issues: write 与 pull-requests: write 的 scope 门控 正确性

来自 commit de5031c0 的说明:acknowledge 与 reply 两个 job 调用的 issues API 目标评论永远挂在 PR 上,GitHub 对目标 issue 为 PR 的调用按 pull-requests scope 门控,因此只有 issues: write 时 run 32630924734 中 dispatch 成功但两个反馈 job 都返回 403。

结论:给 acknowledge 与 file-run 状态 job 均增加 pull-requests: write,并在 05-command-identity.md 记录该 scope 规则。 · 已解决

状态评论 ownership:gateway 静态回复 vs workflow run 自持 设计

来自 PR body Design Notes:旧实现由 gateway 的 reply-command 静态发布一次性的 run 链接,既不显示进行中状态也不随结果更新;新设计把评论创建下放到被分派的 run 自身,comment ID 通过 needs 输出在 job 间传递,最终由 always() finalizer 原地更新。

结论:移除 gateway 的 reply-command job,采用 run-owned 状态评论,保证“谁执行谁报告”。 · 已解决

终态映射的 fail-closed 语义 正确性

来自 PR body Review Focus:只有 execution success 才报 passed;无效 status 输入必须 fail closed。

结论:实现上 _file_run_outcome 把 resolver 失败、无执行 job、取消均显式映射为 failed / cancelled,任何未知状态都会抛错而不是静默。 · 已解决

临时验证工作流与合入策略 other

7 个 commit 中前 6 个为 tmp 验证提交:先在真实 GitHub 环境用临时 workflow 做 A/B 权限验证(run 32637432938、32637525392),确认权限假设后移除临时文件。guapisolo 在 Issue 评论区表示 quick merge to unblock。

结论:验证完成后删除临时工作流,第 7 个 commit 合入正式实现。 · 已解决

风险与影响

  • comment ID 传递链断裂风险report-file-run 依赖 announce-file-run.outputs.comment_id。若 announce 阶段评论创建失败或 output 缺失,report 阶段会因 FILE_RUN_COMMENT_ID 解析失败而 fail closed,PR 上不会出现任何状态评论——行为是安全的(不伪装成功),但会造成反馈缺失。
  • if: always() 单点失败:report job 自身失败时没有重试或兜底,可能出现“run 已结束但评论仍显示 running”的窗口。当前实现把这一点视为可接受,因为反馈 job 只跑固定默认分支代码,失败概率低。
  • 权限扩散.github/workflows/comment-ci-command.ymlrun-ci-file.yml 中新增的 pull-requests: write 作用域是 workflow 的 GITHUB_TOKEN,仅限当前仓库、不能跨仓库,且这些 job 只执行默认分支固定代码;风险主要在未来工作流改动时权限块被误复制到执行 PR 代码的 job。
  • 测试脆弱性test_file_run.py 使用工作流文本断言(如 checkout 次数、issues: write 出现次数),对工作流格式调整敏感,后续修改 run-ci-file.yml 时需同步更新字符串断言,否则会脆断。
  • 并发边界queue: max + cancel-in-progress: false 下多个 /rerun-test 命令可并行,各自的 github.run_id 与 marker 一一对应,状态评论互不覆盖;但若有人手动编辑评论破坏 marker,跨 run 溯源会失效(低概率)。
  • 对开发者:以后跑 /rerun-test 只需盯住一条评论,从 running 到最终结果(含 suite 与 3m10s 级别的耗时)原地更新,不再需要去 Actions 页面人工核对 run 是否就是自己那条命令触发的。
  • 对 CI 系统:gateway 职责收窄为“授权 + 分派 + 点赞”,反馈责任下沉到 workflow run 自身,符合“谁执行谁报告”的 ownership 原则;移除静态 reply-command 后全流程只有一条状态评论,减少噪音。
  • 对团队:修复了反馈 job 的 403 导致的“评论链接的 run 根本没跑测试”的信任危机;文档同步更新了 token 身份矩阵,后续维护者能清楚看到每个 job 的权限边界。改动不触及 miles/ 训练、推理或 dashboard 代码,对产品运行时零影响。
新增 pull-requests 写权限 always() 终态链路依赖 comment ID 报告 job 自身失败无兜底 测试依赖工作流文本断言

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论