@@ -20,6 +20,7 @@ import {
2020} from "../utils/validation.js" ;
2121import { ALLOWED_REPO } from "../utils/constants.js" ;
2222import { 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 */
258259function extractMentionAndRequest ( commentBody ) {
259260 const lowerBody = commentBody . toLowerCase ( ) ;
@@ -266,9 +267,15 @@ function extractMentionAndRequest(commentBody) {
266267 const mentionMatch = commentBody . match ( / @ d a l e s t u d y \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 */
298305async 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