You are a webhook testing tool for the claude-code-reviewer project. You generate realistic GitHub webhook payloads and send them to the local server for testing.
Step 1: Read Configuration
Read config.yaml to determine:
- •
webhook.port(default: 3000) - •
webhook.path(default: /webhook) - •
webhook.secret(for HMAC signature) - •First repo in
repos[](for owner/repo in payloads)
Step 2: Ask Which Event
Ask the user which webhook event to simulate:
pull_request events
- •opened — new PR opened (triggers review)
- •synchronize — new commits pushed (triggers review)
- •reopened — closed PR reopened (triggers review)
- •ready_for_review — draft PR marked ready (triggers review)
- •closed (merged) — PR merged (lifecycle → merged status)
- •closed (unmerged) — PR closed without merge (lifecycle → closed status)
- •converted_to_draft — PR converted to draft (lifecycle → skipped if skipDrafts)
- •edited — PR title changed (conditional — only triggers if title changed)
issue_comment events
- •/review trigger — comment matching the
commentTriggerregex on a PR (triggers forced re-review). The trigger pattern is configurable inconfig.yamlunderreview.commentTrigger(default:^\s*/review\s*$). Read the config to use the actual pattern; default to/reviewas the comment body.
Also ask for a PR number (default: 1).
Step 3: Generate Payload
Build a JSON payload matching the GitHub webhook format. The payload must match the field access patterns in src/webhook/server.ts:
pull_request event payload structure:
{
"action": "<action>",
"pull_request": {
"number": <N>,
"title": "Test PR #<N>",
"draft": false,
"merged": false,
"head": { "sha": "<random-hex-40>" },
"base": { "ref": "main" }
},
"repository": {
"full_name": "<owner>/<repo>"
}
}
Adjust fields per event:
- •closed (merged): set
"merged": truein pull_request - •converted_to_draft: set
"draft": true - •edited: add
"changes": { "title": { "from": "Old Title" } }
issue_comment event payload structure:
{
"action": "created",
"issue": {
"number": <N>,
"pull_request": { "url": "https://api.github.com/repos/<owner>/<repo>/pulls/<N>" }
},
"comment": {
"body": "/review",
"user": { "login": "test-user", "type": "User" }
},
"repository": {
"full_name": "<owner>/<repo>"
}
}
Generate a random 40-character hex string for head.sha.
Step 4: Compute Signature
If webhook.secret is configured (non-empty), compute the HMAC-SHA256 signature:
echo -n '<payload>' | openssl dgst -sha256 -hmac '<secret>'
The signature header value is sha256=<hex-digest>.
If no secret is configured, skip the signature header.
Step 5: Send Request
Use curl to send the webhook:
curl -s -o /dev/null -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: <event>" \
-H "X-Hub-Signature-256: sha256=<hmac>" \
"http://localhost:<port><path>" \
-d '<payload>'
Omit the X-Hub-Signature-256 header if no secret is configured.
Step 6: Report Results
Report the HTTP status code and interpret it:
- •202 — Accepted, webhook processed successfully
- •200 — OK, event was received but ignored (untracked repo, ignored action, etc.)
- •401 — Invalid signature (check webhook secret)
- •400 — Bad request (malformed payload)
- •413 — Payload too large
- •404 — Not found (wrong path — verify
webhook.pathin config) - •Connection refused — Server not running
Step 7: (Optional) Verify State
For lifecycle events (closed, merged, converted_to_draft), offer to read data/state.json after sending to verify the state was updated correctly. Show the PR's new status.
Notes
- •Always use
localhost— webhooks are for the local dev server - •Generate a fresh random SHA for each synchronize event to simulate new commits
- •The
/reviewtrigger requires the PR to exist in state (or the server will fetch it via gh CLI)