File size: 5,399 Bytes
942a4f3
 
 
 
 
 
 
 
b93e458
938ad4a
942a4f3
 
 
 
 
 
 
 
 
d5e4283
 
942a4f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f8d6a4
942a4f3
 
 
 
 
 
 
 
 
 
13c4d84
b22918d
 
 
 
 
 
942a4f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938ad4a
 
 
 
 
 
81a14c5
938ad4a
13c4d84
942a4f3
3f8d6a4
942a4f3
 
 
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import os
import hmac
import hashlib
from typing import Optional
from dotenv import load_dotenv
from fastapi import FastAPI, Request, HTTPException, Header, status
from pydantic import BaseModel, Field

from langgraph_pr_review_bot import graph,PRReviewState

# Load environment variables from .env file
load_dotenv()

app = FastAPI(
    title="GitHub PR Reviewer Bot Webhook Listener",
    description="Listens for GitHub PR events and initiates Langgraph workflows."
)

# --- Configuration ---
GITHUB_WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET")
#GITHUB_WEBHOOK_SECRET ="d62UMC1iQ6n5PHq9w9bSvPHiWXBqhKX4"

if not GITHUB_WEBHOOK_SECRET:
    raise ValueError("GITHUB_WEBHOOK_SECRET environment variable not set. Please create a .env file.")

# --- Pydantic Models for Payload Parsing ---

class Repository(BaseModel):
    id: int
    full_name: str
    html_url: str
    clone_url: str

class PullRequestUser(BaseModel):
    login: str
    id: int
    type: str

class PullRequest(BaseModel):
    id: int
    url: str
    html_url: str
    diff_url: str # This is the changelist URL (diff URL)
    state: str
    title: str
    user: PullRequestUser
    base: dict # Contains info about the base branch/repo
    head: dict # Contains info about the head branch/repo

class GitHubWebhookPayload(BaseModel):
    action: str
    pull_request: PullRequest
    repository: Repository
    sender: PullRequestUser

# --- Helper Function for Signature Verification ---

def verify_signature(x_hub_signature_256: str, payload_body: bytes) -> bool:
    """
    Verifies the GitHub webhook signature.
    """
    if not GITHUB_WEBHOOK_SECRET:
        return False # Should be caught by the initial check, but for safety

    # GitHub sends x-hub-signature-256 in the format 'sha256=<signature>'
    # We need to extract just the signature part
    expected_signature = x_hub_signature_256.split('sha256=')[1]

    # Calculate the HMAC SHA256 signature
    mac = hmac.new(
        GITHUB_WEBHOOK_SECRET.encode('utf-8'),
        msg=payload_body,
        digestmod=hashlib.sha256
    )
    calculated_signature = mac.hexdigest()

    # Use hmac.compare_digest for constant-time comparison to prevent timing attacks
    return hmac.compare_digest(calculated_signature, expected_signature)

# --- Webhook Endpoint ---

@app.post("/webhook")
async def github_webhook(
    request: Request,
    x_github_event: str = Header(..., alias="X-GitHub-Event"),
    x_hub_signature_256: str = Header(..., alias="X-Hub-Signature-256"),
):
    """
    Receives and processes GitHub webhook events for new Pull Request creation.
    """
    # Ensure it's a pull_request event first
    print("/webhook triggered successfully")
    
    # Handle ping event separately
    if x_github_event == "ping":
        print("Received GitHub 'ping' event. Webhook is active.")
        return {"message": "pong"} # Or any success message
    
    if x_github_event != "pull_request":
        print(f"Received non-pull_request event: {x_github_event}. Skipping.")
        return {"message": f"Event type '{x_github_event}' ignored."}

    payload_body = await request.body()

    # 1. Signature Verification
    if not verify_signature(x_hub_signature_256, payload_body):
        print("Webhook signature verification failed!")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid signature"
        )
    print("Webhook signature verified successfully.")

    # 2. Payload Parsing
    try:
        payload = GitHubWebhookPayload.model_validate_json(payload_body)
        print("Payload parsed successfully.")
    except Exception as e:
        print(f"Error parsing JSON payload: {e}")
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"Invalid JSON payload: {e}"
        )

    # --- Filter for only 'opened' PR actions ---
    if payload.action != "opened":
        print(f"Received PR event with action '{payload.action}'. Only 'opened' events are processed. Skipping.")
        return {"message": f"PR event action '{payload.action}' ignored."}

    # If we reach here, it's an 'opened' pull_request event
    print(f"New PR creation event detected! Action: {payload.action}")

    # Extract crucial information
    #pr_id = payload.pull_request.id
    #pr_id = payload.pull_request.number
    repo_id = payload.repository.id
    pr_repo_name = payload.repository.full_name
    changelist_url = payload.pull_request.diff_url
    pr_id = int(changelist_url.split('/pull/')[1].split('.')[0])

    repository_clone_url = payload.repository.clone_url
    pr_action = payload.action # This will now always be "opened"
    pr_title = payload.pull_request.title


    print(f"Received PR event: Action={pr_action}, PR ID={pr_id}, Repo ID={repo_id}")
    print(f"Changelist URL (Diff URL): {changelist_url}")
    print(f"Repository Clone URL: {repository_clone_url}")
    print(f"PR Title: {pr_title}")
    print(f"pr_repo_name: {pr_repo_name} ")

    initial_state_data = {
        "pr_id": pr_id,
        "repo_name": pr_repo_name,
    }
    initial_state = PRReviewState(**initial_state_data)
    #output = graph.invoke(initial_state)
    print(f"completed graph execution with result:{output['last_error']}")
    return
          

@app.get("/")
async def read_root():
    return {"message": "PR Reviewer Bot is running!"}