Skip to content

Commit 2bbc48e

Browse files
authored
Merge pull request #3 from DaleStudy/1-approve-pr
Add PR approval request handling and enhance mention extraction logic
2 parents 26bb550 + b88464a commit 2bbc48e

1 file changed

Lines changed: 177 additions & 6 deletions

File tree

โ€Žhandlers/webhooks.jsโ€Ž

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "../utils/validation.js";
2121
import { ALLOWED_REPO } from "../utils/constants.js";
2222
import { performAIReview, addReactionToComment } from "../utils/prReview.js";
23+
import { hasApprovedReview, safeJson } from "../utils/prActions.js";
2324

2425
/**
2526
* GitHub webhook ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
@@ -253,7 +254,7 @@ async function handlePullRequestEvent(payload, env) {
253254
* ๋Œ“๊ธ€์—์„œ @dalestudy ๋ฉ˜์…˜๊ณผ ์‚ฌ์šฉ์ž ์š”์ฒญ ์ถ”์ถœ
254255
*
255256
* @param {string} commentBody - ๋Œ“๊ธ€ ๋‚ด์šฉ
256-
* @returns {Object|null} { isMentioned, userRequest } ๋˜๋Š” null
257+
* @returns {Object|null} { isMentioned, userRequest, isApprovalRequest } ๋˜๋Š” null
257258
*/
258259
function extractMentionAndRequest(commentBody) {
259260
const lowerBody = commentBody.toLowerCase();
@@ -266,9 +267,15 @@ function extractMentionAndRequest(commentBody) {
266267
const mentionMatch = commentBody.match(/@dalestudy\s*(.*)/i);
267268
let userRequest = mentionMatch && mentionMatch[1].trim() ? mentionMatch[1].trim() : null;
268269

270+
// ์Šน์ธ ์š”์ฒญ ํ‚ค์›Œ๋“œ ํ™•์ธ
271+
const approvalKeywords = ['approve', '์Šน์ธ'];
272+
const isApprovalRequest = userRequest && approvalKeywords.some(keyword =>
273+
userRequest.toLowerCase().trim() === keyword
274+
);
275+
269276
// ์ผ๋ฐ˜์ ์ธ ๋ฆฌ๋ทฐ ์š”์ฒญ ํ‚ค์›Œ๋“œ๋งŒ ์žˆ๋Š” ๊ฒฝ์šฐ userRequest๋ฅผ null๋กœ ์ฒ˜๋ฆฌ
270277
// (์ „์ฒด ๋ฆฌ๋ทฐ ๋ชจ๋“œ๋กœ ๋™์ž‘ํ•˜๋„๋ก)
271-
if (userRequest) {
278+
if (userRequest && !isApprovalRequest) {
272279
const normalizedRequest = userRequest.toLowerCase().trim();
273280
const genericReviewKeywords = [
274281
'review',
@@ -289,11 +296,11 @@ function extractMentionAndRequest(commentBody) {
289296
}
290297
}
291298

292-
return { isMentioned: true, userRequest };
299+
return { isMentioned: true, userRequest, isApprovalRequest };
293300
}
294301

295302
/**
296-
* Issue Comment ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์š”์ฒญ)
303+
* Issue Comment ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ (AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์š”์ฒญ ๋˜๋Š” PR ์Šน์ธ ์š”์ฒญ)
297304
*/
298305
async function handleIssueCommentEvent(payload, env) {
299306
const action = payload.action;
@@ -326,6 +333,71 @@ async function handleIssueCommentEvent(payload, env) {
326333
return corsResponse({ message: "Ignored: not mentioned" });
327334
}
328335

336+
const appToken = await generateGitHubAppToken(env);
337+
338+
// ์Šน์ธ ์š”์ฒญ ์ฒ˜๋ฆฌ
339+
if (mention.isApprovalRequest) {
340+
console.log(`PR approval requested for #${prNumber}`);
341+
342+
try {
343+
// ๐Ÿ‘€ reaction ์ถ”๊ฐ€ (์ฒ˜๋ฆฌ ์‹œ์ž‘ ์•Œ๋ฆผ)
344+
await addReactionToComment(
345+
repoOwner,
346+
repoName,
347+
comment.id,
348+
"issue",
349+
"eyes",
350+
appToken
351+
);
352+
353+
const result = await handleApprovalRequest(
354+
repoOwner,
355+
repoName,
356+
prNumber,
357+
appToken
358+
);
359+
360+
if (result.success) {
361+
// โœ… reaction ์ถ”๊ฐ€ (์„ฑ๊ณต)
362+
await addReactionToComment(
363+
repoOwner,
364+
repoName,
365+
comment.id,
366+
"issue",
367+
"+1",
368+
appToken
369+
);
370+
371+
console.log(`PR #${prNumber} approved successfully`);
372+
return corsResponse({
373+
message: "PR approved",
374+
pr: prNumber,
375+
});
376+
} else {
377+
// โŒ reaction ์ถ”๊ฐ€ (์‹คํŒจ)
378+
await addReactionToComment(
379+
repoOwner,
380+
repoName,
381+
comment.id,
382+
"issue",
383+
"-1",
384+
appToken
385+
);
386+
387+
console.log(`PR #${prNumber} approval failed: ${result.error}`);
388+
return corsResponse({
389+
message: "PR approval failed",
390+
pr: prNumber,
391+
error: result.error,
392+
});
393+
}
394+
} catch (error) {
395+
console.error(`PR approval failed for #${prNumber}:`, error);
396+
return errorResponse(`PR approval failed: ${error.message}`, 500);
397+
}
398+
}
399+
400+
// AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์š”์ฒญ ์ฒ˜๋ฆฌ
329401
console.log(`AI review requested for PR #${prNumber}${mention.userRequest ? ` - Request: ${mention.userRequest}` : ""}`);
330402

331403
// OPENAI_API_KEY ํ™•์ธ
@@ -336,8 +408,6 @@ async function handleIssueCommentEvent(payload, env) {
336408

337409
// AI ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์‹คํ–‰
338410
try {
339-
const appToken = await generateGitHubAppToken(env);
340-
341411
// ๐Ÿ‘€ reaction ์ถ”๊ฐ€ (๋ฆฌ๋ทฐ ์‹œ์ž‘ ์•Œ๋ฆผ)
342412
await addReactionToComment(
343413
repoOwner,
@@ -444,3 +514,104 @@ async function handlePullRequestReviewCommentEvent(payload, env) {
444514
return errorResponse(`AI review failed: ${error.message}`, 500);
445515
}
446516
}
517+
518+
/**
519+
* PR ์Šน์ธ ์š”์ฒญ ์ฒ˜๋ฆฌ
520+
*
521+
* @param {string} repoOwner - ์ €์žฅ์†Œ ์†Œ์œ ์ž
522+
* @param {string} repoName - ์ €์žฅ์†Œ ์ด๋ฆ„
523+
* @param {number} prNumber - PR ๋ฒˆํ˜ธ
524+
* @param {string} githubToken - GitHub ํ† ํฐ
525+
* @returns {Promise<{success: boolean, error?: string}>}
526+
*/
527+
async function handleApprovalRequest(repoOwner, repoName, prNumber, githubToken) {
528+
try {
529+
// PR ์ •๋ณด ์กฐํšŒ
530+
const prResponse = await fetch(
531+
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}`,
532+
{ headers: getGitHubHeaders(githubToken) }
533+
);
534+
535+
if (!prResponse.ok) {
536+
const errorData = await safeJson(prResponse);
537+
return {
538+
success: false,
539+
error: errorData.message || `Failed to fetch PR: ${prResponse.statusText}`,
540+
};
541+
}
542+
543+
const prData = await prResponse.json();
544+
545+
// Closed PR ์ฒดํฌ
546+
if (isClosedPR(prData.state)) {
547+
return {
548+
success: false,
549+
error: "PR is closed",
550+
};
551+
}
552+
553+
// Draft PR ์ฒดํฌ
554+
if (prData.draft) {
555+
return {
556+
success: false,
557+
error: "PR is in draft state",
558+
};
559+
}
560+
561+
// maintenance ๋ผ๋ฒจ ์ฒดํฌ
562+
const labels = prData.labels.map((l) => l.name);
563+
if (hasMaintenanceLabel(labels)) {
564+
return {
565+
success: false,
566+
error: "PR has maintenance label",
567+
};
568+
}
569+
570+
// ์ด๋ฏธ ์Šน์ธ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
571+
const alreadyApproved = await hasApprovedReview(
572+
repoOwner,
573+
repoName,
574+
prNumber,
575+
githubToken
576+
);
577+
578+
if (alreadyApproved) {
579+
return {
580+
success: false,
581+
error: "PR is already approved",
582+
};
583+
}
584+
585+
// PR ์Šน์ธ ์‹คํ–‰
586+
const approvalResponse = await fetch(
587+
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/reviews`,
588+
{
589+
method: "POST",
590+
headers: {
591+
...getGitHubHeaders(githubToken),
592+
"Content-Type": "application/json",
593+
},
594+
body: JSON.stringify({
595+
body: "์Šน์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐Ÿ‘",
596+
event: "APPROVE",
597+
}),
598+
}
599+
);
600+
601+
if (!approvalResponse.ok) {
602+
const errorData = await safeJson(approvalResponse);
603+
return {
604+
success: false,
605+
error: errorData.message || `Approval failed: ${approvalResponse.statusText}`,
606+
};
607+
}
608+
609+
return { success: true };
610+
} catch (error) {
611+
console.error(`handleApprovalRequest error:`, error);
612+
return {
613+
success: false,
614+
error: error.message || "Unknown error",
615+
};
616+
}
617+
}

0 commit comments

Comments
ย (0)