diff --git a/.github/workflows/deploy_fastapi_backend.yml b/.github/workflows/deploy_fastapi_backend.yml index 5ad4d6ac..ede816b3 100644 --- a/.github/workflows/deploy_fastapi_backend.yml +++ b/.github/workflows/deploy_fastapi_backend.yml @@ -148,3 +148,18 @@ jobs: # Deploy to AWS Lambda via Serverless sls deploy --stage ${{ github.ref_name }} --verbose + - name: Smoke test deployed /health + env: + EXPECTED_SHA: ${{ github.sha }} + HEALTH_URL: https://api.${{ steps.set_domain.outputs.domain }}/health + run: | + set -euo pipefail + echo "Probing $HEALTH_URL" + RESPONSE=$(curl -fsSL --max-time 30 --retry 3 --retry-delay 5 --retry-connrefused "$HEALTH_URL") + echo "Response: $RESPONSE" + ACTUAL_SHA=$(echo "$RESPONSE" | jq -r '.sha') + if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then + echo "::error::SHA mismatch. expected=$EXPECTED_SHA actual=$ACTUAL_SHA" + exit 1 + fi + echo "Health check passed. sha=$ACTUAL_SHA" diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 027cfe40..757973f2 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -19,7 +19,9 @@ api_key_header = APIKeyHeader(name=get_settings().API_KEY_NAME, auto_error=False oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -async def validate_api_key(api_key_header: str = Depends(api_key_header)): +async def validate_api_key(request: Request, api_key_header: str = Depends(api_key_header)): + if request.url.path == "/health": + return None if api_key_header != get_settings().API_KEY: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Could not validate credentials" diff --git a/backend/app/main.py b/backend/app/main.py index c9733c18..80c3e038 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,5 @@ import logging +import os from fastapi.responses import JSONResponse from fastapi import FastAPI, Depends, Request, status from fastapi.exceptions import RequestValidationError @@ -22,7 +23,6 @@ else: app = FastAPI(dependencies=[Depends(validate_api_key)]) -# Handle 422 errors (validation failures) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): logger.error(f"422 Validation Error at {request.url}") @@ -37,7 +37,6 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE ) -# Handle generic HTTP exceptions (optional, useful for catching 404, 403, etc.) @app.exception_handler(StarletteHTTPException) async def http_exception_handler(request: Request, exc: StarletteHTTPException): logger.warning(f"{exc.status_code} Error at {request.url} - Detail: {exc.detail}") @@ -47,7 +46,6 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException): ) -# Middleware to log requests @app.middleware("http") async def log_requests(request: Request, call_next): logger.info(f"Incoming request: {request.method} {request.url}") @@ -56,6 +54,11 @@ async def log_requests(request: Request, call_next): return response +@app.get("/health") +async def health(): + return {"status": "ok", "sha": os.getenv("GITHUB_SHA", "unknown")} + + app.include_router(portfolio_router.router, prefix="/v1") app.include_router(plan_router.router, prefix="/v1") app.include_router(whlg_router.router, prefix="/v1") @@ -67,70 +70,3 @@ if get_settings().ENVIRONMENT == "local": app.include_router(local_router.router) handler = Mangum(app) -import logging -from fastapi.responses import JSONResponse -from fastapi import FastAPI, Depends, Request, status -from fastapi.exceptions import RequestValidationError -from fastapi.encoders import jsonable_encoder -from starlette.exceptions import HTTPException as StarletteHTTPException -from mangum import Mangum -from backend.app.portfolio import router as portfolio_router -from backend.app.whlg import router as whlg_router -from backend.app.plan import router as plan_router -from backend.app.bulk_uploads import router as bulk_uploads_router -from backend.app.dependencies import validate_api_key -from backend.app.config import get_settings - -logger = logging.getLogger("uvicorn.error") -logging.basicConfig(level=logging.INFO) - -if get_settings().ENVIRONMENT == "local": - app = FastAPI() -else: - app = FastAPI(dependencies=[Depends(validate_api_key)]) - - -# Handle 422 errors (validation failures) -@app.exception_handler(RequestValidationError) -async def validation_exception_handler(request: Request, exc: RequestValidationError): - logger.error(f"422 Validation Error at {request.url}") - logger.error(f"Body: {exc.body}") - logger.error(f"Validation Errors: {exc.errors()}") - return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - content=jsonable_encoder({ - "detail": exc.errors(), - "body": exc.body - }), - ) - - -# Handle generic HTTP exceptions (optional, useful for catching 404, 403, etc.) -@app.exception_handler(StarletteHTTPException) -async def http_exception_handler(request: Request, exc: StarletteHTTPException): - logger.warning(f"{exc.status_code} Error at {request.url} - Detail: {exc.detail}") - return JSONResponse( - status_code=exc.status_code, - content={"detail": exc.detail}, - ) - - -# Middleware to log requests -@app.middleware("http") -async def log_requests(request: Request, call_next): - logger.info(f"Incoming request: {request.method} {request.url}") - response = await call_next(request) - logger.info(f"Response status: {response.status_code}") - return response - - -app.include_router(portfolio_router.router, prefix="/v1") -app.include_router(plan_router.router, prefix="/v1") -app.include_router(whlg_router.router, prefix="/v1") -app.include_router(bulk_uploads_router.router, prefix="/v1") - -if get_settings().ENVIRONMENT == "local": - from app.local import router as local_router - app.include_router(local_router.router) - -handler = Mangum(app)