github_app / webhooks.py
Samyak000's picture
Rename webhook.py to webhooks.py
8e71598 verified
"""
GitHub Webhooks Handler
Processes real-time GitHub events and updates Supabase automatically
"""
import hmac
import hashlib
import logging
from typing import Optional, Dict, Any
from datetime import datetime
from supabase import Client
logger = logging.getLogger(__name__)
class GitHubWebhookHandler:
"""Handle GitHub webhook events and update Supabase"""
def __init__(self, supabase: Client, webhook_secret: str):
"""
Initialize webhook handler
Args:
supabase: Supabase client instance
webhook_secret: GitHub webhook secret for signature verification
"""
self.supabase = supabase
self.webhook_secret = webhook_secret
def verify_signature(self, payload: bytes, signature: str) -> bool:
"""
Verify GitHub webhook signature
Args:
payload: Raw webhook payload bytes
signature: X-Hub-Signature-256 header value
Returns:
True if signature is valid, False otherwise
"""
expected_signature = "sha256=" + hmac.new(
self.webhook_secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def get_firebase_id_from_installation(self, installation_id: int) -> Optional[str]:
"""Get firebase_id associated with installation_id"""
try:
result = self.supabase.table("Users").select("firebase_id").eq("installation_id", installation_id).limit(1).execute()
if result.data:
return result.data[0]["firebase_id"]
except Exception as e:
logger.error(f"Failed to get firebase_id for installation {installation_id}: {str(e)}")
return None
def get_github_id(self, firebase_id: str, full_name: str) -> Optional[int]:
"""Get github table id for a repo"""
try:
result = self.supabase.table("github").select("id").eq("firebase_id", firebase_id).eq("full_name", full_name).limit(1).execute()
if result.data:
return result.data[0]["id"]
except Exception as e:
logger.error(f"Failed to get github_id for {full_name}: {str(e)}")
return None
# ========================================================================
# PUSH EVENTS (Commits)
# ========================================================================
def handle_push(self, payload: Dict[str, Any]) -> bool:
"""
Handle push event - new commits pushed to repo
Args:
payload: GitHub webhook payload
Returns:
True if handled successfully, False otherwise
"""
try:
installation_id = payload.get("installation", {}).get("id")
repo_full_name = payload.get("repository", {}).get("full_name")
commits = payload.get("commits", [])
if not installation_id or not repo_full_name or not commits:
logger.warning("Incomplete push payload data")
return False
firebase_id = self.get_firebase_id_from_installation(installation_id)
if not firebase_id:
logger.warning(f"No firebase_id found for installation {installation_id}")
return False
github_id = self.get_github_id(firebase_id, repo_full_name)
if not github_id:
logger.warning(f"No github_id found for {repo_full_name}")
return False
# Process each commit
commit_rows = []
for commit in commits:
commit_rows.append({
"github_id": github_id,
"firebase_id": firebase_id,
"sha": commit.get("id"),
"message": commit.get("message"),
"author": commit.get("author", {}).get("name"),
"author_email": commit.get("author", {}).get("email"),
"committed_date": commit.get("timestamp"),
})
if commit_rows:
self.supabase.table("commits").upsert(commit_rows, on_conflict="firebase_id,sha").execute()
logger.info(f"✅ Stored {len(commit_rows)} commits from push event")
return True
except Exception as e:
logger.error(f"Failed to handle push event: {str(e)}", exc_info=True)
return False
# ========================================================================
# PULL REQUEST EVENTS
# ========================================================================
def handle_pull_request(self, payload: Dict[str, Any]) -> bool:
"""
Handle pull request events (opened, synchronize, closed, merged)
Args:
payload: GitHub webhook payload
Returns:
True if handled successfully, False otherwise
"""
try:
action = payload.get("action") # opened, synchronize, closed, reopened, edited
installation_id = payload.get("installation", {}).get("id")
repo_full_name = payload.get("repository", {}).get("full_name")
pr = payload.get("pull_request", {})
if not installation_id or not repo_full_name or not pr:
logger.warning("Incomplete pull_request payload data")
return False
firebase_id = self.get_firebase_id_from_installation(installation_id)
if not firebase_id:
logger.warning(f"No firebase_id found for installation {installation_id}")
return False
github_id = self.get_github_id(firebase_id, repo_full_name)
if not github_id:
logger.warning(f"No github_id found for {repo_full_name}")
return False
pr_data = {
"github_id": github_id,
"firebase_id": firebase_id,
"pr_id": pr.get("id"),
"pr_number": pr.get("number"),
"title": pr.get("title"),
"body": pr.get("body"),
"state": pr.get("state"),
"author": pr.get("user", {}).get("login"),
"created_at": pr.get("created_at"),
"updated_at": pr.get("updated_at"),
"merged_at": pr.get("merged_at"),
"closed_at": pr.get("closed_at"),
"url": pr.get("html_url"),
"head_branch": pr.get("head", {}).get("ref"),
"base_branch": pr.get("base", {}).get("ref"),
"draft": pr.get("draft", False),
"mergeable": pr.get("mergeable"),
"comments": pr.get("comments", 0),
"commits": pr.get("commits", 0),
"additions": pr.get("additions", 0),
"deletions": pr.get("deletions", 0),
"changed_files": pr.get("changed_files", 0),
}
self.supabase.table("pull_requests").upsert([pr_data], on_conflict="firebase_id,pr_id").execute()
logger.info(f"✅ Updated PR #{pr.get('number')} ({action}) in {repo_full_name}")
return True
except Exception as e:
logger.error(f"Failed to handle pull_request event: {str(e)}", exc_info=True)
return False
# ========================================================================
# ISSUES EVENTS
# ========================================================================
def handle_issues(self, payload: Dict[str, Any]) -> bool:
"""
Handle issues events (opened, edited, closed, reopened, etc.)
Args:
payload: GitHub webhook payload
Returns:
True if handled successfully, False otherwise
"""
try:
action = payload.get("action") # opened, edited, closed, reopened
installation_id = payload.get("installation", {}).get("id")
repo_full_name = payload.get("repository", {}).get("full_name")
issue = payload.get("issue", {})
if not installation_id or not repo_full_name or not issue:
logger.warning("Incomplete issues payload data")
return False
# Skip if this is actually a pull request (pulled_request key exists)
if "pull_request" in issue:
logger.debug("Skipping issue event - this is actually a PR")
return False
firebase_id = self.get_firebase_id_from_installation(installation_id)
if not firebase_id:
logger.warning(f"No firebase_id found for installation {installation_id}")
return False
github_id = self.get_github_id(firebase_id, repo_full_name)
if not github_id:
logger.warning(f"No github_id found for {repo_full_name}")
return False
issue_data = {
"github_id": github_id,
"firebase_id": firebase_id,
"issue_id": issue.get("id"),
"issue_number": issue.get("number"),
"title": issue.get("title"),
"body": issue.get("body"),
"state": issue.get("state"),
"author": issue.get("user", {}).get("login"),
"created_at": issue.get("created_at"),
"updated_at": issue.get("updated_at"),
"url": issue.get("html_url"),
"labels": [label.get("name") for label in issue.get("labels", [])],
"comments": issue.get("comments", 0),
"reactions": issue.get("reactions", {}).get("total_count", 0) if issue.get("reactions") else 0,
}
self.supabase.table("issues").upsert([issue_data], on_conflict="firebase_id,issue_id").execute()
logger.info(f"✅ Updated issue #{issue.get('number')} ({action}) in {repo_full_name}")
return True
except Exception as e:
logger.error(f"Failed to handle issues event: {str(e)}", exc_info=True)
return False
# ========================================================================
# REPOSITORY EVENTS
# ========================================================================
def handle_repository(self, payload: Dict[str, Any]) -> bool:
"""
Handle repository events (created, deleted, renamed, etc.)
Args:
payload: GitHub webhook payload
Returns:
True if handled successfully, False otherwise
"""
try:
action = payload.get("action")
installation_id = payload.get("installation", {}).get("id")
repo = payload.get("repository", {})
if not installation_id or not repo:
logger.warning("Incomplete repository payload data")
return False
firebase_id = self.get_firebase_id_from_installation(installation_id)
if not firebase_id:
logger.warning(f"No firebase_id found for installation {installation_id}")
return False
logger.info(f"✅ Repository event ({action}): {repo.get('full_name')}")
return True
except Exception as e:
logger.error(f"Failed to handle repository event: {str(e)}", exc_info=True)
return False
# ========================================================================
# MAIN DISPATCHER
# ========================================================================
def handle_webhook(self, event_type: str, payload: Dict[str, Any]) -> Dict[str, Any]:
"""
Main webhook dispatcher - routes to appropriate handler
Args:
event_type: GitHub event type (from X-GitHub-Event header)
payload: GitHub webhook payload
Returns:
Response dict with status and details
"""
logger.info(f"Processing webhook event: {event_type}")
handlers = {
"push": self.handle_push,
"pull_request": self.handle_pull_request,
"issues": self.handle_issues,
"repository": self.handle_repository,
}
handler = handlers.get(event_type)
if not handler:
logger.warning(f"No handler for event type: {event_type}")
return {
"success": False,
"message": f"No handler for event type: {event_type}",
"event": event_type
}
try:
success = handler(payload)
return {
"success": success,
"message": f"Event {event_type} processed",
"event": event_type
}
except Exception as e:
logger.error(f"Error handling {event_type}: {str(e)}", exc_info=True)
return {
"success": False,
"message": f"Error processing {event_type}: {str(e)}",
"event": event_type
}