feat(#157): Set up webhook receiver endpoint

Implement FastAPI webhook receiver for Gitea issue assignment events
with HMAC SHA256 signature verification and event routing.

Implementation details:
- FastAPI application with /webhook/gitea POST endpoint
- HMAC SHA256 signature verification in security.py
- Event routing for assigned, unassigned, closed actions
- Comprehensive logging for all webhook events
- Health check endpoint at /health
- Docker containerization with health checks
- 91% test coverage (exceeds 85% requirement)

TDD workflow followed:
- Wrote 16 tests first (RED phase)
- Implemented features to pass tests (GREEN phase)
- All tests passing with 91% coverage
- Type checking with mypy: success
- Linting with ruff: success

Files created:
- apps/coordinator/src/main.py - FastAPI application
- apps/coordinator/src/webhook.py - Webhook handlers
- apps/coordinator/src/security.py - HMAC verification
- apps/coordinator/src/config.py - Configuration management
- apps/coordinator/tests/ - Comprehensive test suite
- apps/coordinator/Dockerfile - Production container
- apps/coordinator/pyproject.toml - Python project config

Configuration:
- Updated .env.example with GITEA_WEBHOOK_SECRET
- Updated docker-compose.yml with coordinator service

Testing:
- 16 unit and integration tests
- Security tests for signature verification
- Event handler tests for all supported actions
- Health check endpoint tests
- All tests passing with 91% coverage

This unblocks issue #158 (issue parser).

Fixes #157

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 17:41:46 -06:00
parent 658ec0774d
commit e23c09f1f2
17 changed files with 1118 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
"""FastAPI application for mosaic-coordinator webhook receiver."""
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic import BaseModel
from .config import settings
from .webhook import router as webhook_router
# Configure logging
def setup_logging() -> None:
"""Configure logging for the application."""
log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Setup logging on module import
setup_logging()
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""
Application lifespan manager.
Handles startup and shutdown logic.
"""
# Startup
logger.info("Starting mosaic-coordinator webhook receiver")
logger.info(f"Gitea URL: {settings.gitea_url}")
logger.info(f"Log level: {settings.log_level}")
logger.info(f"Server: {settings.host}:{settings.port}")
yield
# Shutdown
logger.info("Shutting down mosaic-coordinator webhook receiver")
# Create FastAPI application
app = FastAPI(
title="Mosaic Coordinator",
description="Webhook receiver for Gitea issue events",
version="0.0.1",
lifespan=lifespan,
)
class HealthResponse(BaseModel):
"""Health check response model."""
status: str
service: str
@app.get("/health", response_model=HealthResponse)
async def health_check() -> HealthResponse:
"""
Health check endpoint.
Returns:
HealthResponse indicating service is healthy
"""
return HealthResponse(status="healthy", service="mosaic-coordinator")
# Include webhook router
app.include_router(webhook_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"src.main:app",
host=settings.host,
port=settings.port,
reload=True,
log_level=settings.log_level.lower(),
)