跳到主要内容

使用 Webhook 实现自动化的 GitHub PR 评论

本指南将引导你将 Hermes Agent 连接到 GitHub,使其能够自动获取拉取请求(Pull Request, PR)的差异(diff),分析代码变更,并发布评论——这一切均由 webhook 事件触发,无需人工干预。

当 PR 被打开或更新时,GitHub 会向你的 Hermes 实例发送一个 webhook POST 请求。Hermes 会使用一个提示词(prompt)运行 agent,指示它通过 gh CLI 获取差异内容,并将响应发布回 PR 讨论区。

想要更简单的设置且无需公网端点?

如果你没有公网 URL 或者只是想快速开始,请查看构建 GitHub PR 审查 Agent——它使用 cron 作业按计划轮询 PR,可在 NAT 和防火墙后方工作。

参考文档

有关完整的 webhook 平台参考(所有配置选项、交付类型、动态订阅、安全模型),请参阅 Webhooks

提示词注入风险

Webhook 载荷包含攻击者可控的数据——PR 标题、提交消息和描述可能包含恶意指令。当你的 webhook 端点暴露在互联网上时,请在沙箱环境(Docker、SSH 后端)中运行网关。请参阅下方的安全说明


前提条件

  • 已安装并运行 Hermes Agent (hermes gateway)
  • 在网关主机上已安装并认证 gh CLI (gh auth login)
  • 你的 Hermes 实例拥有一个可公开访问的 URL(如果在本地运行,请参阅使用 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}"

关键字段:

字段描述
secret(路由级别)此路由的 HMAC 密钥。如果省略,则回退到全局 extra.secret
events要接受的 X-GitHub-Event 头值列表。空列表 = 接受所有事件。
prompt模板;{field}{nested.field} 将从 GitHub 载荷中解析。
delivergithub_comment 通过 gh pr comment 发布评论。log 仅写入网关日志。
deliver_extra.repo从载荷中解析为例如 org/repo
deliver_extra.pr_number从载荷中解析为 PR 编号。
载荷不包含代码

GitHub webhook 载荷包括 PR 元数据(标题、描述、分支名称、URL),但不包含差异内容。上述提示词指示 agent 运行 gh pr diff 来获取实际的变更。terminal 工具包含在默认的 hermes-webhook 工具集中,因此无需额外配置。


步骤 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(设置)→ WebhooksAdd webhook(添加 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 层级中,每次 ngrok 重启时 URL 都会更改——请在每个会话中更新你的 GitHub webhook。付费的 ngrok 账户可获得静态域名。

你可以直接使用 curl 对静态路由进行冒烟测试——无需 GitHub 账户或真实的 PR。

在本地测试时使用 deliver: log

在测试期间,将配置中的 deliver: github_comment 更改为 deliver: log。否则,agent 将尝试向测试载荷中虚构的 org/repo#99 仓库发布评论,这将会失败。当你对提示词输出满意后,再切换回 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 中的路由。


过滤特定操作

GitHub 会为许多操作发送 pull_request 事件:openedsynchronizereopenedclosedlabeled 等。events 列表仅根据 X-GitHub-Event 标头值进行过滤——它无法在路由级别按操作子类型进行过滤。

步骤 1 中的提示已通过指示代理针对 closedlabeled 事件提前停止来处理此问题。

代理仍然运行并消耗令牌

“在此停止”指令可防止进行有意义的审查,但无论操作如何,代理仍会为每个 pull_request 事件运行至完成。GitHub Webhook 只能按事件类型(pull_requestpushissues 等)过滤,而不能按操作子类型(openedclosedlabeled)过滤。不存在针对子操作的路由级过滤器。对于高流量的仓库,请接受此成本,或使用有条件调用你的 Webhook URL 的 GitHub Actions 工作流在上游进行过滤。

不存在 Jinja2 或条件模板语法。{field}{nested.field} 是唯一支持的替换项。任何其他内容都将逐字传递给代理。


使用技能保持一致的审查风格

加载 Hermes 技能 以赋予代理一致的审查角色。在 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

用目标平台替换路由中的 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,响应将发送到该平台配置的主频道。

有效的 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 中使用精确的标头值:

events:
- Merge Request Hook

GitLab 的有效负载字段与 GitHub 的不同——例如,MR 标题使用 {object_attributes.title},MR 编号使用 {object_attributes.iid}。发现完整有效负载结构的最简单方法是结合使用 Webhook 设置中的 GitLab Test 按钮和 Recent Deliveries(最近交付)日志。或者,从你的路由配置中省略 prompt——Hermes 随后会将格式化的 JSON 完整有效负载直接传递给代理,而代理的响应(在带有 deliver: log 的网关日志中可见)将描述其结构。


安全说明

  • 切勿在生产环境中使用 INSECURE_NO_AUTH——它会完全禁用签名验证。它仅用于本地开发。
  • 定期轮换你的 Webhook 密钥,并在 GitHub(Webhook 设置)和你的 config.yaml 中更新它。
  • 速率限制默认为每路由 30 请求/分钟(可通过 extra.rate_limit 配置)。超出限制将返回 429
  • 重复交付(Webhook 重试)通过 1 小时幂等性缓存进行去重。缓存键依次为 X-GitHub-Delivery(如果存在)、X-Request-ID,然后是毫秒时间戳。当未设置任何交付 ID 标头时,重试不会去重。
  • 提示注入: PR 标题、描述和提交消息由攻击者控制。恶意 PR 可能会尝试操纵代理的操作。当暴露在公共互联网上时,请在沙箱环境(Docker、VM)中运行网关。

故障排除

症状检查项
401 Invalid signatureconfig.yaml 中的密钥与 GitHub Webhook 密钥不匹配
404 Unknown routeURL 中的路由名称与 routes: 中的键不匹配
429 Rate limit exceeded超过每路由 30 请求/分钟的限制——在从 GitHub UI 重新交付测试事件时很常见;等待一分钟或提高 extra.rate_limit
未发布评论未安装 gh、不在 PATH 中或未进行身份验证(gh auth login
代理运行但未发表评论检查网关日志——如果代理输出为空或仅为 "SKIP",仍会尝试交付
端口已被占用更改 config.yaml 中的 extra.port
代理运行但仅审查 PR 描述提示未包含 gh pr diff 指令——差异信息不在 Webhook 有效负载中
看不到 ping 事件被忽略的事件仅在 DEBUG 日志级别返回 {"status":"ignored","event":"ping"}——检查 GitHub 的交付日志(仓库 → Settings → Webhooks → 你的 Webhook → Recent Deliveries)

GitHub 的 Recent Deliveries(最近交付)选项卡(仓库 → Settings → Webhooks → 你的 Webhook)显示每次交付的确切请求标头、有效负载、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

接下来是什么?