import os import sys from fastapi import FastAPI, Request, BackgroundTasks, HTTPException import uvicorn from contextlib import asynccontextmanager # Import the core agent workflow we built earlier from src.main import build_and_run_graph @asynccontextmanager async def lifespan(app: FastAPI): print("šŸš€ Context Brain Webhook Server Starting up...") yield print("šŸ›‘ Context Brain Webhook Server Shutting down...") app = FastAPI(title="GitLab Context Brain Webhook", lifespan=lifespan) def run_agent_safely(project_id: str, mr_iid: str): """ Wrapper to run the LangGraph pipeline. It intentionally catches SystemExit because the original main.py script uses sys.exit(1) to fail the CI Job. In a webhook server context, we don't want sys.exit() to kill the entire FastAPI server! """ print(f"\n[BACKGROUND THREAD] Starting Agent for Project {project_id} / MR {mr_iid}") try: build_and_run_graph(project_id, mr_iid) except SystemExit as e: print(f"[BACKGROUND THREAD] Agent finished logic. (Caught graceful exit code: {e.code})") except Exception as e: print(f"[BACKGROUND THREAD] Agent encountered a fatal error: {e}") finally: print(f"[BACKGROUND THREAD] Finished processing Project {project_id} / MR {mr_iid}\n") @app.get("/health") async def health_check(): return {"status": "ok", "message": "Context Brain Webhook is running!"} @app.post("/webhook/gitlab") async def gitlab_webhook(request: Request, background_tasks: BackgroundTasks): """ GitLab will POST to this endpoint whenever an event occurs. """ # 1. Parse JSON Payload try: payload = await request.json() except Exception: raise HTTPException(status_code=400, detail="Invalid JSON Payload") # 2. Filter for Merge Request Events Only object_kind = payload.get("object_kind") if object_kind != "merge_request": return {"status": "ignored", "reason": f"Event type '{object_kind}' is not a merge request event."} # 3. Extract IDs needed for the API project = payload.get("project", {}) project_id = str(project.get("id")) attributes = payload.get("object_attributes", {}) mr_iid = str(attributes.get("iid")) action = attributes.get("action") # "open", "update", "close" if not project_id or not mr_iid: raise HTTPException(status_code=400, detail="Missing project.id or object_attributes.iid in payload") # We typically only care about new or updated MRs if action not in ["open", "update", "reopen"]: return {"status": "ignored", "reason": f"MR action '{action}' doesn't require analysis."} if action == "update" and "oldrev" not in attributes: # Ignore updates that don't change code (e.g. label changes or comment edits) # This prevents an infinite loop where the agent labels the MR, triggering itself! return {"status": "ignored", "reason": "Update event contains no code changes."} print(f"\nšŸ”” Webhook Triggered! Project: {project_id} | MR: {mr_iid} | Action: {action}") # 4. Offload heavy lifting to a background task # We must respond to GitLab immediately (within 10 seconds), otherwise it marks the webhook as failed. background_tasks.add_task(run_agent_safely, project_id, mr_iid) return { "status": "accepted", "message": f"Processing Project {project_id} MR {mr_iid} in the background." } if __name__ == "__main__": uvicorn.run("webhook:app", host="0.0.0.0", port=8000, reload=True)