Spaces:
Paused
Paused
| 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 --- | |
| 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 | |
| async def read_root(): | |
| return {"message": "PR Reviewer Bot is running!"} |