Spaces:
Paused
Paused
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!"} |