跳到主要內容

使用 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

接下來是什麼?