File size: 6,715 Bytes
3a507e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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)

@router.post("/github/{repo_id}")
@router.post("/github/{repo_id}/")
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)}")