PR_Review_Bot / app.py
nikhmr1235's picture
Update app.py
81a14c5 verified
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!"}