feat: webhook event firing on issue creation (background task)

This commit is contained in:
Zhi
2026-02-22 02:43:34 +00:00
parent a0d81ec9f5
commit 1a76de7c50
3 changed files with 62 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI, Depends, HTTPException, status from fastapi import FastAPI, Depends, HTTPException, status, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
@@ -11,6 +11,7 @@ from pydantic import BaseModel
from app.core.config import get_db, settings from app.core.config import get_db, settings
from app.models import models from app.models import models
from app.schemas import schemas from app.schemas import schemas
from app.services.webhook import fire_webhooks_sync
app = FastAPI( app = FastAPI(
title="HarborForge API", title="HarborForge API",
@@ -99,11 +100,13 @@ async def get_me(current_user: models.User = Depends(get_current_user)):
# ============ Issues API ============ # ============ Issues API ============
@app.post("/issues", response_model=schemas.IssueResponse, status_code=status.HTTP_201_CREATED) @app.post("/issues", response_model=schemas.IssueResponse, status_code=status.HTTP_201_CREATED)
def create_issue(issue: schemas.IssueCreate, db: Session = Depends(get_db)): def create_issue(issue: schemas.IssueCreate, bg: BackgroundTasks, db: Session = Depends(get_db)):
db_issue = models.Issue(**issue.model_dump()) db_issue = models.Issue(**issue.model_dump())
db.add(db_issue) db.add(db_issue)
db.commit() db.commit()
db.refresh(db_issue) db.refresh(db_issue)
event = "resolution.created" if db_issue.issue_type == "resolution" else "issue.created"
bg.add_task(fire_webhooks_sync, event, {"issue_id": db_issue.id, "title": db_issue.title, "type": db_issue.issue_type, "status": db_issue.status}, db_issue.project_id, db)
return db_issue return db_issue

56
app/services/webhook.py Normal file
View File

@@ -0,0 +1,56 @@
import json
import hmac
import hashlib
import logging
from sqlalchemy.orm import Session
from app.models.webhook import Webhook, WebhookLog
logger = logging.getLogger(__name__)
def fire_webhooks_sync(event: str, payload: dict, project_id: int, db: Session):
"""Find matching webhooks and send payloads (sync version)."""
import httpx
webhooks = db.query(Webhook).filter(Webhook.is_active == True).all()
matched = []
for wh in webhooks:
events = [e.strip() for e in wh.events.split(",")]
if event not in events:
continue
if wh.project_id is not None and wh.project_id != project_id:
continue
matched.append(wh)
if not matched:
return
payload_json = json.dumps(payload, default=str)
for wh in matched:
log = WebhookLog(
webhook_id=wh.id,
event=event,
payload=payload_json,
)
try:
headers = {"Content-Type": "application/json"}
if wh.secret:
sig = hmac.new(
wh.secret.encode(), payload_json.encode(), hashlib.sha256
).hexdigest()
headers["X-Webhook-Signature"] = sig
with httpx.Client(timeout=10.0) as client:
resp = client.post(wh.url, content=payload_json, headers=headers)
log.response_status = resp.status_code
log.response_body = resp.text[:1000]
log.success = 200 <= resp.status_code < 300
except Exception as e:
log.response_body = str(e)[:1000]
log.success = False
logger.warning(f"Webhook delivery failed for {wh.url}: {e}")
db.add(log)
db.commit()

View File

@@ -10,3 +10,4 @@ bcrypt==4.0.1
python-multipart==0.0.6 python-multipart==0.0.6
alembic==1.13.1 alembic==1.13.1
python-dotenv==1.0.0 python-dotenv==1.0.0
httpx==0.27.0