跳到主要内容

通过 Webhook 自动发布 GitHub PR 评论

本指南会带你把 Hermes Agent 连接到 GitHub,让它在收到 webhook 事件后自动拉取 pull request 的 diff、分析代码变更,并回帖发表评论,整个过程不需要手动触发。

当 PR 被创建或更新时,GitHub 会向你的 Hermes 实例发送一个 webhook POST 请求。Hermes 会用一个提示启动 agent,提示中要求它通过 gh CLI 拉取 diff,然后再把结果发布回 PR 讨论线程。

Want a simpler setup without a public endpoint?

如果你没有公网 URL,或者只是想更快开始,推荐先看 Build a GitHub PR Review Agent。它使用 cron 定时轮询 PR,不需要公网入口,也能在 NAT 和防火墙之后运行。

Reference docs

如果你想查看完整的 webhook 平台参考(包括所有配置项、投递类型、动态订阅和安全模型),请参阅 Webhooks

Prompt injection risk

Webhook payload 中包含攻击者可控的数据,例如 PR 标题、提交信息和描述都可能带有恶意指令。如果你的 webhook 端点暴露在公网,请把网关运行在沙箱环境里(例如 Docker、SSH 后端)。详见下方的 security section


前置条件

  • 已安装并运行 Hermes Agent(hermes gateway
  • 已在网关主机上安装并认证 gh CLIgh 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}"

关键字段说明:

FieldDescription
secret(路由级)该路由的 HMAC 密钥。如果省略,则回退到全局 extra.secret
events允许接收的 X-GitHub-Event header 值列表。空列表表示全部接收
prompt模板字符串;{field}{nested.field} 会从 GitHub payload 中解析
delivergithub_comment 通过 gh pr comment 发评论;log 只写入网关日志
deliver_extra.repo解析为 payload 中的 org/repo 这样的值
deliver_extra.pr_number解析为 payload 中的 PR 编号
The payload does not contain code

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

  1. 打开你的仓库 -> Settings -> Webhooks -> Add webhook
  2. 填写以下内容:
    • Payload URL: https://your-public-url.example.com/webhooks/github-pr-review
    • Content type: application/json
    • Secret: 与路由配置中 secret 完全一致
    • Which events? -> 选择单独事件 -> 勾选 Pull requests
  3. 点击 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。

Use 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 事件,例如 openedsynchronizereopenedclosedlabeled 等。events 列表只能按 X-GitHub-Event header 做过滤,不能在路由层面按 action 子类型过滤。

第 1 步中的 prompt 已经通过自然语言约束处理了这一点:它要求 agent 在遇到 closedlabeled 时提前停止。

The agent still runs and consumes tokens

“到这里停止”的指令能阻止它继续做真正的审查,但对每一个 pull_request 事件来说,agent 仍然会被启动并消耗 token。GitHub webhook 只能按事件类型(如 pull_requestpushissues)过滤,不能按子动作(如 openedclosedlabeled)过滤。如果你的仓库事件量很大,要么接受这部分成本,要么在上游用 GitHub Actions 条件过滤后再调用 webhook URL。

不支持 Jinja2 或条件模板语法。只有 {field}{nested.field} 这两种替换形式会被解析,其它内容都会原样传给 agent。


用技能统一审查风格

你可以给路由加载一个 Hermes skill,让 agent 拥有一致的审查人格和规则。做法是在 config.yamlplatforms.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,也可以把路由中的 deliverdeliver_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 HookPush HookPipeline 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 等沙箱环境中运行它

故障排除

SymptomCheck
401 Invalid signatureconfig.yaml 中的 secret 与 GitHub webhook secret 不一致
404 Unknown routeURL 中的路由名与 routes: 里的 key 不一致
429 Rate limit exceeded某一路由超过了 30 req/min,常见于在 GitHub UI 中反复重投测试事件;稍等一分钟,或提高 extra.rate_limit
No comment postedgh 未安装、未在 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

接下来可以看什么