Prhub

#42053 Use `pre-commit / pre-run-check` to gate docs build too

原始 PR 作者 hmellor 合并时间 2026-05-09 08:02 文件变更 1 提交数 3 评论 2 代码增减 +38 / -0

执行摘要

文档构建增加 pre-run-check 门控

PR body 明确指出:"Use the pre-commit / pre-run-check result to also skip the docs build",预期 "With unauthenticated requests this should save 12h of docs builder time every hour"。作者希望避免在 pre-run-check 明确失败后仍然浪费文档构建资源。

该 PR 设计方向正确,但建议精读 review 评论并与作者确认竞态条件的处理方案。值得关注的设计决策是利用 post_checkout 钩子注入脚本,这是一种轻量级的门控实现方式。如果团队后续有更复杂的门控需求,可考虑迁移到专门的 ReadTheDocs 构建步骤或外部 CI 编排。

讨论亮点

Gemini-code-assist[bot] 在 review 中提出两个高优先级问题:

  1. 轮询间隔过短(15秒):未认证 GitHub API 限制为 60 次/小时/IP,vllm 活跃度高,极易触发速率限制导致门控失效。建议改为 60 秒及以上。
  2. 无 check-run 时立即放行引入竞态:文档构建可能先于 GitHub Actions 运行,此时 check-run 尚未注册,立即退出循环会导致门控被绕过。应继续轮询直到超时。

作者在后续提交中将 INTERVAL 从 15 改为 60,MAX_WAIT 从 600 改为 300,表明已采纳第一点。但对于第二点,当前实现(见 head_excerpt)在 STATUS=none 时仍直接 break,竞态问题未完全解决。

实现拆解

  1. 在 .readthedocs.yaml 的 post_checkout 阶段插入门控脚本:仅在 $READTHEDOCS_VERSION_TYPE 为 external(即 PR 构建)时执行。使用 curl 向 https://api.github.com/repos/vllm-project/vllm/commits/${READTHEDOCS_GIT_COMMIT_HASH}/check-runs?check_name=pre-run-check&filter=latest 发起未认证请求。
  2. 解析 GitHub API 响应:通过 Python 内联脚本提取 check_runs[0].status 和 check_runs[0].conclusion,处理 none(无对应 check-run)、completed 等状态。
  3. 根据检查结论决定构建行为:若结论为 failure、cancelled、timed_out 则 exit 1 使构建失败;若通过则继续流程。
  4. 实现超时与降级逻辑:设置 MAX_WAIT=300 秒、INTERVAL=60 秒轮询;若超时或 API 返回非 200(速率限制、无对应 check-run),则跳过门控并允许构建继续。
文件 模块 状态 重要度
.readthedocs.yaml 文档构建 modified 5.6

关键源码片段

.readthedocs.yaml configuration

唯一变更文件,新增了基于 GitHub API 的 pre-run-check 门控逻辑,影响所有外部 PR 的文档构建行为。

# .readthedocs.yaml 的 post_checkout 阶段新增门控逻辑
# 仅在 PR 构建 (external) 时执行,用于等待 pre-run-check 完成
    jobs:
      post_checkout:
        - |
          if [ "$READTHEDOCS_VERSION_TYPE" = "external" ]; then
            MAX_WAIT=300          # 最多等 5 分钟
            INTERVAL=60           # 每 60 秒轮询一次(避免速率限制)
            ELAPSED=0
            while :; do
              # 发起未认证的 GitHub API 请求获取 check-runs
              RAW=$(curl -sS -w "\n%{http_code}" "https://api.github.com/repos/vllm-project/vllm/commits/${READTHEDOCS_GIT_COMMIT_HASH}/check-runs?check_name=pre-run-check&filter=latest")
              HTTP_CODE=$(printf %s "$RAW" | tail -n1)
              BODY=$(printf %s "$RAW" | head -n -1)
              # 如果触发速率限制 (非 200),降级放行
              if [ "$HTTP_CODE" != "200" ]; then
                echo "GitHub API returned HTTP $HTTP_CODE (likely rate-limited); skipping pre-run-check gate."
                break
              fi
              # 提取 check-run 的 status 和 conclusion
              STATUS=$(printf %s "$BODY" | python3 -c "import sys, json; r=json.load(sys.stdin).get(\"check_runs\",[]); print((r[0].get(\"status\") or \"\") if r else \"none\")")
              CONCLUSION=$(printf %s "$BODY" | python3 -c "import sys, json; r=json.load(sys.stdin).get(\"check_runs\",[]); print((r[0].get(\"conclusion\") or \"\") if r else \"\")")
              # 若不存在 check-run,当前实现直接跳过(存在竞态风险)
              if [ "$STATUS" = "none" ]; then
                echo "no pre-run-check found for this commit; skipping gate."
                break
              fi
              # 如果已有结论,根据结论决定通过还是失败
              if [ -n "$CONCLUSION" ]; then
                echo "pre-run-check conclusion: $CONCLUSION"
                if [ "$CONCLUSION" = "failure" ] || [ "$CONCLUSION" = "cancelled" ] || [ "$CONCLUSION" = "timed_out" ]; then
                  echo "pre-run-check did not pass; failing docs build."
                  exit 1
                fi
                break
              fi
              # 超时处理:达到最大等待时间后降级放行
              if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
                echo "pre-run-check status=$STATUS after ${MAX_WAIT}s; skipping gate."
                break
              fi
              echo "pre-run-check status=$STATUS; waiting ${INTERVAL}s..."
              sleep "$INTERVAL"
              ELAPSED=$((ELAPSED + INTERVAL))
            done
          else
            # 非 PR 构建(如标签、分支推送)直接跳过门控
            echo "Not a PR build (version type=$READTHEDOCS_VERSION_TYPE); skipping pre-run-check gate."
          fi
          git fetch origin main --unshallow --no-tags --filter=blob:none || true

评论区精华

轮询间隔过短导致速率限制风险 性能

gemini-code-assist[bot] 指出原实现中 15 秒的轮询间隔对未认证的 GitHub API 请求(限制 60 次 / 小时 /IP)过于激进,vllm 活跃度高,极易触发限制导致门控降级,从而失效。

结论:作者将 INTERVAL 从 15 改为 60,MAX_WAIT 从 600 改为 300,已被采纳。 · 已解决

无 check-run 时立即放行引入竞态条件 正确性

gemini-code-assist[bot] 指出当 check-run 尚未被 GitHub Actions 注册时(文档构建先于 Actions 运行),`STATUS='none'` 分支会立即退出循环并放行,导致门控被绕过。建议继续轮询直到超时。

结论:作者未调整该逻辑,当前实现 (STATUS=none 时 break) 仍存在竞态条件。此问题未解决。 · unresolved

风险与影响

  1. 速率限制风险:虽然间隔已改为 60 秒,但若多个 PR 的文档构建同时运行(共享 ReadTheDocs IP),仍可能触发 60 次/小时的限制。当 API 返回 403 时,门控被跳过,文档构建仍会执行,效果与不设门控相同。
  2. 竞态条件风险:如 gemini-code-assist[bot] 指出的「无 check-run 时立即放行」问题未完全修复,可能导致部分情况下门控失效。
  3. 单点故障:完全依赖 GitHub API 的可用性。若 API 不可用,构建将降级为无条件构建。
  4. 脚本安全性:虽然无外部输入,但 curl 命令包含 URL 拼接,理论上若 READTHEDOCS_GIT_COMMIT_HASH 被特殊构造(不可信环境)可能存在间接注入风险。

对用户无直接影响,仅在 ReadTheDocs PR 构建中生效。对系统层面可显著减少因 pre-run-check 失败而浪费的文档构建资源(PR body 声称每小时节省 12 小时构建时间)。对团队而言降低了 CI 资源消耗,但引入了对 GitHub API 的依赖和少量维护成本。

依赖外部 API 竞态条件风险 速率限制降级

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论