|
3 | 3 | import { NextRequest } from "next/server"; |
4 | 4 | import { App, Octokit } from "octokit"; |
5 | 5 | import { WebhookEventDefinition} from "@octokit/webhooks/types"; |
| 6 | +import { Gitlab } from "@gitbeaker/rest"; |
6 | 7 | import { env } from "@sourcebot/shared"; |
7 | | -import { processGitHubPullRequest } from "@/features/agents/review-agent/app"; |
| 8 | +import { processGitHubPullRequest, processGitLabMergeRequest } from "@/features/agents/review-agent/app"; |
8 | 9 | import { throttling, type ThrottlingOptions } from "@octokit/plugin-throttling"; |
9 | 10 | import fs from "fs"; |
10 | | -import { GitHubPullRequest } from "@/features/agents/review-agent/types"; |
| 11 | +import { GitHubPullRequest, GitLabMergeRequestPayload, GitLabNotePayload } from "@/features/agents/review-agent/types"; |
11 | 12 | import { createLogger } from "@sourcebot/shared"; |
12 | 13 |
|
13 | | -const logger = createLogger('github-webhook'); |
| 14 | +const logger = createLogger('webhook'); |
14 | 15 |
|
15 | 16 | const DEFAULT_GITHUB_API_BASE_URL = "https://api.github.com"; |
16 | 17 | type GitHubAppBaseOptions = Omit<ConstructorParameters<typeof App>[0], "Octokit"> & { throttle: ThrottlingOptions }; |
@@ -95,6 +96,40 @@ function isIssueCommentEvent(eventHeader: string, payload: unknown): payload is |
95 | 96 | return eventHeader === "issue_comment" && typeof payload === "object" && payload !== null && "action" in payload && typeof payload.action === "string" && payload.action === "created"; |
96 | 97 | } |
97 | 98 |
|
| 99 | +function isGitLabMergeRequestEvent(eventHeader: string, payload: unknown): payload is GitLabMergeRequestPayload { |
| 100 | + return ( |
| 101 | + eventHeader === "Merge Request Hook" && |
| 102 | + typeof payload === "object" && |
| 103 | + payload !== null && |
| 104 | + "object_attributes" in payload && |
| 105 | + typeof (payload as GitLabMergeRequestPayload).object_attributes?.action === "string" && |
| 106 | + ["open", "update", "reopen"].includes((payload as GitLabMergeRequestPayload).object_attributes.action) |
| 107 | + ); |
| 108 | +} |
| 109 | + |
| 110 | +function isGitLabNoteEvent(eventHeader: string, payload: unknown): payload is GitLabNotePayload { |
| 111 | + return ( |
| 112 | + eventHeader === "Note Hook" && |
| 113 | + typeof payload === "object" && |
| 114 | + payload !== null && |
| 115 | + "object_attributes" in payload && |
| 116 | + (payload as GitLabNotePayload).object_attributes?.noteable_type === "MergeRequest" |
| 117 | + ); |
| 118 | +} |
| 119 | + |
| 120 | +let gitlabClient: InstanceType<typeof Gitlab> | undefined; |
| 121 | + |
| 122 | +if (env.GITLAB_REVIEW_AGENT_TOKEN) { |
| 123 | + try { |
| 124 | + gitlabClient = new Gitlab({ |
| 125 | + host: `https://${env.GITLAB_REVIEW_AGENT_HOST}`, |
| 126 | + token: env.GITLAB_REVIEW_AGENT_TOKEN, |
| 127 | + }); |
| 128 | + } catch (error) { |
| 129 | + logger.error(`Error initializing GitLab client: ${error}`); |
| 130 | + } |
| 131 | +} |
| 132 | + |
98 | 133 | export const POST = async (request: NextRequest) => { |
99 | 134 | const body = await request.json(); |
100 | 135 | const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value])); |
@@ -161,5 +196,62 @@ export const POST = async (request: NextRequest) => { |
161 | 196 | } |
162 | 197 | } |
163 | 198 |
|
| 199 | + const gitlabEvent = headers['x-gitlab-event']; |
| 200 | + if (gitlabEvent) { |
| 201 | + logger.info('GitLab event received:', gitlabEvent); |
| 202 | + |
| 203 | + const token = headers['x-gitlab-token']; |
| 204 | + if (!env.GITLAB_REVIEW_AGENT_WEBHOOK_SECRET || token !== env.GITLAB_REVIEW_AGENT_WEBHOOK_SECRET) { |
| 205 | + logger.warn('GitLab webhook token is invalid or GITLAB_REVIEW_AGENT_WEBHOOK_SECRET is not set'); |
| 206 | + return Response.json({ status: 'ok' }); |
| 207 | + } |
| 208 | + |
| 209 | + if (!gitlabClient) { |
| 210 | + logger.warn('Received GitLab webhook event but GITLAB_REVIEW_AGENT_TOKEN is not set'); |
| 211 | + return Response.json({ status: 'ok' }); |
| 212 | + } |
| 213 | + |
| 214 | + if (isGitLabMergeRequestEvent(gitlabEvent, body)) { |
| 215 | + if (env.REVIEW_AGENT_AUTO_REVIEW_ENABLED === "false") { |
| 216 | + logger.info('Review agent auto review (REVIEW_AGENT_AUTO_REVIEW_ENABLED) is disabled, skipping'); |
| 217 | + return Response.json({ status: 'ok' }); |
| 218 | + } |
| 219 | + |
| 220 | + await processGitLabMergeRequest( |
| 221 | + gitlabClient, |
| 222 | + body.project.id, |
| 223 | + body, |
| 224 | + env.GITLAB_REVIEW_AGENT_HOST, |
| 225 | + ); |
| 226 | + } |
| 227 | + |
| 228 | + if (isGitLabNoteEvent(gitlabEvent, body)) { |
| 229 | + const noteBody = body.object_attributes?.note; |
| 230 | + if (noteBody === `/${env.REVIEW_AGENT_REVIEW_COMMAND}`) { |
| 231 | + logger.info('Review agent review command received on GitLab MR, processing'); |
| 232 | + |
| 233 | + const mrPayload: GitLabMergeRequestPayload = { |
| 234 | + object_kind: "merge_request", |
| 235 | + object_attributes: { |
| 236 | + iid: body.merge_request.iid, |
| 237 | + title: body.merge_request.title, |
| 238 | + description: body.merge_request.description, |
| 239 | + action: "update", |
| 240 | + last_commit: body.merge_request.last_commit, |
| 241 | + diff_refs: body.merge_request.diff_refs, |
| 242 | + }, |
| 243 | + project: body.project, |
| 244 | + }; |
| 245 | + |
| 246 | + await processGitLabMergeRequest( |
| 247 | + gitlabClient, |
| 248 | + body.project.id, |
| 249 | + mrPayload, |
| 250 | + env.GITLAB_REVIEW_AGENT_HOST, |
| 251 | + ); |
| 252 | + } |
| 253 | + } |
| 254 | + } |
| 255 | + |
164 | 256 | return Response.json({ status: 'ok' }); |
165 | 257 | } |
0 commit comments