From f5eae4d77bc4c4c2f4b180fa54429de5bd08529d Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Sat, 5 Jul 2025 09:34:28 -0800 Subject: [PATCH] Dry up schemas and ignore vercel PR comments (#206) --- .../github/handlers/issueCommentHandler.ts | 46 ++----- .../webhooks/github/handlers/issueHandler.ts | 17 +-- .../github/handlers/pullRequestHandler.ts | 16 +-- .../pullRequestReviewCommentHandler.ts | 36 +----- .../app/api/webhooks/github/handlers/types.ts | 119 ++++++++++++++++++ .../app/api/webhooks/github/handlers/utils.ts | 5 + 6 files changed, 142 insertions(+), 97 deletions(-) create mode 100644 apps/roomote/src/app/api/webhooks/github/handlers/types.ts diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/issueCommentHandler.ts b/apps/roomote/src/app/api/webhooks/github/handlers/issueCommentHandler.ts index c65a0b943d..443d147c55 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/issueCommentHandler.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/issueCommentHandler.ts @@ -1,36 +1,12 @@ import { NextResponse } from 'next/server'; -import { z } from 'zod'; import type { JobPayload } from '@roo-code-cloud/db'; -import { createAndEnqueueJob, fetchGitHubAPI } from './utils'; - -const githubIssueCommentWebhookSchema = z.object({ - action: z.string(), - issue: z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - pull_request: z.object({ url: z.string() }).optional(), - }), - comment: z.object({ - id: z.number(), - body: z.string(), - html_url: z.string(), - user: z.object({ login: z.string() }), - }), - repository: z.object({ - full_name: z.string(), - }), -}); - -const githubPullRequestWebhookSchema = z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - head: z.object({ ref: z.string() }), - base: z.object({ ref: z.string() }), -}); +import { + githubIssueCommentWebhookSchema, + githubPullRequestSchema, +} from './types'; +import { createAndEnqueueJob, fetchGitHubAPI, isRoomoteMention } from './utils'; export async function handleIssueCommentEvent(body: string) { const data = githubIssueCommentWebhookSchema.parse(JSON.parse(body)); @@ -40,13 +16,12 @@ export async function handleIssueCommentEvent(body: string) { return NextResponse.json({ message: 'action_ignored' }); } - if (!comment.body.includes('@roomote') || comment.user.login === 'roomote') { + if (!isRoomoteMention(comment)) { return NextResponse.json({ message: 'no_roomote_mention' }); } console.log('🗄️ Issue Comment Webhook ->', data); - // Handle PR comments (when comment is on a pull request) if (issue.pull_request) { const response = await fetchGitHubAPI(issue.pull_request.url); @@ -61,9 +36,7 @@ export async function handleIssueCommentEvent(body: string) { // Example: // https://api.github.com/repos/RooCodeInc/Roo-Code/pulls/4796 - const pull_request = githubPullRequestWebhookSchema.parse( - await response.json(), - ); + const pull_request = githubPullRequestSchema.parse(await response.json()); console.log(`🗄️ Pull Request -> ${issue.pull_request.url}`, pull_request); @@ -72,8 +45,8 @@ export async function handleIssueCommentEvent(body: string) { prNumber: pull_request.number, prTitle: pull_request.title, prBody: pull_request.body || '', - prBranch: pull_request.head.ref, - baseRef: pull_request.base.ref, + prBranch: pull_request.head?.ref || '', + baseRef: pull_request.base?.ref || '', commentId: comment.id, commentBody: comment.body, commentAuthor: comment.user.login, @@ -93,7 +66,6 @@ export async function handleIssueCommentEvent(body: string) { }); } - // Handle issue comments (when comment is on a regular issue) const type = 'github.issue.comment.respond' as const; const payload: JobPayload = { diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/issueHandler.ts b/apps/roomote/src/app/api/webhooks/github/handlers/issueHandler.ts index e322e85683..0c5022e802 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/issueHandler.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/issueHandler.ts @@ -1,23 +1,10 @@ import { NextResponse } from 'next/server'; -import { z } from 'zod'; import type { JobPayload } from '@roo-code-cloud/db'; +import { githubIssueWebhookSchema } from './types'; import { createAndEnqueueJob } from './utils'; -const githubIssueWebhookSchema = z.object({ - action: z.string(), - issue: z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - labels: z.array(z.object({ name: z.string() })), - }), - repository: z.object({ - full_name: z.string(), - }), -}); - export async function handleIssueEvent(body: string) { const data = githubIssueWebhookSchema.parse(JSON.parse(body)); const { action, repository, issue } = data; @@ -33,7 +20,7 @@ export async function handleIssueEvent(body: string) { issue: issue.number, title: issue.title, body: issue.body || '', - labels: issue.labels.map(({ name }) => name), + labels: issue.labels?.map(({ name }) => name) || [], }; const { jobId, enqueuedJobId } = await createAndEnqueueJob( diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestHandler.ts b/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestHandler.ts index e9b9d74757..11c20fc4fa 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestHandler.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestHandler.ts @@ -1,23 +1,10 @@ import { NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; -import { z } from 'zod'; import { db, cloudJobs } from '@roo-code-cloud/db/server'; import { SlackNotifier } from '@/lib/slack'; - -const githubPullRequestWebhookSchema = z.object({ - action: z.string(), - pull_request: z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - html_url: z.string(), - }), - repository: z.object({ - full_name: z.string(), - }), -}); +import { githubPullRequestWebhookSchema } from './types'; export async function handlePullRequestEvent(body: string) { const data = githubPullRequestWebhookSchema.parse(JSON.parse(body)); @@ -50,6 +37,7 @@ export async function handlePullRequestEvent(body: string) { // Filter jobs to find the one matching this repo and issue. const job = jobs.find((j) => { const payload = j.payload as { repo: string; issue: number }; + return ( payload.repo === repository.full_name && payload.issue === issueNumber ); diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestReviewCommentHandler.ts b/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestReviewCommentHandler.ts index 65517e7668..9c46935d34 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestReviewCommentHandler.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/pullRequestReviewCommentHandler.ts @@ -1,35 +1,9 @@ import { NextResponse } from 'next/server'; -import { z } from 'zod'; import type { JobPayload } from '@roo-code-cloud/db'; -import { createAndEnqueueJob } from './utils'; - -export const githubPullRequestReviewCommentWebhookSchema = z.object({ - action: z.string(), - comment: z.object({ - id: z.number(), - body: z.string(), - html_url: z.string(), - user: z.object({ - login: z.string(), - }), - }), - pull_request: z.object({ - number: z.number(), - title: z.string(), - body: z.string().nullable(), - head: z.object({ - ref: z.string(), - }), - base: z.object({ - ref: z.string(), - }), - }), - repository: z.object({ - full_name: z.string(), - }), -}); +import { githubPullRequestReviewCommentWebhookSchema } from './types'; +import { createAndEnqueueJob, isRoomoteMention } from './utils'; export async function handlePullRequestReviewCommentEvent(body: string) { const data = githubPullRequestReviewCommentWebhookSchema.parse( @@ -41,7 +15,7 @@ export async function handlePullRequestReviewCommentEvent(body: string) { return NextResponse.json({ message: 'action_ignored' }); } - if (!comment.body.includes('@roomote') || comment.user.login === 'roomote') { + if (!isRoomoteMention(comment)) { return NextResponse.json({ message: 'no_roomote_mention' }); } @@ -52,8 +26,8 @@ export async function handlePullRequestReviewCommentEvent(body: string) { prNumber: pull_request.number, prTitle: pull_request.title, prBody: pull_request.body || '', - prBranch: pull_request.head.ref, - baseRef: pull_request.base.ref, + prBranch: pull_request.head?.ref || '', + baseRef: pull_request.base?.ref || '', commentId: comment.id, commentBody: comment.body, commentAuthor: comment.user.login, diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/types.ts b/apps/roomote/src/app/api/webhooks/github/handlers/types.ts new file mode 100644 index 0000000000..81ec4e19eb --- /dev/null +++ b/apps/roomote/src/app/api/webhooks/github/handlers/types.ts @@ -0,0 +1,119 @@ +import { z } from 'zod'; + +/** + * GithubComment + */ + +export const githubCommentSchema = z.object({ + id: z.number(), + body: z.string(), + html_url: z.string(), + user: z.object({ + login: z.string(), + }), +}); + +export type GithubComment = z.infer; + +/** + * GithubPullRequest + */ + +export const githubPullRequestSchema = z.object({ + number: z.number(), + title: z.string(), + body: z.string().nullable(), + head: z + .object({ + ref: z.string(), + }) + .optional(), + base: z + .object({ + ref: z.string(), + }) + .optional(), + html_url: z.string(), +}); + +export type GithubPullRequest = z.infer; + +/** + * GithubIssue + */ + +export const githubIssueSchema = z.object({ + number: z.number(), + title: z.string(), + body: z.string().nullable(), + pull_request: z.object({ url: z.string() }).optional(), + labels: z.array(z.object({ name: z.string() })).optional(), +}); + +export type GithubIssue = z.infer; + +/** + * GithubRepository + */ + +export const githubRepositorySchema = z.object({ + full_name: z.string(), +}); + +export type GithubRepository = z.infer; + +/** + * GithubPullRequestReviewCommentWebhook + */ + +export const githubPullRequestReviewCommentWebhookSchema = z.object({ + action: z.string(), + comment: githubCommentSchema, + pull_request: githubPullRequestSchema, + repository: githubRepositorySchema, +}); + +export type GithubPullRequestReviewCommentWebhook = z.infer< + typeof githubPullRequestReviewCommentWebhookSchema +>; + +/** + * GithubIssueCommentWebhook + */ + +export const githubIssueCommentWebhookSchema = z.object({ + action: z.string(), + issue: githubIssueSchema, + comment: githubCommentSchema, + repository: githubRepositorySchema, +}); + +export type GithubIssueCommentWebhook = z.infer< + typeof githubIssueCommentWebhookSchema +>; + +/** + * GithubPullRequestWebhook + */ + +export const githubPullRequestWebhookSchema = z.object({ + action: z.string(), + pull_request: githubPullRequestSchema, + repository: githubRepositorySchema, +}); + +export type GithubPullRequestWebhook = z.infer< + typeof githubPullRequestWebhookSchema +>; + +/** + * GithubIssueWebhook + */ + +export const githubIssueWebhookSchema = z.object({ + action: z.string(), + issue: githubIssueSchema, + repository: githubRepositorySchema, +}); + +export type GithubIssueWebhook = z.infer; diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts b/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts index 12b47b9bcd..a192a2c36e 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts @@ -8,6 +8,7 @@ import { } from '@roo-code-cloud/db/server'; import { enqueue } from '@/lib'; +import type { GithubComment } from './types'; export function verifySignature( body: string, @@ -57,3 +58,7 @@ export async function fetchGitHubAPI(url: string, options: RequestInit = {}) { return fetch(url, { ...options, headers }); } + +export const isRoomoteMention = (comment: GithubComment) => + comment.body.includes('@roomote') && + !['roomote', 'vercel'].includes(comment.user.login);