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=' # 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!"}