Skip to content

feat: GitHub webhook route for PR comment feedback#266

Merged
sweetmantech merged 8 commits intotestfrom
sweetmantech/myc-4438-github-webhook-route-for-coding-agent-pr-feedback
Mar 7, 2026
Merged

feat: GitHub webhook route for PR comment feedback#266
sweetmantech merged 8 commits intotestfrom
sweetmantech/myc-4438-github-webhook-route-for-coding-agent-pr-feedback

Conversation

@sweetmantech
Copy link
Contributor

@sweetmantech sweetmantech commented Mar 7, 2026

Summary

  • Adds /api/coding-agent/github webhook route for GitHub PR comment feedback
  • Verifies webhook signature using GITHUB_WEBHOOK_SECRET
  • Handles issue_comment events: extracts repo/branch, looks up shared Redis PR state, triggers update-pr
  • Posts acknowledgment comment on the PR with View Task link
  • Adds GITHUB_WEBHOOK_SECRET to required env vars

New files

  • app/api/coding-agent/github/route.ts — Route handler
  • lib/coding-agent/handleGitHubWebhook.ts — Webhook logic
  • lib/coding-agent/verifyGitHubWebhook.ts — HMAC signature verification

Test plan

  • 58 tests pass across 17 files
  • Set up GitHub webhook on a repo pointing to preview URL /api/coding-agent/github
  • Comment @recoup-coding-agent <feedback> on a PR that was created by the bot
  • Verify bot posts acknowledgment comment and triggers update-pr task

Closes MYC-4438

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added GitHub webhook integration to receive and process pull request comment events
    • PR comments mentioning the bot now trigger automated PR updates
    • Implemented secure webhook signature verification using HMAC-SHA-256

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]>
@vercel
Copy link
Contributor

vercel bot commented Mar 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Mar 7, 2026 2:52am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Warning

Rate limit exceeded

@sweetmantech has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 11 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a32c9e11-4907-43b5-84ec-8779edeb6d84

📥 Commits

Reviewing files that changed from the base of the PR and between 8931abc and 5ae22c5.

⛔ Files ignored due to path filters (5)
  • lib/coding-agent/__tests__/bot.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/encodeGitHubThreadId.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/extractPRComment.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/handleGitHubWebhook.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/postGitHubComment.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (6)
  • lib/coding-agent/bot.ts
  • lib/coding-agent/encodeGitHubThreadId.ts
  • lib/coding-agent/extractPRComment.ts
  • lib/coding-agent/handleGitHubWebhook.ts
  • lib/coding-agent/postGitHubComment.ts
  • lib/coding-agent/verifyGitHubWebhook.ts
📝 Walkthrough

Walkthrough

This 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

Cohort / File(s) Summary
API Route Handler
app/api/coding-agent/github/route.ts
New Next.js API route that accepts POST requests and delegates webhook processing to the handleGitHubWebhook handler function.
Webhook Processing
lib/coding-agent/handleGitHubWebhook.ts
Main webhook handler that authenticates requests, filters for PR comment events with bot mentions, fetches PR details, manages PR state transitions, triggers PR updates, and posts confirmation comments to GitHub.
Signature Verification & Validation
lib/coding-agent/verifyGitHubWebhook.ts, lib/coding-agent/validateEnv.ts
HMAC-SHA-256 signature verification utility for webhook authentication, and environment variable validation requiring the GITHUB_WEBHOOK_SECRET.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🪝 A webhook arrives, carrying news,
GitHub whispers, we cannot refuse,
Signatures checked with cryptographic care,
PR states dance through the update affair,
Comments posted, the feedback takes flight! 🚀

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning handleGitHubWebhook violates SRP with 6+ responsibilities: signature verification, payload parsing, state management, GitHub API calls, task enqueueing, and response handling. Extract responsibilities into focused functions: validateGitHubWebhookBody.ts with Zod schema, separate state management layer, isolated task enqueueing with error handling and atomic transitions using Redis lock/CAS.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4438-github-webhook-route-for-coding-agent-pr-feedback

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

});

const [owner, repoName] = repo.split("/");
await fetch(`https://api.github.com/repos/${owner}/${repoName}/issues/${prNumber}/comments`, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Split webhook-only env checks out of the shared bot validator.

lib/coding-agent/bot.ts calls validateCodingAgentEnv() during singleton initialization, so adding GITHUB_WEBHOOK_SECRET here makes any bot import fail in environments that never expose /api/coding-agent/github. A dedicated webhook validator called from handleGitHubWebhook() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6120d56 and 8931abc.

⛔ Files ignored due to path filters (4)
  • lib/coding-agent/__tests__/bot.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/handleGitHubWebhook.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/validateEnv.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/verifyGitHubWebhook.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (4)
  • app/api/coding-agent/github/route.ts
  • lib/coding-agent/handleGitHubWebhook.ts
  • lib/coding-agent/validateEnv.ts
  • lib/coding-agent/verifyGitHubWebhook.ts

Comment on lines +29 to +41
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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +35 to +38
const commentBody: string = payload.comment?.body ?? "";
if (!commentBody.includes(BOT_MENTION)) {
return NextResponse.json({ status: "ignored" }, { headers: getCorsHeaders() });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and read the handleGitHubWebhook.ts file
find . -name "handleGitHubWebhook.ts" -type f

Repository: 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 -20

Repository: 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 tsx

Repository: 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 3

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Read the handleGitHubWebhook.ts file
cat -n ./lib/coding-agent/handleGitHubWebhook.ts

Repository: 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 2

Repository: recoupable/api

Length of output: 40


🏁 Script executed:

# Search for permission/authorization logic more broadly
rg "permission|authorization|access" lib/coding-agent/ -A 2

Repository: 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.ts

Repository: 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.

Comment on lines +61 to +88
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}`,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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]>
@sweetmantech sweetmantech merged commit a0a7ef8 into test Mar 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant