Spaces:
Sleeping
Sleeping
| # This file handles all incoming GitHub webhooks | |
| # ContriBot registers webhooks when a repo is activated | |
| # GitHub sends events here: push, issues, pull_request, issue_comment | |
| import hmac, hashlib, logging | |
| from fastapi import APIRouter, Request, Header, HTTPException, BackgroundTasks | |
| from services.supabase_service import db | |
| from services.agent_orchestrator import orchestrator | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter() | |
| async def verify_signature(payload: bytes, sig_header: str, secret: str) -> bool: | |
| if not sig_header or not sig_header.startswith("sha256="): | |
| return False | |
| expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() | |
| return hmac.compare_digest(f"sha256={expected}", sig_header) | |
| async def github_webhook( | |
| repo_id: str, | |
| request: Request, | |
| background_tasks: BackgroundTasks, | |
| x_github_event: str = Header(None), | |
| x_hub_signature_256: str = Header(None), | |
| ): | |
| try: | |
| logger.info("=" * 60) | |
| logger.info(f"[WEBHOOK] Received webhook for repo {repo_id}. Event: {x_github_event}") | |
| payload_bytes = await request.body() | |
| # Get repo and verify webhook secret | |
| repo = await db.get_repo_by_id(repo_id) | |
| if not repo: | |
| logger.error(f"[WEBHOOK] Webhook received for unknown repo: {repo_id}") | |
| raise HTTPException(404, "Repo not found") | |
| repo_name = repo.get("github_full_name", "unknown/repo") | |
| repo_prefix = f" [REPO: {repo_name}]" | |
| webhook_secret = repo.get("webhook_secret", "") | |
| if webhook_secret: | |
| if not await verify_signature(payload_bytes, x_hub_signature_256, webhook_secret): | |
| logger.error(f"[WEBHOOK]{repo_prefix} Invalid webhook signature for repo {repo_id}") | |
| raise HTTPException(403, "Invalid webhook signature") | |
| logger.info(f"[WEBHOOK]{repo_prefix} Signature verification successful.") | |
| else: | |
| logger.warning(f"[WEBHOOK]{repo_prefix} No webhook secret configured for repo, skipping verification.") | |
| try: | |
| payload = await request.json() | |
| except Exception as e: | |
| logger.error(f"[WEBHOOK]{repo_prefix} Failed to parse webhook JSON: {e}") | |
| raise HTTPException(400, "Invalid JSON") | |
| action = payload.get("action", "") | |
| logger.info(f"[WEBHOOK]{repo_prefix} Event: {x_github_event}, Action: {action}") | |
| logger.debug(f"[WEBHOOK]{repo_prefix} Payload summary: {list(payload.keys())}") | |
| # Log webhook arrival | |
| await db.log_activity( | |
| repo_id, | |
| "webhook_received", | |
| f"Received {x_github_event} event from GitHub", | |
| metadata={"event": x_github_event, "action": action} | |
| ) | |
| task_triggered = False | |
| if x_github_event == "issues" and action == "opened": | |
| issue_number = payload["issue"]["number"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Triggering process_new_issue for issue #{issue_number}") | |
| background_tasks.add_task(orchestrator.process_new_issue, repo_id, issue_number) | |
| task_triggered = True | |
| elif x_github_event == "issue_comment" and action == "created": | |
| comment_body = payload["comment"]["body"].strip().lower() | |
| issue_number = payload["issue"]["number"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Received comment on issue #{issue_number}: {comment_body[:50]}...") | |
| if comment_body in ["yes", "no"]: | |
| # Find issue in DB by github_issue_number | |
| issues = await db.get_issues_by_repo(repo_id, status="pending_approval") | |
| matching = [i for i in issues if i["github_issue_number"] == issue_number] | |
| if matching: | |
| issue_id = matching[0]["id"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Triggering handle_user_response for issue_id {issue_id}") | |
| background_tasks.add_task( | |
| orchestrator.handle_user_response, repo_id, issue_id, comment_body | |
| ) | |
| task_triggered = True | |
| else: | |
| logger.info(f"[WEBHOOK]{repo_prefix} No matching pending_approval issue found for issue #{issue_number}") | |
| elif x_github_event == "pull_request" and action == "opened": | |
| pr_number = payload["pull_request"]["number"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Triggering handle_new_pr for PR #{pr_number}") | |
| background_tasks.add_task(orchestrator.handle_new_pr, repo_id, pr_number) | |
| task_triggered = True | |
| elif x_github_event == "pull_request" and action == "closed": | |
| if payload["pull_request"].get("merged"): | |
| pr_number = payload["pull_request"]["number"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Triggering handle_pr_merged for PR #{pr_number}") | |
| background_tasks.add_task(orchestrator.handle_pr_merged, repo_id, pr_number) | |
| task_triggered = True | |
| else: | |
| logger.info(f"[WEBHOOK]{repo_prefix} PR #{payload['pull_request']['number']} closed without merging, no task triggered.") | |
| elif x_github_event == "check_run" and action == "completed": | |
| if payload["check_run"]["conclusion"] == "failure": | |
| # Find the PR associated with this check run | |
| pull_requests = payload["check_run"].get("pull_requests", []) | |
| logger.info(f"[WEBHOOK]{repo_prefix} Check run failed. Associated PRs: {[pr['number'] for pr in pull_requests]}") | |
| for pr in pull_requests: | |
| pr_number = pr["number"] | |
| logger.info(f"[WEBHOOK]{repo_prefix} Triggering handle_ci_failure for PR #{pr_number}") | |
| background_tasks.add_task(orchestrator.handle_ci_failure, repo_id, pr_number, payload["check_run"]["id"]) | |
| task_triggered = True | |
| else: | |
| logger.info(f"[WEBHOOK]{repo_prefix} Check run completed with conclusion: {payload['check_run']['conclusion']}") | |
| if not task_triggered: | |
| logger.info(f"[WEBHOOK]{repo_prefix} No background task triggered for this event/action.") | |
| return {"status": "received", "event": x_github_event, "action": action} | |
| except Exception as e: | |
| logger.exception(f"[WEBHOOK] Unhandled exception in webhook handler for repo {repo_id}: {e}") | |
| raise HTTPException(500, f"Internal server error: {str(e)}") | |