init
This commit is contained in:
8
backend/app/webhooks/__init__.py
Normal file
8
backend/app/webhooks/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Webhook handlers"""
|
||||
|
||||
from app.webhooks.gitea import handle_gitea_webhook
|
||||
from app.webhooks.github import handle_github_webhook
|
||||
from app.webhooks.bitbucket import handle_bitbucket_webhook
|
||||
|
||||
__all__ = ["handle_gitea_webhook", "handle_github_webhook", "handle_bitbucket_webhook"]
|
||||
|
||||
97
backend/app/webhooks/bitbucket.py
Normal file
97
backend/app/webhooks/bitbucket.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Bitbucket webhook handler"""
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models import Repository, PullRequest, Review
|
||||
from app.models.pull_request import PRStatusEnum
|
||||
from app.models.review import ReviewStatusEnum
|
||||
from app.schemas.webhook import BitbucketWebhook
|
||||
|
||||
|
||||
async def handle_bitbucket_webhook(
|
||||
webhook_data: BitbucketWebhook,
|
||||
db: AsyncSession
|
||||
) -> dict:
|
||||
"""Handle Bitbucket webhook"""
|
||||
|
||||
# Find repository by URL
|
||||
repo_url = webhook_data.repository.get("links", {}).get("html", {}).get("href", "")
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.url == repo_url)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
# Check if repository is active
|
||||
if not repository.is_active:
|
||||
return {"message": "Repository is not active"}
|
||||
|
||||
# Get PR state
|
||||
pr_state = webhook_data.pullrequest.state.lower()
|
||||
|
||||
# Handle PR events
|
||||
if pr_state in ["open", "opened"]:
|
||||
# Create or update PR
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.pullrequest.id
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
|
||||
if not pr:
|
||||
pr = PullRequest(
|
||||
repository_id=repository.id,
|
||||
pr_number=webhook_data.pullrequest.id,
|
||||
title=webhook_data.pullrequest.title,
|
||||
author=webhook_data.pullrequest.author.get("display_name", ""),
|
||||
source_branch=webhook_data.pullrequest.source.get("branch", {}).get("name", ""),
|
||||
target_branch=webhook_data.pullrequest.destination.get("branch", {}).get("name", ""),
|
||||
url=webhook_data.pullrequest.links.get("html", {}).get("href", ""),
|
||||
status=PRStatusEnum.OPEN
|
||||
)
|
||||
db.add(pr)
|
||||
await db.commit()
|
||||
await db.refresh(pr)
|
||||
else:
|
||||
pr.title = webhook_data.pullrequest.title
|
||||
pr.status = PRStatusEnum.OPEN
|
||||
await db.commit()
|
||||
|
||||
# Create review
|
||||
review = Review(
|
||||
pull_request_id=pr.id,
|
||||
status=ReviewStatusEnum.PENDING
|
||||
)
|
||||
db.add(review)
|
||||
await db.commit()
|
||||
await db.refresh(review)
|
||||
|
||||
return {
|
||||
"message": "Review created",
|
||||
"review_id": review.id,
|
||||
"pr_id": pr.id
|
||||
}
|
||||
|
||||
elif pr_state in ["closed", "merged", "declined"]:
|
||||
# Mark PR as closed
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.pullrequest.id
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
if pr:
|
||||
pr.status = PRStatusEnum.CLOSED
|
||||
await db.commit()
|
||||
|
||||
return {"message": "PR closed"}
|
||||
|
||||
return {"message": "Event not handled"}
|
||||
|
||||
116
backend/app/webhooks/gitea.py
Normal file
116
backend/app/webhooks/gitea.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Gitea webhook handler"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models import Repository, PullRequest, Review
|
||||
from app.models.pull_request import PRStatusEnum
|
||||
from app.models.review import ReviewStatusEnum
|
||||
from app.schemas.webhook import GiteaWebhook
|
||||
|
||||
|
||||
def verify_gitea_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||
"""Verify Gitea webhook signature"""
|
||||
if not signature:
|
||||
return False
|
||||
|
||||
expected_signature = hmac.new(
|
||||
secret.encode(),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
|
||||
async def handle_gitea_webhook(
|
||||
webhook_data: GiteaWebhook,
|
||||
signature: str,
|
||||
raw_payload: bytes,
|
||||
db: AsyncSession
|
||||
) -> dict:
|
||||
"""Handle Gitea webhook"""
|
||||
|
||||
# Find repository by URL
|
||||
repo_url = webhook_data.repository.get("html_url", "")
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.url == repo_url)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
# Verify signature
|
||||
if not verify_gitea_signature(raw_payload, signature, repository.webhook_secret):
|
||||
raise HTTPException(status_code=403, detail="Invalid signature")
|
||||
|
||||
# Check if repository is active
|
||||
if not repository.is_active:
|
||||
return {"message": "Repository is not active"}
|
||||
|
||||
# Handle PR events
|
||||
if webhook_data.action in ["opened", "synchronized", "reopened"]:
|
||||
# Create or update PR
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.number
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
|
||||
if not pr:
|
||||
pr = PullRequest(
|
||||
repository_id=repository.id,
|
||||
pr_number=webhook_data.number,
|
||||
title=webhook_data.pull_request.title,
|
||||
author=webhook_data.pull_request.user.get("login", ""),
|
||||
source_branch=webhook_data.pull_request.head.get("ref", ""),
|
||||
target_branch=webhook_data.pull_request.base.get("ref", ""),
|
||||
url=webhook_data.pull_request.html_url,
|
||||
status=PRStatusEnum.OPEN
|
||||
)
|
||||
db.add(pr)
|
||||
await db.commit()
|
||||
await db.refresh(pr)
|
||||
else:
|
||||
pr.title = webhook_data.pull_request.title
|
||||
pr.status = PRStatusEnum.OPEN
|
||||
await db.commit()
|
||||
|
||||
# Create review
|
||||
review = Review(
|
||||
pull_request_id=pr.id,
|
||||
status=ReviewStatusEnum.PENDING
|
||||
)
|
||||
db.add(review)
|
||||
await db.commit()
|
||||
await db.refresh(review)
|
||||
|
||||
return {
|
||||
"message": "Review created",
|
||||
"review_id": review.id,
|
||||
"pr_id": pr.id
|
||||
}
|
||||
|
||||
elif webhook_data.action == "closed":
|
||||
# Mark PR as closed
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.number
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
if pr:
|
||||
pr.status = PRStatusEnum.CLOSED
|
||||
await db.commit()
|
||||
|
||||
return {"message": "PR closed"}
|
||||
|
||||
return {"message": "Event not handled"}
|
||||
|
||||
116
backend/app/webhooks/github.py
Normal file
116
backend/app/webhooks/github.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""GitHub webhook handler"""
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.models import Repository, PullRequest, Review
|
||||
from app.models.pull_request import PRStatusEnum
|
||||
from app.models.review import ReviewStatusEnum
|
||||
from app.schemas.webhook import GitHubWebhook
|
||||
|
||||
|
||||
def verify_github_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||||
"""Verify GitHub webhook signature"""
|
||||
if not signature or not signature.startswith("sha256="):
|
||||
return False
|
||||
|
||||
expected_signature = "sha256=" + hmac.new(
|
||||
secret.encode(),
|
||||
payload,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return hmac.compare_digest(signature, expected_signature)
|
||||
|
||||
|
||||
async def handle_github_webhook(
|
||||
webhook_data: GitHubWebhook,
|
||||
signature: str,
|
||||
raw_payload: bytes,
|
||||
db: AsyncSession
|
||||
) -> dict:
|
||||
"""Handle GitHub webhook"""
|
||||
|
||||
# Find repository by URL
|
||||
repo_url = webhook_data.repository.get("html_url", "")
|
||||
result = await db.execute(
|
||||
select(Repository).where(Repository.url == repo_url)
|
||||
)
|
||||
repository = result.scalar_one_or_none()
|
||||
|
||||
if not repository:
|
||||
raise HTTPException(status_code=404, detail="Repository not found")
|
||||
|
||||
# Verify signature
|
||||
if not verify_github_signature(raw_payload, signature, repository.webhook_secret):
|
||||
raise HTTPException(status_code=403, detail="Invalid signature")
|
||||
|
||||
# Check if repository is active
|
||||
if not repository.is_active:
|
||||
return {"message": "Repository is not active"}
|
||||
|
||||
# Handle PR events
|
||||
if webhook_data.action in ["opened", "synchronize", "reopened"]:
|
||||
# Create or update PR
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.number
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
|
||||
if not pr:
|
||||
pr = PullRequest(
|
||||
repository_id=repository.id,
|
||||
pr_number=webhook_data.number,
|
||||
title=webhook_data.pull_request.title,
|
||||
author=webhook_data.pull_request.user.get("login", ""),
|
||||
source_branch=webhook_data.pull_request.head.get("ref", ""),
|
||||
target_branch=webhook_data.pull_request.base.get("ref", ""),
|
||||
url=webhook_data.pull_request.html_url,
|
||||
status=PRStatusEnum.OPEN
|
||||
)
|
||||
db.add(pr)
|
||||
await db.commit()
|
||||
await db.refresh(pr)
|
||||
else:
|
||||
pr.title = webhook_data.pull_request.title
|
||||
pr.status = PRStatusEnum.OPEN
|
||||
await db.commit()
|
||||
|
||||
# Create review
|
||||
review = Review(
|
||||
pull_request_id=pr.id,
|
||||
status=ReviewStatusEnum.PENDING
|
||||
)
|
||||
db.add(review)
|
||||
await db.commit()
|
||||
await db.refresh(review)
|
||||
|
||||
return {
|
||||
"message": "Review created",
|
||||
"review_id": review.id,
|
||||
"pr_id": pr.id
|
||||
}
|
||||
|
||||
elif webhook_data.action == "closed":
|
||||
# Mark PR as closed
|
||||
result = await db.execute(
|
||||
select(PullRequest).where(
|
||||
PullRequest.repository_id == repository.id,
|
||||
PullRequest.pr_number == webhook_data.number
|
||||
)
|
||||
)
|
||||
pr = result.scalar_one_or_none()
|
||||
if pr:
|
||||
pr.status = PRStatusEnum.CLOSED
|
||||
await db.commit()
|
||||
|
||||
return {"message": "PR closed"}
|
||||
|
||||
return {"message": "Event not handled"}
|
||||
|
||||
Reference in New Issue
Block a user