通过 Webhook 自动发布 GitHub PR 评论
本指南会带你把 Hermes Agent 连接到 GitHub,让它在收到 webhook 事件后自动拉取 pull request 的 diff、分析代码变更,并回帖发表评论,整个过程不需要手动触发。
当 PR 被创建或更新时,GitHub 会向你的 Hermes 实例发送一个 webhook POST 请求。Hermes 会用一个提示启动 agent,提示中要求它通过 gh CLI 拉取 diff,然后再把结果发布回 PR 讨论线程。
如果你没有公网 URL,或者只是想更快开始,推荐先看 Build a GitHub PR Review Agent。它使用 cron 定时轮询 PR,不需要公网入口,也能在 NAT 和防火墙之后运行。
如果你想查看完整的 webhook 平台参考(包括所有配置项、投递类型、动态订阅和安全模型),请参阅 Webhooks。
Webhook payload 中包含攻击者可控的数据,例如 PR 标题、提交信息和描述都可能带有恶意指令。如果你的 webhook 端点暴露在公网,请把网关运行在沙箱环境里(例如 Docker、SSH 后端)。详见下方的 security section。
前置条件
- 已安装并运行 Hermes Agent(
hermes gateway) - 已在网关主机上安装并认证
ghCLI(gh auth login) - Hermes 实例有一个可从公网访问的 URL(如果你在本机运行,可参考 Local testing with ngrok)
- 你拥有 GitHub 仓库的管理员权限(用于管理 webhook)
第 1 步:启用 webhook 平台
把以下配置加入 ~/.hermes/config.yaml:
platforms:
webhook:
enabled: true
extra:
port: 8644 # default; change if another service occupies this port
rate_limit: 30 # max requests per minute per route (not a global cap)
routes:
github-pr-review:
secret: "your-webhook-secret-here" # must match the GitHub webhook secret exactly
events:
- pull_request
# The agent is instructed to fetch the actual diff before reviewing.
# {number} and {repository.full_name} are resolved from the GitHub payload.
prompt: |
A pull request event was received (action: {action}).
PR #{number}: {pull_request.title}
Author: {pull_request.user.login}
Branch: {pull_request.head.ref} → {pull_request.base.ref}
Description: {pull_request.body}
URL: {pull_request.html_url}
If the action is "closed" or "labeled", stop here and do not post a comment.
Otherwise:
1. Run: gh pr diff {number} --repo {repository.full_name}
2. Review the code changes for correctness, security issues, and clarity.
3. Write a concise, actionable review comment and post it.
deliver: github_comment
deliver_extra:
repo: "{repository.full_name}"
pr_number: "{number}"
关键字段说明:
| Field | Description |
|---|---|
secret(路由级) | 该路由的 HMAC 密钥。如果省略,则回退到全局 extra.secret |
events | 允许接收的 X-GitHub-Event header 值列表。空列表表示全部接收 |
prompt | 模板字符串;{field} 和 {nested.field} 会从 GitHub payload 中解析 |
deliver | github_comment 通过 gh pr comment 发评论;log 只写入网关日志 |
deliver_extra.repo | 解析为 payload 中的 org/repo 这样的值 |
deliver_extra.pr_number | 解析为 payload 中的 PR 编号 |
GitHub webhook payload 包含的是 PR 元数据(标题、描述、分支名、URL),不包含 diff 内容。上面的 prompt 会明确要求 agent 执行 gh pr diff 来抓取实际变更。terminal 工具已经包含在默认的 hermes-webhook toolset 中,因此不需要额外配置。
第 2 步:启动网关
hermes gateway
你应该看到:
[webhook] Listening on 0.0.0.0:8644 — routes: github-pr-review
确认它正在运行:
curl http://localhost:8644/health
# {"status": "ok", "platform": "webhook"}
第 3 步:在 GitHub 中注册 webhook
- 打开你的仓库 -> Settings -> Webhooks -> Add webhook
- 填写以下内容:
- Payload URL:
https://your-public-url.example.com/webhooks/github-pr-review - Content type:
application/json - Secret: 与路由配置中
secret完全一致 - Which events? -> 选择单独事件 -> 勾选 Pull requests
- Payload URL:
- 点击 Add webhook
GitHub 会立刻发送一个 ping 事件来验证连接。它会被安全地忽略,因为 ping 不在你的 events 列表中;返回结果会是 {"status": "ignored", "event": "ping"}。此外它只会记录在 DEBUG 级别,所以默认日志级别下控制台里看不到。
第 4 步:打开一个测试 PR
创建一个分支,推送一组变更,再打开一个 PR。通常在 30 到 90 秒内(取决于 PR 体积和模型),Hermes 就会发布一条审查评论。
如果你想实时查看 agent 的进度:
tail -f "${HERMES_HOME:-$HOME/.hermes}/logs/gateway.log"
使用 ngrok 做本地测试
如果 Hermes 运行在你的笔记本电脑上,可以用 ngrok 把它暴露出去:
ngrok http 8644
复制生成的 https://...ngrok-free.app URL,并把它作为 GitHub 的 Payload URL。免费版 ngrok 每次重启都会换地址,因此每次会话都要重新更新 GitHub webhook;付费版可以获得固定域名。
你也可以直接用 curl 对静态路由做烟雾测试,不需要 GitHub 账号,也不需要真实 PR。
deliver: log when testing locally本地测试时,建议把配置中的 deliver: github_comment 改成 deliver: log。否则 agent 会尝试对测试 payload 里的假仓库 org/repo#99 发评论,而这一定会失败。等你确认 prompt 输出满意后,再切回 deliver: github_comment。
SECRET="your-webhook-secret-here"
BODY='{"action":"opened","number":99,"pull_request":{"title":"Test PR","body":"Adds a feature.","user":{"login":"testuser"},"head":{"ref":"feat/x"},"base":{"ref":"main"},"html_url":"https://github.com/org/repo/pull/99"},"repository":{"full_name":"org/repo"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print "sha256="$2}')
curl -s -X POST http://localhost:8644/webhooks/github-pr-review \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: pull_request" \
-H "X-Hub-Signature-256: $SIG" \
-d "$BODY"
# Expected: {"status":"accepted","route":"github-pr-review","event":"pull_request","delivery_id":"..."}
然后观察 agent 执行过程:
tail -f "${HERMES_HOME:-$HOME/.hermes}/logs/gateway.log"
hermes webhook test <name> 只适用于通过 hermes webhook subscribe 创建的动态订阅,不会读取 config.yaml 中定义的静态 routes。
只处理特定 action
GitHub 会针对很多不同动作发送 pull_request 事件,例如 opened、synchronize、reopened、closed、labeled 等。events 列表只能按 X-GitHub-Event header 做过滤,不能在路由层面按 action 子类型过滤。
第 1 步中的 prompt 已经通过自然语言约束处理了这一点:它要求 agent 在遇到 closed 和 labeled 时提前停止。
“到这里停止”的指令能阻止它继续做真正的审查,但对每一个 pull_request 事件来说,agent 仍然会被启动并消耗 token。GitHub webhook 只能按事件类型(如 pull_request、push、issues)过滤,不能按子动作(如 opened、closed、labeled)过滤。如果你的仓库事件量很大,要么接受这部分成本,要么在上游用 GitHub Actions 条件过滤后再调用 webhook URL。
不支持 Jinja2 或条件模板语法。只有
{field}和{nested.field}这两种替换形式会被解析,其它内容都会原样传给 agent。
用技能统一审查风格
你可以给路由加载一个 Hermes skill,让 agent 拥有一致的审查人格和规则。做法是在 config.yaml 中 platforms.webhook.extra.routes 对应路由下添加 skills:
platforms:
webhook:
enabled: true
extra:
routes:
github-pr-review:
secret: "your-webhook-secret-here"
events: [pull_request]
prompt: |
A pull request event was received (action: {action}).
PR #{number}: {pull_request.title} by {pull_request.user.login}
URL: {pull_request.html_url}
If the action is "closed" or "labeled", stop here and do not post a comment.
Otherwise:
1. Run: gh pr diff {number} --repo {repository.full_name}
2. Review the diff using your review guidelines.
3. Write a concise, actionable review comment and post it.
skills:
- review
deliver: github_comment
deliver_extra:
repo: "{repository.full_name}"
pr_number: "{number}"
注意: 列表里只会加载第一个找到的技能。Hermes 不会叠加多个技能,后续条目会被忽略。
改为发送到 Slack 或 Discord
如果你不想把结果直接发回 GitHub,也可以把路由中的 deliver 和 deliver_extra 改成其它目标平台:
# Inside platforms.webhook.extra.routes.<route-name>:
# Slack
deliver: slack
deliver_extra:
chat_id: "C0123456789" # Slack channel ID (omit to use the configured home channel)
# Discord
deliver: discord
deliver_extra:
chat_id: "987654321012345678" # Discord channel ID (omit to use home channel)
目标平台本身也必须已经在网关中启用并连接好。如果省略 chat_id,回复会发送到该平台配置的 home channel。
合法的 deliver 值包括:log · github_comment · telegram · discord · slack · signal · sms
GitLab 支持
同一套适配器也支持 GitLab。GitLab 使用 X-Gitlab-Token 进行认证(纯字符串匹配,不是 HMAC),Hermes 会自动处理两种方式。
在事件过滤方面,GitLab 会把 X-GitLab-Event 设成 Merge Request Hook、Push Hook、Pipeline Hook 之类的值,因此你需要在 events 中填精确 header 值:
events:
- Merge Request Hook
GitLab 的 payload 字段和 GitHub 不同,例如 MR 标题是 {object_attributes.title},MR 编号是 {object_attributes.iid}。要了解完整 payload 结构,最简单的方法是使用 GitLab webhook 设置里的 Test 按钮,再结合 Recent Deliveries 日志查看。另一种办法是直接在路由配置中省略 prompt,这样 Hermes 会把完整 payload 作为格式化 JSON 直接交给 agent,随后你就能从 deliver: log 的网关日志中看到 agent 对其结构的总结。
安全说明
- 生产环境中绝不要使用
INSECURE_NO_AUTH。它会完全关闭签名校验,只适合本地开发 - 定期轮换 webhook secret,并同时更新 GitHub 和
config.yaml - 速率限制 默认是每路由 30 req/min(通过
extra.rate_limit可配置);超出时会返回429 - 重复投递(webhook retry)会通过 1 小时幂等缓存去重。缓存 key 优先用
X-GitHub-Delivery,其次是X-Request-ID,最后才是毫秒时间戳。如果两种 delivery ID header 都不存在,则不会去重 - 提示注入风险: PR 标题、描述和提交信息都可被攻击者控制。恶意 PR 可能试图操纵 agent 的行为。如果你的网关暴露在公网,请在 Docker 或 VM 等沙箱环境中运行它
故障排除
| Symptom | Check |
|---|---|
401 Invalid signature | config.yaml 中的 secret 与 GitHub webhook secret 不一致 |
404 Unknown route | URL 中的路由名与 routes: 里的 key 不一致 |
429 Rate limit exceeded | 某一路由超过了 30 req/min,常见于在 GitHub UI 中反复重投测试事件;稍等一分钟,或提高 extra.rate_limit |
| No comment posted | gh 未安装、未在 PATH 中,或未认证(gh auth login) |
| Agent runs but no comment | 查看 gateway log;如果 agent 输出为空或只是 "SKIP",仍然会尝试投递 |
| Port already in use | 修改 config.yaml 中的 extra.port |
| Agent runs but reviews only the PR description | 说明 prompt 里没包含 gh pr diff 指令,因为 webhook payload 本身不带 diff |
| Can't see the ping event | 被忽略的事件只会以 DEBUG 级别返回 {"status":"ignored","event":"ping"};请去 GitHub 的 delivery log 查看 |
GitHub 的 Recent Deliveries 标签页(仓库 -> Settings -> Webhooks -> 你的 webhook)会显示每次投递的完整 request headers、payload、HTTP 状态和响应体。这通常是定位问题最快的方法,无需先翻服务器日志。
完整配置参考
platforms:
webhook:
enabled: true
extra:
host: "0.0.0.0" # bind address (default: 0.0.0.0)
port: 8644 # listen port (default: 8644)
secret: "" # optional global fallback secret
rate_limit: 30 # requests per minute per route
max_body_bytes: 1048576 # payload size limit in bytes (default: 1 MB)
routes:
<route-name>:
secret: "required-per-route"
events: [] # [] = accept all; otherwise list X-GitHub-Event values
prompt: "" # {field} / {nested.field} resolved from payload
skills: [] # first matching skill is loaded (only one)
deliver: "log" # log | github_comment | telegram | discord | slack | signal | sms
deliver_extra: {} # repo + pr_number for github_comment; chat_id for others
接下来可以看什么
- Cron-Based PR Reviews — 用定时轮询的方式做 PR 审查,不需要公网入口
- Webhook Reference — webhook 平台完整配置参考
- Build a Plugin — 把审查逻辑封装成可共享插件
- Profiles — 运行一个拥有独立记忆与配置的专用审查 profile