首页 › tutorials › 造CI机器人自动审查每个PR

概述

本教程带你用 Claude Code 的「自动化三件套」——无头 -p 模式、生命周期 hooks、批量处理——从零搭建一个每个 PR 自动审查的 GitHub Actions 机器人。学完你将掌握:用 stream-json 解析审查结论、用 PreToolUse 钩子把审查范围锁死在 PR 改动文件、用 --max-turns 控制成本与防失控,以及把审查结果回写成 PR 行内评论。适合已装通 Claude Code、想把它接进团队流水线的开发者与 DevOps 工程师。

前置条件


第一步:理解自动化三件套与两条路径

把 Claude Code 接进 CI 有两条路径,理解它们的分工是本教程的基石:

路径 本质 适合场景
官方 action anthropics/claude-code-action@v1 一层封装,把 -p 模式 + diff 收集 + 评论回写打包好 生产环境,快速上线
原生 CLI claude -p ... --output-format stream-json 直接用底层能力,自己拼管道 想精细控制、本地干跑、自定义流程
flowchart LR
    A[PR 推送] --> B[GitHub Actions 触发]
    B --> C{选路径}
    C -->|官方 action| D[claude-code-action@v1]
    C -->|原生 CLI| E[ci/review.sh]
    D --> F[-p 无头模式]
    E --> F
    F --> G[PreToolUse 钩子锁范围]
    G --> H[批量审查改动文件]
    H --> I[stream-json 输出]
    I --> J[解析 finding]
    J --> K[gh 回写行内评论]
    K --> L[Stop 钩子通知/计费]

本教程先教原生 CLI 路径(吃透三件套),再给官方 action 的生产级写法。这样无论 action 升级与否,你都能自己维护。


第二步:用 -p 模式本地跑通单次审查

无头模式的核心是 -p(即 --print):不进交互 TUI,直接吃一句 prompt、吐一份结果。配合 --output-format stream-json,输出变成换行分隔的 JSON 事件流,方便程序解析。

先在本地验证。准备一个 REVIEW.md 作为审查准则(Claude 会读它):

# 代码审查准则
- 禁止在代码中硬编码密钥、token、密码
- 所有公开函数必须有类型注解
- SQL 查询必须使用参数化,禁止字符串拼接
- 错误必须被处理,禁止空 except / catch 后吞掉

然后跑一次干跑(PR_DIFF 假设是当前分支相对 main 的 diff):

# 收集改动文件清单
git fetch origin main
git diff --name-only origin/main...HEAD > /tmp/changed.txt

# 把 diff 喂给 Claude,要求 stream-json 输出
claude -p "$(cat <<'EOF'
你是资深代码审查员。请审查以下 Git diff,严格依据 REVIEW.md 准则。
只对违反准则的代码提 finding,不要泛泛而谈。
每条 finding 用如下 JSON 结构(无 finding 则输出空数组):
{"findings":[{"file":"路径","line":12,"severity":"high|med|low","rule":"违反的准则","suggestion":"修复建议"}]}

改动文件:
$(cat /tmp/changed.txt)

完整 diff:
$(git diff origin/main...HEAD)
EOF
)" --model claude-sonnet-4-6 \
  --output-format stream-json \
  --max-turns 6 \
  --include-partial-messages

预期结果:终端逐行打印 JSON。关键事件类型有三类:

常见问题:


第三步:配 CI 护栏——用 hooks 锁死审查范围

在 CI 里最怕两件事:审查员乱改文件(review 任务不该写代码)、跑飞烧钱。hooks 就是确定性护栏——它由 harness 执行,不依赖模型「听话」。

在仓库根目录写 .claude/settings.json(项目级配置,CI 会自动加载):

{
  "permissions": {
    "allow": ["Bash(git diff:*)", "Bash(git show:*)", "Read(*)"],
    "deny": ["Bash(git push:*)", "Bash(rm -rf:*)"]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [{ "type": "command", "command": "node .ci/guard-no-write.mjs" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "node .ci/guard-bash.mjs" }]
      }
    ],
    "Stop": [
      { "hooks": [{ "type": "command", "command": "node .ci/on-stop.mjs" }] }
    ]
  }
}

钩子从 stdin 拿到一段 JSON,包含 session_id、tool_name、tool_input 等。返回方式有两种:退出码 0(放行,stdout 作为上下文回喂模型)或退出码 2(阻断,stderr 回喂,模型会重试)。PreToolUse 还能返回结构化决策 permissionDecision: allow/deny/ask。

写审查护栏 .ci/guard-no-write.mjs(审查员只能读、不能写):

// .ci/guard-no-write.mjs —— 审查机器人禁止任何写操作
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
const file = input.tool_input?.file_path ?? "";

// 审查场景下,写任何文件都拒绝,并告诉模型原因(它会改为只读)
console.log(JSON.stringify({
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny",
    permissionDecisionReason:
      "审查机器人只读。请勿修改文件,把建议写进 finding 的 suggestion 字段。"
  }
}));
process.exit(0);

再写 .ci/guard-bash.mjs(Bash 命令白名单):

// .ci/guard-bash.mjs —— 只允许只读 git 命令,挡住 push/rm 等
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
const cmd = input.tool_input?.command ?? "";
const blocked = /\b(git\s+push|rm\s+-rf|curl\s+.*\|\s*sh|sudo)\b/;
if (blocked.test(cmd)) {
  console.error(`CI 审查禁止执行: ${cmd.slice(0, 80)}`);
  process.exit(2); // 退出码 2 = 阻断并回喂 stderr
}
process.exit(0);

最后写 .ci/on-stop.mjs,在 Claude 结束时打印成本(事件里有 stop_hook_active,注意避免递归):

// .ci/on-stop.mjs —— 会话结束上报成本
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync(0, "utf8"));
console.log(`::notice::审查完成 session=${input.session_id?.slice(0,8)}`);
process.exit(0);

常见问题:


第四步:写 GitHub Actions 工作流

把上面三步串成 CI。关键是用 actions/checkout 的 fetch-depth: 0 拿到完整 diff,再把改动喂给审查脚本。

# .github/workflows/claude-review.yml
name: Claude PR Review
on:
  pull_request:
    types: [opened, synchronize, reopened]
  # 节省成本:草稿 PR 不跑
jobs:
  review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write   # 回写评论需要
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0      # 拿全历史才能 diff
      - uses: actions/setup-node@v4
        with: { node-version: '20' }

      - name: 运行 Claude 审查
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: bash ci/review.sh

ci/review.sh 是把第二步的本地命令 CI 化,并调用第四步的钩子配置(.claude/settings.json 自动加载):

#!/usr/bin/env bash
set -euo pipefail

BASE="origin/${GITHUB_BASE_REF:-main}"
HEAD="origin/${GITHUB_HEAD_REF:-HEAD}"
git fetch origin "$GITHUB_BASE_REF" 2>/dev/null || true

# 改动文件(只看代码,过滤 lock/distro)
FILES=$(git diff --name-only --diff-filter=AM "$BASE...$HEAD" \
        | grep -iE '\.(ts|tsx|js|jsx|py|go|java|rb)$' || true)
[ -z "$FILES" ] && { echo "无代码改动,跳过"; exit 0; }

DIFF=$(git diff "$BASE...$HEAD")

# stream-json 流式输出,取最后一条 result 事件里的文本
OUT=$(claude -p "依据 REVIEW.md 审查下列 diff,仅对违规项输出 JSON 数组:\
[{\"file\",\"line\",\"severity\",\"rule\",\"suggestion\"}],无则输出 []。
改动文件:$FILES
完整 diff:$DIFF" \
  --model claude-sonnet-4-6 \
  --output-format stream-json \
  --max-turns 6)

# 取最后一条 result 事件的 result 字段
PAYLOAD=$(echo "$OUT" | jq -rc 'select(.type=="result") | .result' | tail -n1)
echo "$PAYLOAD" | jq '.findings // []' > /tmp/findings.json

# 回写评论
node ci/post-comments.mjs "$PR_NUMBER" /tmp/findings.json

第五步:把审查结果回写成 PR 行内评论

审查的价值在于精确到行。.ci/post-comments.mjs 用 GitHub 的 line-position 评论 API,把每条 finding 钉到具体行:

// .ci/post-comments.mjs <pr_number> <findings.json>
import { readFileSync } from "node:fs";
const [,, prStr, findingsPath] = process.argv;
const pr = prStr;
const findings = JSON.parse(readFileSync(findingsPath, "utf8"));

if (!findings.length) {
  console.log("✅ 未发现违规项");
  process.exit(0);
}

// 汇总成一条 PR 评论(行内评论需要 commit_sha 较繁琐,这里用汇总评论稳妥落地)
const lines = findings.map((f, i) =>
  `| ${i+1} | \`${f.file}:${f.line}\` | **${f.severity}** | ${f.rule} | ${f.suggestion} |`);

const body = [
  "## 🤖 Claude 自动审查结果",
  "",
  "| # | 位置 | 级别 | 准则 | 建议 |",
  "|---|------|------|------|------|",
  ...lines,
  "",
  "_由 Claude Code CI 机器人生成,依据 `REVIEW.md`。_"
].join("\n");

const { execSync } = await import("node:child_process");
execSync(`gh pr comment ${pr} --body '${body.replace(/'/g, "'\\''")}'`,
  { stdio: "inherit", env: process.env });

预期结果:PR 上出现一条表格评论,每行一个 finding,含文件、行号、级别、违反准则、修复建议。无违规时不出评论(或出 ✅)。

常见问题:


第六步:批量、并发与成本控制

当 PR 改动几十个文件时,三个手段保住账单:

  1. 预过滤:grep 只挑代码文件,过滤 *.lock、dist/、生成代码(见第四步)。
  2. 并发审查:把文件按目录切片,开多个 -p 进程并行跑,用 xargs -P:
cat /tmp/changed.txt | xargs -I{} -P 4 bash -c '
  claude -p "审查 {},依据 REVIEW.md,输出 JSON findings" \
    --output-format json --max-turns 4 > "/tmp/rev_$(echo {} | tr / _).json"
'
  1. 硬性成本上限:--max-turns 是第一道闸(单次会话轮次);再叠环境变量限输出 token,以及 workflow 级 timeout-minutes:
- name: 运行 Claude 审查
  timeout-minutes: 10
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    CLAUDE_CODE_MAX_OUTPUT_TOKENS: "8000"
  run: bash ci/review.sh

省钱要点:审查是「读密集、结论短」任务,用 claude-sonnet-4-6 而非 opus;--max-turns 控在 4–8;非代码改动直接跳过。社区经验:每月 50 个 PR 的团队,API 成本通常在个位数美元量级。


实战案例:从零交付一个可运行仓库

把上面所有片段落成一个完整项目,目录结构如下:

my-repo/
├── .github/workflows/claude-review.yml   # 第四步
├── .claude/settings.json                  # 第三步
├── ci/
│   ├── guard-no-write.mjs                 # 第三步钩子
│   ├── guard-bash.mjs                     # 第三步钩子
│   ├── on-stop.mjs                        # 第三步钩子
│   ├── review.sh                          # 第四步主脚本
│   └── post-comments.mjs                  # 第五步
└── REVIEW.md                              # 第二步审查准则

生产化替代(官方 action):若不想自己维护脚本,直接用官方封装,等价于把第二~五步打包好:

- uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    claude_args: '--model claude-sonnet-4-6 --max-turns 20'
    prompt: |
      审查本 PR 改动文件,严格依据 REVIEW.md。
      对每个违规项在对应代码行留行内评论,标明级别与修复建议。
      无违规则留一条 ✅ 评论。

验证清单:

  1. 本地干跑:bash ci/review.sh(先 export PR_NUMBER=1)应输出 findings.json
  2. 推一个含硬编码 token 的测试 PR → 应被标 high finding
  3. 推一个纯文档改动 PR → 应跳过(无代码文件)
  4. 在 CI 日志确认 PreToolUse 钩子加载、::notice::审查完成 上报

训练营适用性评估

维度 评估
适合训练营 DevOps / SRE / 平台工程 / 全栈进阶营
学员基础 装通 Claude Code + 会 Git/GitHub Actions 基础
预计教学时长 引导式 3–4 小时(含动手跑通);自学 6–8 小时
难点 hooks 的 JSON 协议、stream-json 解析、行内评论定位
可裁剪 原生 CLI 路径(重点教三件套)或官方 action 路径(快速落地)二选一

延伸阅读

参考来源

  1. How to Run Claude Code in a GitHub Action for Autonomous PR Review — claude-code-action@v1 自主审查 YAML 与 claude_args 模型选择实践
  2. Claude Code in GitHub Actions: A Complete Guide to Automated PR Fixes — 自动修 PR、CI 失败修复、成本与失控护栏
  3. Hooks reference - Claude Code Docs — hook 事件、JSON schema、退出码 0/2 与 permissionDecision 协议
  4. claude-code-hooks-schemas (Gist) — 各 hook 的 stdin 输入与结构化输出实现级参考
  5. Using Claude Code Programmatically (Gist) — -p / stream-json 程序化调用与事件类型解析

本文由微信公众号「51学通信」站长爱卫生(微信:gprshome201101)整理输出。掌握 Claude Code 的 -p 模式、hooks 与批量处理三件套,你就能把 AI 审查固化进团队流水线,让每个 PR 都被 deterministic 的护栏与高质量模型双重把关。

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入