1+ import asyncio
2+ import json
3+ import tempfile
4+ import uuid
5+ from datetime import datetime
6+ from typing import Dict
7+
8+ from fastapi import APIRouter , BackgroundTasks , HTTPException , Request
9+
10+ from prometheus .app .services .euni_fix import (
11+ EuniFixResult ,
12+ clone_repository ,
13+ commit_changes ,
14+ push_to_branch ,
15+ run_euni_fix ,
16+ )
17+ from prometheus .configuration .github import github_settings
18+ from prometheus .git .github_service import GitHubService
19+ from prometheus .utils .github_sec import parse_fix_command , verify_webhook_signature
20+ from prometheus .utils .logger_manager import get_logger
21+
22+ router = APIRouter ()
23+ logger = get_logger (__name__ )
24+
25+
26+ @router .post ("/webhook" )
27+ async def github_webhook (request : Request , background_tasks : BackgroundTasks ):
28+ """
29+ Handle GitHub webhook events.
30+
31+ Listens for issue_comment events and processes /fix commands.
32+ """
33+ # Get raw request body for signature verification
34+ body = await request .body ()
35+
36+ # Verify webhook signature
37+ signature = request .headers .get ("X-Hub-Signature-256" )
38+ if not verify_webhook_signature (body , signature ):
39+ logger .warning ("Invalid webhook signature" )
40+ raise HTTPException (status_code = 401 , detail = "Invalid signature" )
41+
42+ # Parse JSON payload
43+ try :
44+ payload = json .loads (body .decode ('utf-8' ))
45+ except json .JSONDecodeError :
46+ logger .error ("Invalid JSON payload" )
47+ raise HTTPException (status_code = 400 , detail = "Invalid JSON" )
48+
49+ # Check event type
50+ event_type = request .headers .get ("X-GitHub-Event" )
51+ if event_type != "issue_comment" :
52+ logger .info (f"Ignoring event type: { event_type } " )
53+ return {"message" : "Event type not supported" }
54+
55+ # Check if it's a comment creation event
56+ action = payload .get ("action" )
57+ if action != "created" :
58+ logger .info (f"Ignoring comment action: { action } " )
59+ return {"message" : "Action not supported" }
60+
61+ # Extract comment data
62+ comment = payload .get ("comment" , {})
63+ comment_body = comment .get ("body" , "" )
64+ comment_author = comment .get ("user" , {}).get ("login" , "" )
65+
66+ # Check for /fix command
67+ fix_command = parse_fix_command (comment_body , github_settings .BOT_HANDLE )
68+ if not fix_command :
69+ logger .info ("No /fix command found in comment" )
70+ return {"message" : "No fix command found" }
71+
72+ command , args = fix_command
73+ logger .info (f"Fix command detected: { command } with args: { args } " )
74+
75+ # Extract repository and issue information
76+ repository = payload .get ("repository" , {})
77+ issue = payload .get ("issue" , {})
78+
79+ repo_owner = repository .get ("owner" , {}).get ("login" , "" )
80+ repo_name = repository .get ("name" , "" )
81+ repo_full_name = repository .get ("full_name" , "" )
82+ repo_clone_url = repository .get ("clone_url" , "" )
83+ installation_id = payload .get ("installation" , {}).get ("id" )
84+
85+ issue_number = issue .get ("number" )
86+ issue_title = issue .get ("title" , "" )
87+ is_pull_request = "pull_request" in issue
88+
89+ if not all ([repo_owner , repo_name , installation_id , issue_number ]):
90+ logger .error ("Missing required webhook data" )
91+ raise HTTPException (status_code = 400 , detail = "Missing required data" )
92+
93+ # Start background task to process the fix
94+ background_tasks .add_task (
95+ process_fix_request ,
96+ installation_id = installation_id ,
97+ repo_owner = repo_owner ,
98+ repo_name = repo_name ,
99+ repo_clone_url = repo_clone_url ,
100+ issue_number = issue_number ,
101+ issue_title = issue_title ,
102+ is_pull_request = is_pull_request ,
103+ fix_args = args ,
104+ comment_author = comment_author ,
105+ issue_context = issue
106+ )
107+
108+ return {"message" : "Fix request received and processing" }
109+
110+
111+ async def process_fix_request (
112+ installation_id : int ,
113+ repo_owner : str ,
114+ repo_name : str ,
115+ repo_clone_url : str ,
116+ issue_number : int ,
117+ issue_title : str ,
118+ is_pull_request : bool ,
119+ fix_args : str ,
120+ comment_author : str ,
121+ issue_context : Dict
122+ ):
123+ """
124+ Background task to process the fix request.
125+ """
126+ github_service = GitHubService ()
127+ temp_repo_dir = None
128+
129+ try :
130+ logger .info (f"Processing fix request for { repo_owner } /{ repo_name } #{ issue_number } " )
131+
132+ # Get installation token
133+ token = await github_service .get_installation_token (installation_id )
134+
135+ # Check organization membership if ORG_NAME is set
136+ if github_settings .ORG_NAME :
137+ is_member = await github_service .check_org_membership (
138+ comment_author , github_settings .ORG_NAME , token
139+ )
140+ if not is_member :
141+ await github_service .post_comment (
142+ repo_owner , repo_name , issue_number ,
143+ f"❌ @{ comment_author } is not a member of the { github_settings .ORG_NAME } organization." ,
144+ token
145+ )
146+ return
147+
148+ # Post placeholder comment
149+ placeholder_comment = await github_service .post_comment (
150+ repo_owner , repo_name , issue_number ,
151+ f"🤖 EuniBot is analyzing the issue and preparing fixes...\n \n "
152+ f"Requested by: @{ comment_author } \n "
153+ f"Arguments: `{ fix_args if fix_args else 'None' } `\n \n "
154+ f"⏳ This may take a few minutes." ,
155+ token
156+ )
157+
158+ comment_id = placeholder_comment ["id" ]
159+
160+ # Get default branch
161+ default_branch = await github_service .get_repository_default_branch (
162+ repo_owner , repo_name , token
163+ )
164+
165+ # Clone repository
166+ temp_repo_dir = await clone_repository (repo_clone_url , default_branch )
167+
168+ # Run EuniFix
169+ fix_result = await run_euni_fix (temp_repo_dir , fix_args , issue_context )
170+
171+ if fix_result .success and fix_result .files_changed :
172+ # Generate unique branch name
173+ branch_name = f"euni-fix-{ issue_number } -{ uuid .uuid4 ().hex [:8 ]} "
174+
175+ # Commit changes
176+ commit_message = f"🤖 EuniFix: { issue_title } \n \n Fixes #{ issue_number } \n Requested by: @{ comment_author } "
177+ if fix_args :
178+ commit_message += f"\n Arguments: { fix_args } "
179+
180+ commit_sha = await commit_changes (temp_repo_dir , fix_result .files_changed , commit_message )
181+
182+ if commit_sha :
183+ # Get latest commit SHA from default branch
184+ base_sha = await github_service .get_latest_commit_sha (
185+ repo_owner , repo_name , default_branch , token
186+ )
187+
188+ # Create new branch
189+ await github_service .create_branch (
190+ repo_owner , repo_name , branch_name , base_sha , token
191+ )
192+
193+ # Push changes to the new branch
194+ authenticated_clone_url = repo_clone_url .replace (
195+ "https://" , f"https://x-access-token:{ token } @"
196+ )
197+ push_success = await push_to_branch (
198+ temp_repo_dir , branch_name , authenticated_clone_url
199+ )
200+
201+ if push_success :
202+ # Create pull request
203+ pr_title = f"🤖 EuniFix: { issue_title } "
204+ pr_body = (
205+ f"This PR was automatically generated by EuniBot to fix issue #{ issue_number } .\n \n "
206+ f"## Changes Made\n "
207+ f"- { fix_result .message } \n \n "
208+ f"## Files Modified\n "
209+ )
210+ for file_path in fix_result .files_changed :
211+ pr_body += f"- `{ file_path } `\n "
212+
213+ pr_body += f"\n ## Requested by\n @{ comment_author } "
214+ if fix_args :
215+ pr_body += f"\n \n ## Arguments\n `{ fix_args } `"
216+
217+ pr_body += f"\n \n Closes #{ issue_number } "
218+
219+ pr = await github_service .create_pull_request (
220+ repo_owner , repo_name , pr_title , pr_body ,
221+ branch_name , default_branch , token
222+ )
223+
224+ # Update placeholder comment with success
225+ success_message = (
226+ f"✅ **EuniFix completed successfully!**\n \n "
227+ f"📋 **Summary**: { fix_result .message } \n "
228+ f"🔧 **Files modified**: { len (fix_result .files_changed )} \n "
229+ f"🌿 **Branch**: `{ branch_name } `\n "
230+ f"🔗 **Pull Request**: #{ pr ['number' ]} - { pr ['html_url' ]} \n \n "
231+ f"**Modified files:**\n "
232+ )
233+ for file_path in fix_result .files_changed :
234+ success_message += f"- `{ file_path } `\n "
235+
236+ await github_service .update_comment (
237+ repo_owner , repo_name , comment_id , success_message , token
238+ )
239+
240+ logger .info (f"Successfully created PR #{ pr ['number' ]} for fix request" )
241+ else :
242+ raise Exception ("Failed to push changes to branch" )
243+ else :
244+ raise Exception ("Failed to commit changes" )
245+ else :
246+ # Update placeholder comment with failure
247+ error_message = (
248+ f"❌ **EuniFix failed**\n \n "
249+ f"📋 **Message**: { fix_result .message } \n "
250+ )
251+ if fix_result .error :
252+ error_message += f"🚨 **Error**: { fix_result .error } \n "
253+
254+ error_message += f"\n Requested by: @{ comment_author } "
255+
256+ await github_service .update_comment (
257+ repo_owner , repo_name , comment_id , error_message , token
258+ )
259+
260+ logger .error (f"EuniFix failed: { fix_result .message } " )
261+
262+ except Exception as e :
263+ logger .error (f"Error processing fix request: { e } " )
264+
265+ try :
266+ # Try to update the placeholder comment with error
267+ error_message = (
268+ f"❌ **EuniFix encountered an error**\n \n "
269+ f"🚨 **Error**: { str (e )} \n "
270+ f"Requested by: @{ comment_author } \n \n "
271+ f"Please try again or contact support if the issue persists."
272+ )
273+
274+ # Get token again if needed
275+ if 'token' not in locals ():
276+ token = await github_service .get_installation_token (installation_id )
277+
278+ if 'comment_id' in locals ():
279+ await github_service .update_comment (
280+ repo_owner , repo_name , comment_id , error_message , token
281+ )
282+ except Exception as update_error :
283+ logger .error (f"Failed to update error comment: { update_error } " )
284+
285+ finally :
286+ # Clean up temporary directory
287+ if temp_repo_dir :
288+ try :
289+ import shutil
290+ shutil .rmtree (temp_repo_dir )
291+ logger .info (f"Cleaned up temporary directory: { temp_repo_dir } " )
292+ except Exception as cleanup_error :
293+ logger .error (f"Failed to cleanup temp directory: { cleanup_error } " )
0 commit comments