feat: GitHub webhook route for PR comment feedback#266
Conversation
Add /api/coding-agent/github to receive GitHub issue_comment webhooks. When @recoup-coding-agent is mentioned on a PR comment: - Verifies webhook signature (GITHUB_WEBHOOK_SECRET) - Fetches PR branch, looks up shared Redis PR state - Triggers update-pr task with the feedback - Posts acknowledgment comment on the PR New files: - app/api/coding-agent/github/route.ts - lib/coding-agent/handleGitHubWebhook.ts - lib/coding-agent/verifyGitHubWebhook.ts MYC-4438 Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (5)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR introduces GitHub webhook integration for the coding-agent system, enabling PR comment-triggered updates. It adds an API endpoint, webhook signature verification, request handling logic, and required environment variable validation for webhook secret management. Changes
Sequence DiagramsequenceDiagram
participant GH as GitHub
participant API as API Route
participant Verify as Verify Webhook
participant Handler as Webhook Handler
participant GHAPI as GitHub API
participant State as PR State
participant Task as Task Trigger
GH->>API: POST /api/coding-agent/github<br/>(webhook event)
API->>Verify: verifyGitHubWebhook(body, signature, secret)
Verify-->>API: boolean (valid signature?)
alt Signature Invalid
API-->>GH: 401 Unauthorized
else Signature Valid
API->>Handler: handleGitHubWebhook(request)
Handler->>GHAPI: Fetch PR details
GHAPI-->>Handler: PR metadata
Handler->>State: Read stored PR state
alt State Not Found
Handler-->>API: no_state response
else State Exists
Handler->>State: Update state to "updating"
Handler->>Task: triggerUpdatePR(feedback, PR metadata)
Task-->>Handler: Task created
Handler->>GHAPI: Post confirmation comment
GHAPI-->>Handler: Comment posted
Handler-->>API: update_triggered response
end
API-->>GH: 200 OK (JSON response)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| }); | ||
|
|
||
| const [owner, repoName] = repo.split("/"); | ||
| await fetch(`https://api.github.com/repos/${owner}/${repoName}/issues/${prNumber}/comments`, { |
There was a problem hiding this comment.
SRP - new lib for the fetch @recoup-coding-agent
Handle both issue_comment and pull_request_review_comment webhook events. Review comments include the PR branch directly in the payload, avoiding an extra API call to fetch PR details. Co-Authored-By: Claude Opus 4.6 <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/coding-agent/validateEnv.ts (1)
1-8:⚠️ Potential issue | 🟠 MajorSplit webhook-only env checks out of the shared bot validator.
lib/coding-agent/bot.tscallsvalidateCodingAgentEnv()during singleton initialization, so addingGITHUB_WEBHOOK_SECREThere makes any bot import fail in environments that never expose/api/coding-agent/github. A dedicated webhook validator called fromhandleGitHubWebhook()keeps the non-webhook path working and still fails fast for this endpoint.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/coding-agent/validateEnv.ts` around lines 1 - 8, Remove the webhook-only variable GITHUB_WEBHOOK_SECRET from the shared REQUIRED_ENV_VARS array used by validateCodingAgentEnv so bot initialization won't fail in non-webhook environments; create a new function (e.g., validateGitHubWebhookEnv or validateCodingAgentWebhookEnv) that checks webhook-specific vars including GITHUB_WEBHOOK_SECRET, and call that new validator from handleGitHubWebhook instead of validateCodingAgentEnv; ensure validateCodingAgentEnv keeps only vars required for the general bot (SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, GITHUB_TOKEN, REDIS_URL, CODING_AGENT_CALLBACK_SECRET) and update any imports/usages to call the webhook validator only in the webhook handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/coding-agent/handleGitHubWebhook.ts`:
- Around line 77-88: Before calling triggerUpdatePR, you currently set the PR to
"updating" via setCodingAgentPRState and if triggerUpdatePR throws the PR
remains stuck; wrap the triggerUpdatePR call in a try/catch and on error call
setCodingAgentPRState(repo, branch, { ...prState, status: previousStatus }) or
set status:"failed" (using prState to get previousStatus) and rethrow or handle
the error so the PR is not left permanently "updating"; ensure you reference the
same repo/branch/prNumber context when logging or restoring state.
- Around line 61-88: The current flow uses getCodingAgentPRState and then
setCodingAgentPRState which allows race conditions from duplicate webhooks;
change this to an atomic check-and-set in the prState layer (e.g., implement and
call a Redis-backed lock or CAS helper such as acquireCodingAgentPRLock /
updateCodingAgentPRStateIfStatus) that verifies status === "pr_created" and
snapshotId/prs exist and atomically sets status to "updating"; only if the
CAS/lock succeeds should you call triggerUpdatePR (keep the same payload
including snapshotId, branch, repo, feedback and callbackThreadId) and release
the lock if used.
- Around line 29-41: Add Zod-based validation for the webhook body and use it
before accessing nested fields: create validateGitHubWebhookBody.ts exporting a
Zod schema and inferred type, then import and run
validateGitHubWebhookBody.parse/body safe-parse in handleGitHubWebhook.ts
(before JSON.parse or reading payload.repository.full_name, BOT_MENTION checks,
prNumber etc.); if parsing/shape validation fails return NextResponse.json({
status: "bad_request", error: "<validation message>" }, { headers:
getCorsHeaders() }) with 400, otherwise proceed using the typed payload to avoid
runtime throws.
- Around line 35-38: The handler currently only checks commentBody for
BOT_MENTION and accepts any commenter; add validation of
payload.comment?.author_association (default "NONE") and reject requests unless
it is an allowed role (e.g., "OWNER","MEMBER","COLLABORATOR"). Update
handleGitHubWebhook logic immediately after reading commentBody/BOT_MENTION:
read author_association, compare against the allowlist, and return the same
NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() }) when not
authorized. Ensure you reference payload.comment?.author_association and keep
the existing BOT_MENTION and getCorsHeaders flow.
In `@lib/coding-agent/verifyGitHubWebhook.ts`:
- Around line 23-29: The string equality check returning `signature ===
expected` leaks timing information; replace it with a constant-time comparison
using Node's crypto.timingSafeEqual: convert the computed HMAC (`sig` / `hex` /
`expected`) and the incoming `signature` into Buffer/Uint8Array with the same
encoding, then call `crypto.timingSafeEqual(bufferA, bufferB)` and return that
result; ensure you import/require Node's `crypto` if not already present and
handle mismatched lengths by normalizing lengths or early-returning false to
avoid exceptions.
---
Outside diff comments:
In `@lib/coding-agent/validateEnv.ts`:
- Around line 1-8: Remove the webhook-only variable GITHUB_WEBHOOK_SECRET from
the shared REQUIRED_ENV_VARS array used by validateCodingAgentEnv so bot
initialization won't fail in non-webhook environments; create a new function
(e.g., validateGitHubWebhookEnv or validateCodingAgentWebhookEnv) that checks
webhook-specific vars including GITHUB_WEBHOOK_SECRET, and call that new
validator from handleGitHubWebhook instead of validateCodingAgentEnv; ensure
validateCodingAgentEnv keeps only vars required for the general bot
(SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, GITHUB_TOKEN, REDIS_URL,
CODING_AGENT_CALLBACK_SECRET) and update any imports/usages to call the webhook
validator only in the webhook handler.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f9fd764f-e962-48ad-9e63-50b804c891eb
⛔ Files ignored due to path filters (4)
lib/coding-agent/__tests__/bot.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/handleGitHubWebhook.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/validateEnv.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/verifyGitHubWebhook.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (4)
app/api/coding-agent/github/route.tslib/coding-agent/handleGitHubWebhook.tslib/coding-agent/validateEnv.tslib/coding-agent/verifyGitHubWebhook.ts
| const payload = JSON.parse(body); | ||
|
|
||
| if (event !== "issue_comment" || payload.action !== "created" || !payload.issue?.pull_request) { | ||
| return NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| const commentBody: string = payload.comment?.body ?? ""; | ||
| if (!commentBody.includes(BOT_MENTION)) { | ||
| return NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| const repo: string = payload.repository.full_name; | ||
| const prNumber: number = payload.issue.number; |
There was a problem hiding this comment.
Validate the webhook payload before reading nested fields.
A signed but malformed body will throw on JSON.parse or payload.repository.full_name, which turns bad input into a 500. Parse this through a validateGitHubWebhookBody schema and return 400 on parse/shape failures. As per coding guidelines, "All API endpoints should use a validate function for input parsing using Zod for schema validation" and "Create validate functions in validate<EndpointName>Body.ts or validate<EndpointName>Query.ts files that export both the schema and inferred TypeScript type."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/coding-agent/handleGitHubWebhook.ts` around lines 29 - 41, Add Zod-based
validation for the webhook body and use it before accessing nested fields:
create validateGitHubWebhookBody.ts exporting a Zod schema and inferred type,
then import and run validateGitHubWebhookBody.parse/body safe-parse in
handleGitHubWebhook.ts (before JSON.parse or reading
payload.repository.full_name, BOT_MENTION checks, prNumber etc.); if
parsing/shape validation fails return NextResponse.json({ status: "bad_request",
error: "<validation message>" }, { headers: getCorsHeaders() }) with 400,
otherwise proceed using the typed payload to avoid runtime throws.
| const commentBody: string = payload.comment?.body ?? ""; | ||
| if (!commentBody.includes(BOT_MENTION)) { | ||
| return NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and read the handleGitHubWebhook.ts file
find . -name "handleGitHubWebhook.ts" -type fRepository: recoupable/api
Length of output: 99
🏁 Script executed:
# Let's explore the structure around lib/coding-agent/
git ls-files | grep -E "lib/coding-agent|webhooks?" | head -20Repository: recoupable/api
Length of output: 1016
🏁 Script executed:
# Search for how GitHub webhook payloads are handled and if permission checks exist
rg "OWNER|MEMBER|COLLABORATOR|author_association" -t ts -t tsxRepository: recoupable/api
Length of output: 85
🏁 Script executed:
# Search for any existing GitHub permission validation logic
rg "permission|authorization|access" lib/coding-agent/ -t ts -A 3Repository: recoupable/api
Length of output: 40
🏁 Script executed:
# Read the handleGitHubWebhook.ts file
cat -n ./lib/coding-agent/handleGitHubWebhook.tsRepository: recoupable/api
Length of output: 4315
🏁 Script executed:
# Search for permission checking patterns without the tsx flag
rg "OWNER|MEMBER|COLLABORATOR|author_association" -A 2 -B 2Repository: recoupable/api
Length of output: 40
🏁 Script executed:
# Search for permission/authorization logic more broadly
rg "permission|authorization|access" lib/coding-agent/ -A 2Repository: recoupable/api
Length of output: 40
🏁 Script executed:
# Check if there's any verification of comment author in tests
cat -n ./lib/coding-agent/__tests__/handleGitHubWebhook.test.tsRepository: recoupable/api
Length of output: 6904
Validate commenter permissions before accepting feedback.
The webhook only checks for the bot mention, allowing any account that can comment on the PR to trigger the update-pr workflow. The GitHub webhook payload includes author_association (OWNER, MEMBER, COLLABORATOR, etc.)—validate this field to ensure only authorized users can submit feedback to the agent.
const authorAssociation = payload.comment?.author_association ?? "NONE";
if (!["OWNER", "MEMBER", "COLLABORATOR"].includes(authorAssociation)) {
return NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/coding-agent/handleGitHubWebhook.ts` around lines 35 - 38, The handler
currently only checks commentBody for BOT_MENTION and accepts any commenter; add
validation of payload.comment?.author_association (default "NONE") and reject
requests unless it is an allowed role (e.g., "OWNER","MEMBER","COLLABORATOR").
Update handleGitHubWebhook logic immediately after reading
commentBody/BOT_MENTION: read author_association, compare against the allowlist,
and return the same NextResponse.json({ status: "ignored" }, { headers:
getCorsHeaders() }) when not authorized. Ensure you reference
payload.comment?.author_association and keep the existing BOT_MENTION and
getCorsHeaders flow.
| const prState = await getCodingAgentPRState(repo, branch); | ||
|
|
||
| if (!prState) { | ||
| return NextResponse.json({ status: "no_state" }, { headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| if (prState.status === "running" || prState.status === "updating") { | ||
| return NextResponse.json({ status: "busy" }, { headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| if (prState.status !== "pr_created" || !prState.snapshotId || !prState.prs?.length) { | ||
| return NextResponse.json({ status: "no_state" }, { headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| const feedback = commentBody.replace(BOT_MENTION, "").trim(); | ||
|
|
||
| await setCodingAgentPRState(repo, branch, { | ||
| ...prState, | ||
| status: "updating", | ||
| }); | ||
|
|
||
| const handle = await triggerUpdatePR({ | ||
| feedback, | ||
| snapshotId: prState.snapshotId, | ||
| branch: prState.branch, | ||
| repo: prState.repo, | ||
| callbackThreadId: `github:${repo}:${prNumber}`, | ||
| }); |
There was a problem hiding this comment.
Make the busy check and state transition atomic.
getCodingAgentPRState() and setCodingAgentPRState() are plain Redis get/set calls, so duplicate webhook deliveries can both observe pr_created, both write updating, and both call triggerUpdatePR(). Move this behind a Redis lock/CAS in the prState layer before enqueueing the task.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/coding-agent/handleGitHubWebhook.ts` around lines 61 - 88, The current
flow uses getCodingAgentPRState and then setCodingAgentPRState which allows race
conditions from duplicate webhooks; change this to an atomic check-and-set in
the prState layer (e.g., implement and call a Redis-backed lock or CAS helper
such as acquireCodingAgentPRLock / updateCodingAgentPRStateIfStatus) that
verifies status === "pr_created" and snapshotId/prs exist and atomically sets
status to "updating"; only if the CAS/lock succeeds should you call
triggerUpdatePR (keep the same payload including snapshotId, branch, repo,
feedback and callbackThreadId) and release the lock if used.
| * @param event - The x-github-event header value | ||
| * @param payload - The parsed webhook payload | ||
| */ | ||
| function extractPRComment( |
There was a problem hiding this comment.
@recoup-coding-agent srp - new lib for this function
Add postGitHubComment helper and parseGitHubThreadId to enable GitHub PR comment replies. The callback handler now posts to GitHub when the threadId is a github: prefixed ID, covering pr_created, updated, no_changes, and failed statuses. Co-Authored-By: Claude Opus 4.6 <[email protected]>
When the callback threadId starts with "github:", the Chat SDK has no GitHub adapter and crashes. Now GitHub threads skip thread.post() and thread.setState(), using only postGitHubComment and Redis state instead. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Register @chat-adapter/github alongside the Slack adapter so the
Chat SDK natively handles github:{repo}:{prNumber} thread IDs.
This replaces the manual postGitHubComment workarounds in the
callback handler — thread.post() and thread.setState() now work
for both Slack and GitHub threads.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Move extractPRComment function and constants (BOT_MENTION, SUPPORTED_EVENTS) from handleGitHubWebhook.ts into its own file with dedicated tests. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Replace custom PRComment type with GitHubThreadId from @chat-adapter/github. Add encodeGitHubThreadId to build thread ID strings matching the SDK format (including review comment IDs). Remove parseGitHubThreadId — the adapter's decodeThreadId handles it. Co-Authored-By: Claude Opus 4.6 <[email protected]>
…er failure - Use crypto.timingSafeEqual for webhook signature verification to prevent timing attacks - Wrap triggerUpdatePR in try/catch to restore Redis PR state if trigger fails Co-Authored-By: Claude Opus 4.6 <[email protected]>
Summary
/api/coding-agent/githubwebhook route for GitHub PR comment feedbackGITHUB_WEBHOOK_SECRETissue_commentevents: extracts repo/branch, looks up shared Redis PR state, triggersupdate-prGITHUB_WEBHOOK_SECRETto required env varsNew files
app/api/coding-agent/github/route.ts— Route handlerlib/coding-agent/handleGitHubWebhook.ts— Webhook logiclib/coding-agent/verifyGitHubWebhook.ts— HMAC signature verificationTest plan
/api/coding-agent/github@recoup-coding-agent <feedback>on a PR that was created by the botCloses MYC-4438
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes