Skip to content

Revert "main block detection for topic" #257

Revert "main block detection for topic"

Revert "main block detection for topic" #257

name: Deploy FPAI Backend to AWS Lambda
on:
push:
branches: ["main", "prod"]
paths:
- "backend/**"
- ".github/workflows/deploy-backend-aws.yml"
workflow_dispatch:
permissions:
id-token: write
contents: read
concurrency:
group: deploy-fpai-backend-${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
cancel-in-progress: true
env:
AWS_REGION: us-east-1
AWS_ROLE_TO_ASSUME: arn:aws:iam::675177356722:role/GitHubActionsDeployer
DEPLOY_REF: ${{ github.ref_name }}
ENVIRONMENT: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_PYTHON_VERSION_WARNING: "1"
jobs:
# ============================================
# PRE-DEPLOYMENT CHECKS (run in parallel)
# ============================================
lint:
name: Lint & Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: .github/workflows/deploy-backend-aws.yml
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install ruff mypy
- name: Run Ruff linter
working-directory: backend
run: |
ruff check . --output-format=github || true
ruff check . --select=E,F,W --exit-zero
- name: Run Ruff formatter check
working-directory: backend
run: |
ruff format --check . || echo "::warning::Code formatting issues detected"
- name: Validate Python syntax (strict)
working-directory: backend
run: |
python -m py_compile lambda_handler.py agent.py llm_client.py wiki_images.py
- name: Check for common issues
working-directory: backend
run: |
# Check for print statements that should be logging
echo "Checking for debug print statements..."
if grep -rn "print(" *.py --include="*.py" | grep -v "test_" | grep -v "__main__" | grep -v "# debug" | head -5; then
echo "::warning::Found print statements - consider using logging instead"
fi
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: .github/workflows/deploy-backend-aws.yml
- name: Install security tools
run: |
python -m pip install --upgrade pip
pip install pip-audit bandit
- name: Run pip-audit (dependency vulnerabilities)
id: pip_audit
working-directory: backend
run: |
# Audit declared requirements directly; avoid installing heavy runtime deps (e.g. manim)
# that require OS libraries just to perform vulnerability checks.
set +e
if grep -qE "^[a-zA-Z]" requirements.txt 2>/dev/null; then
pip-audit -r requirements.txt -f json -o pip-audit-report.json
AUDIT_STATUS=$?
else
echo "No external dependencies to audit"
printf '{"message":"No external dependencies to audit"}\n' > pip-audit-report.json
AUDIT_STATUS=0
fi
set -e
if [ ! -s pip-audit-report.json ]; then
printf '{"audit_error":"pip-audit did not generate a report"}\n' > pip-audit-report.json
fi
VULN_COUNT=$(jq -r '
if has("dependencies") and (.dependencies | type == "array") then
[.dependencies[]? | (.vulns // []) | length] | add // 0
else
0
end
' pip-audit-report.json 2>/dev/null || echo 0)
if [ "$VULN_COUNT" -gt 0 ]; then
echo "::error::pip-audit found $VULN_COUNT vulnerabilities"
FINAL_STATUS=1
elif [ "$AUDIT_STATUS" -ne 0 ]; then
# Dependency-resolution failures (e.g. native build deps) should not block deploy
# when no vulnerability report is available.
echo "::warning::pip-audit failed to fully resolve dependencies; continuing without blocking on resolver error"
FINAL_STATUS=0
else
FINAL_STATUS=0
fi
echo "status=$FINAL_STATUS" >> "$GITHUB_OUTPUT"
echo "vuln_count=$VULN_COUNT" >> "$GITHUB_OUTPUT"
- name: Run Bandit (security linter)
id: bandit_scan
working-directory: backend
run: |
set +e
# Only high-severity issues should block deployment.
bandit -r . -x ./tests -lll -ii -f json -o bandit-report.json
BANDIT_EXIT=$?
set -e
if [ ! -s bandit-report.json ]; then
printf '{"results":[]}\n' > bandit-report.json
fi
HIGH_COUNT=$(jq -r '[.results[]? | select(.issue_severity == "HIGH")] | length' bandit-report.json 2>/dev/null || echo 0)
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "::error::Bandit detected $HIGH_COUNT high-severity findings"
FINAL_STATUS=1
else
FINAL_STATUS=0
if [ "$BANDIT_EXIT" -ne 0 ]; then
echo "::warning::Bandit returned non-zero with no high-severity findings"
fi
fi
echo "status=$FINAL_STATUS" >> "$GITHUB_OUTPUT"
echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT"
- name: Upload backend security reports
uses: actions/upload-artifact@v4
if: always()
with:
name: backend-security-reports
path: |
backend/pip-audit-report.json
backend/bandit-report.json
retention-days: 7
- name: Enforce security gate
run: |
if [ "${{ steps.pip_audit.outputs.status }}" != "0" ]; then
exit 1
fi
if [ "${{ steps.bandit_scan.outputs.status }}" != "0" ]; then
exit 1
fi
secret-scan:
name: Secret Scan (Gitleaks)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
fetch-depth: 0
- name: Install gitleaks CLI
run: |
go version
go install github.com/zricethezav/gitleaks/v8@v8.24.2
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Run gitleaks
run: |
gitleaks detect --source . --redact --report-format sarif --report-path gitleaks-report.sarif
- name: Upload gitleaks report
uses: actions/upload-artifact@v4
if: always()
with:
name: backend-gitleaks-report
path: gitleaks-report.sarif
retention-days: 7
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: .github/workflows/deploy-backend-aws.yml
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov pytest-xdist boto3 numpy
- name: Run unit tests with coverage
working-directory: backend
run: |
python -m pytest -v --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=25
env:
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: dummy
AWS_SECRET_ACCESS_KEY: dummy
JOBS_TABLE: fpai-jobs-test
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: backend/coverage.xml
retention-days: 7
validate-sam:
name: Validate SAM Template
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Validate SAM template
working-directory: backend
run: |
sam validate --lint
echo "✅ SAM template is valid"
- name: Check template structure
working-directory: backend
run: |
# Verify critical resources are defined
if ! grep -q "AWS::Serverless::Function" template.yaml; then
echo "::error::No Lambda function defined in template"
exit 1
fi
if ! grep -q "AWS::Serverless::Api\|Api:" template.yaml; then
echo "::warning::No explicit API Gateway defined"
fi
echo "✅ Template structure looks good"
validate-routes:
name: Validate API Routes
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: .github/workflows/deploy-backend-aws.yml
- name: Validate route definitions
working-directory: backend
run: |
pip install boto3 numpy
python << 'EOF'
import ast
import sys
import re
# Parse lambda_handler.py
import unittest.mock as mock
import sys
mock_boto = mock.MagicMock()
sys.modules['boto3'] = mock_boto
with open("lambda_handler.py", "r") as f:
content = f.read()
tree = ast.parse(content)
# Extract routes from handler function
routes_in_code = []
route_pattern = r'(path|resource)\s*==\s*["\']([^"\']+)["\']'
method_pattern = r'method\s*==\s*["\']([^"\']+)["\']'
routes_found = [m[1] for m in re.findall(route_pattern, content)]
print(f"Found {len(routes_found)} route definitions in code:")
for route in routes_found:
print(f" - {route}")
# Verify handler functions exist for each route
handler_functions = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name.startswith("handle_"):
handler_functions.append(node.name)
print(f"\nHandler functions found: {handler_functions}")
# Basic validation against current route surface
expected_routes = [
"/",
"/access",
"/chat",
"/continue",
"/documents",
"/events/frontend",
"/feedback",
"/health",
"/images",
"/invites/redeem",
"/jobs/{job_id}",
"/jobs/{job_id}/events",
"/memory",
"/session",
"/sessions",
"/status",
"/tools",
"/uploads/prepare",
"/uploads/presigned",
"/waitlist",
]
missing_routes = [r for r in expected_routes if r not in routes_found]
if missing_routes:
print(f"\n❌ Missing expected routes: {missing_routes}")
sys.exit(1)
expected_handlers = [
"handle_chat",
"handle_clear_memory",
"handle_continue",
"handle_delete_session",
"handle_frontend_events",
"handle_feedback",
"handle_get_access",
"handle_get_job",
"handle_get_job_events",
"handle_get_memory",
"handle_get_presigned_url",
"handle_prepare_uploads",
"handle_get_status",
"handle_health",
"handle_list_documents",
"handle_list_sessions",
"handle_redeem_invite",
"handle_search_images",
"handle_session",
"handle_tools",
"handle_update_memory",
"handle_update_session",
"handle_waitlist_signup",
]
missing_handlers = [h for h in expected_handlers if h not in handler_functions]
if missing_handlers:
print(f"\n❌ Missing expected handlers: {missing_handlers}")
sys.exit(1)
print("\n✅ Route validation complete")
EOF
- name: Test route imports
working-directory: backend
run: |
python -c "
import sys
try:
import unittest.mock as mock
mock_boto = mock.MagicMock()
sys.modules['boto3'] = mock_boto
from lambda_handler import handler
print('✅ Lambda handler imports successfully')
# Test each route with mock events
test_routes = [
{'httpMethod': 'GET', 'path': '/health', 'ok': {200, 401}},
{'httpMethod': 'GET', 'path': '/status', 'ok': {200, 401}},
{'httpMethod': 'GET', 'path': '/tools', 'ok': {200, 401}},
{'httpMethod': 'OPTIONS', 'path': '/chat', 'ok': {200}},
{'httpMethod': 'POST', 'path': '/waitlist', 'body': '{\"email\":\"ci@example.com\"}', 'ok': {200, 201, 202, 400, 401, 410}},
]
for event in test_routes:
try:
response = handler(event, None)
status = response.get('statusCode', 'unknown')
print(f\" ✅ {event['httpMethod']} {event['path']} -> {status}\")
if status not in event['ok']:
print(f\" ❌ Unexpected status for {event['path']}: {status} (expected one of {sorted(event['ok'])})\")
sys.exit(1)
except Exception as e:
print(f\" ❌ {event['httpMethod']} {event['path']} -> Error: {e}\")
sys.exit(1)
except Exception as e:
print(f'❌ Import failed: {e}')
sys.exit(1)
"
env:
AWS_DEFAULT_REGION: us-east-1
AWS_ACCESS_KEY_ID: dummy
AWS_SECRET_ACCESS_KEY: dummy
JOBS_TABLE: fpai-jobs-test
# ============================================
# QUALITY GATE (all checks must pass)
# ============================================
quality-gate:
name: Quality Gate
needs: [lint, security, secret-scan, test, validate-sam, validate-routes]
runs-on: ubuntu-latest
steps:
- name: All checks passed
run: |
echo "## ✅ All Pre-Deployment Checks Passed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Lint & Code Quality | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Security Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Secret Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Unit Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "| SAM Validation | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "| Route Validation | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Proceeding to deployment..." >> $GITHUB_STEP_SUMMARY
# ============================================
# DEPLOYMENT (only after quality gate passes)
# ============================================
deploy:
needs: quality-gate
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
env:
FIREBASE_PROJECT_ID: ${{ vars.BACKEND_FIREBASE_PROJECT_ID || 'fpai-cbcb2' }}
OPENAI_MODEL: ${{ vars.BACKEND_OPENAI_MODEL || 'gpt-5.2' }}
API_KEYS_SECRET_ID: ${{ vars.BACKEND_API_KEYS_SECRET_ID }}
ALLOWED_ORIGINS: ${{ vars.BACKEND_ALLOWED_ORIGINS || 'http://localhost:3000,http://localhost:5173,https://fpai.reunifylabs.com,https://d1gyb6wodlq2q2.cloudfront.net' }}
ASSETS_ALLOWED_ORIGINS: ${{ vars.BACKEND_ASSETS_ALLOWED_ORIGINS || 'http://localhost:3000,http://localhost:5173,https://d1gyb6wodlq2q2.cloudfront.net,https://fpai.reunifylabs.com' }}
SHARE_PUBLIC_BASE_URL: ${{ vars.BACKEND_SHARE_PUBLIC_BASE_URL || 'https://fpai.reunifylabs.com' }}
REQUIRE_BRAVE_API_KEY: ${{ vars.BACKEND_REQUIRE_BRAVE_API_KEY || 'false' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: |
backend/requirements.txt
backend/template.yaml
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
- name: Cache SAM build artifacts
uses: actions/cache@v4
with:
path: backend/.aws-sam/cache
key: ${{ runner.os }}-sam-${{ env.ENVIRONMENT }}-${{ hashFiles('backend/template.yaml', 'backend/requirements.txt') }}
restore-keys: |
${{ runner.os }}-sam-${{ env.ENVIRONMENT }}-
${{ runner.os }}-sam-
- name: Who am I (debug)
run: aws sts get-caller-identity
- name: Sync secrets to AWS Secrets Manager
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
FIREBASE_SERVICE_ACCOUNT_JSON: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
REQUIRE_BRAVE_API_KEY: ${{ env.REQUIRE_BRAVE_API_KEY }}
run: |
if [ -z "$OPENAI_API_KEY" ]; then
echo "::error::Missing GitHub secret OPENAI_API_KEY"
exit 1
fi
EFFECTIVE_GEMINI_API_KEY="${GEMINI_API_KEY:-$GOOGLE_API_KEY}"
if [ -z "$EFFECTIVE_GEMINI_API_KEY" ]; then
echo "::error::Missing GitHub secret GEMINI_API_KEY (or GOOGLE_API_KEY)"
exit 1
fi
if [ -z "$FIREBASE_SERVICE_ACCOUNT_JSON" ]; then
echo "::error::Missing GitHub secret FIREBASE_SERVICE_ACCOUNT_JSON"
exit 1
fi
if [ "${REQUIRE_BRAVE_API_KEY}" != "false" ] && [ -z "$BRAVE_API_KEY" ]; then
echo "::error::Missing GitHub secret BRAVE_API_KEY"
exit 1
fi
if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
DEFAULT_SECRET_ID="fpai/api-keys"
else
DEFAULT_SECRET_ID="fpai/api-keys-${{ env.ENVIRONMENT }}"
fi
SECRET_ID="${API_KEYS_SECRET_ID:-$DEFAULT_SECRET_ID}"
echo "Using Secrets Manager secret: $SECRET_ID"
if aws secretsmanager describe-secret --secret-id "$SECRET_ID" >/dev/null 2>&1; then
SECRET_EXISTS=true
CURRENT_SECRET=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" \
--query SecretString \
--output text 2>/dev/null || echo "{}")
else
SECRET_EXISTS=false
CURRENT_SECRET="{}"
fi
UPDATED_SECRET=$(printf '%s' "$CURRENT_SECRET" \
| jq --arg openai_key "$OPENAI_API_KEY" \
--arg gemini_key "$EFFECTIVE_GEMINI_API_KEY" \
--arg brave_key "$BRAVE_API_KEY" \
--arg firebase_sa "$FIREBASE_SERVICE_ACCOUNT_JSON" \
'.OPENAI_API_KEY = $openai_key
| .GEMINI_API_KEY = $gemini_key
| .FIREBASE_SERVICE_ACCOUNT_JSON = $firebase_sa
| if (($brave_key // "") | length) > 0 then .BRAVE_API_KEY = $brave_key else . end')
if [ "$SECRET_EXISTS" = "true" ]; then
aws secretsmanager put-secret-value \
--secret-id "$SECRET_ID" \
--secret-string "$UPDATED_SECRET"
else
aws secretsmanager create-secret \
--name "$SECRET_ID" \
--secret-string "$UPDATED_SECRET"
fi
echo "✅ Updated OPENAI_API_KEY + GEMINI_API_KEY + FIREBASE_SERVICE_ACCOUNT_JSON in Secrets Manager ($SECRET_ID)"
if [ -n "$BRAVE_API_KEY" ]; then
echo "✅ BRAVE_API_KEY was also updated in Secrets Manager"
else
echo "INFO: BRAVE_API_KEY not provided; existing Secrets Manager value preserved"
fi
- name: Validate required Secrets Manager fields
env:
REQUIRE_BRAVE_API_KEY: ${{ env.REQUIRE_BRAVE_API_KEY }}
run: |
if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
DEFAULT_SECRET_ID="fpai/api-keys"
else
DEFAULT_SECRET_ID="fpai/api-keys-${{ env.ENVIRONMENT }}"
fi
SECRET_ID="${API_KEYS_SECRET_ID:-$DEFAULT_SECRET_ID}"
SECRET_JSON=$(aws secretsmanager get-secret-value \
--secret-id "$SECRET_ID" \
--query SecretString \
--output text)
echo "$SECRET_JSON" | jq -e '
((.OPENAI_API_KEY // "") | length > 0) and
((.GEMINI_API_KEY // "") | length > 0) and
((.FIREBASE_SERVICE_ACCOUNT_JSON // "") | length > 0)
' >/dev/null
if [ "${REQUIRE_BRAVE_API_KEY}" != "false" ]; then
echo "$SECRET_JSON" | jq -e '(.BRAVE_API_KEY // "") | length > 0' >/dev/null
fi
- name: Validate required dev assets CORS origins
run: |
set -euo pipefail
ASSETS_ALLOWED_ORIGINS_CLEAN=$(echo "${ASSETS_ALLOWED_ORIGINS}" | tr -d '[:space:]')
REQUIRED_ORIGINS=(
"http://localhost:3000"
"http://localhost:5173"
"https://d1gyb6wodlq2q2.cloudfront.net"
"https://fpai.reunifylabs.com"
)
MISSING=()
for origin in "${REQUIRED_ORIGINS[@]}"; do
if [[ ",${ASSETS_ALLOWED_ORIGINS_CLEAN}," != *",${origin},"* ]]; then
MISSING+=("${origin}")
fi
done
if [ "${#MISSING[@]}" -gt 0 ]; then
echo "::error::ASSETS_ALLOWED_ORIGINS is missing required values: ${MISSING[*]}"
exit 1
fi
- name: SAM Build
working-directory: backend
run: sam build --cached --parallel --cache-dir .aws-sam/cache
- name: SAM Deploy
working-directory: backend
run: |
if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
DEFAULT_SECRET_ID="fpai/api-keys"
else
DEFAULT_SECRET_ID="fpai/api-keys-${{ env.ENVIRONMENT }}"
fi
SECRET_ID="${API_KEYS_SECRET_ID:-$DEFAULT_SECRET_ID}"
ALLOWED_ORIGINS_CLEAN=$(echo "${ALLOWED_ORIGINS}" | tr -d '[:space:]')
ASSETS_ALLOWED_ORIGINS_CLEAN=$(echo "${ASSETS_ALLOWED_ORIGINS}" | tr -d '[:space:]')
SHARE_PUBLIC_BASE_URL_CLEAN=$(echo "${SHARE_PUBLIC_BASE_URL}" | tr -d '[:space:]')
if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
BETA_GATE_ENABLED=true
else
BETA_GATE_ENABLED=false
fi
sam deploy \
--config-env ${{ env.ENVIRONMENT }} \
--no-confirm-changeset \
--no-fail-on-empty-changeset \
--resolve-s3 \
--resolve-image-repos \
--capabilities CAPABILITY_IAM \
--parameter-overrides "Environment=${{ env.ENVIRONMENT }} OpenAIModel=${OPENAI_MODEL} FirebaseProjectId=${FIREBASE_PROJECT_ID} BetaGateEnabled=${BETA_GATE_ENABLED} EnableJobEventStreaming=true ApiKeysSecretId=${SECRET_ID} AllowedOrigins=${ALLOWED_ORIGINS_CLEAN} AssetsAllowedOrigins=${ASSETS_ALLOWED_ORIGINS_CLEAN} SharePublicBaseUrl=${SHARE_PUBLIC_BASE_URL_CLEAN}"
- name: Force API Gateway stage deployment
run: |
ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name fpai-backend-${{ env.ENVIRONMENT }} \
--query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \
--output text)
REST_API_ID=$(echo "$ENDPOINT" | sed -E 's#https://([^.]+)\.execute-api\..*#\1#')
aws apigateway create-deployment \
--rest-api-id "$REST_API_ID" \
--stage-name "${{ env.ENVIRONMENT }}" \
--description "github-actions-${GITHUB_RUN_ID}-${GITHUB_SHA}"
- name: Get API Endpoint
id: get-endpoint
working-directory: backend
run: |
ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name fpai-backend-${{ env.ENVIRONMENT }} \
--query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \
--output text)
echo "endpoint=$ENDPOINT" >> $GITHUB_OUTPUT
echo "API Endpoint: $ENDPOINT"
- name: Health Check
run: |
ENDPOINT="${{ steps.get-endpoint.outputs.endpoint }}"
echo "Testing health endpoint: $ENDPOINT/health"
for i in {1..5}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$ENDPOINT/health" || echo "000")
# 200 = healthy (auth disabled), 401 = Lambda is up but auth is enforced — both mean the function is running.
if [ "$STATUS" = "200" ] || [ "$STATUS" = "401" ]; then
echo "✅ Lambda is responding (status: $STATUS)"
exit 0
fi
echo "Attempt $i: Status $STATUS, retrying..."
sleep 5
done
echo "❌ Health check failed after 5 attempts"
exit 1
- name: Test Chat Endpoint
if: success()
run: |
ENDPOINT="${{ steps.get-endpoint.outputs.endpoint }}"
echo "Testing chat endpoint reachability: $ENDPOINT/chat"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$ENDPOINT/chat" \
-H "Content-Type: application/json" \
-d '{"message": "Hello"}')
# 200/202 = success, 401 = auth enforced (expected in prod), 400 = reached but bad input — all confirm Lambda is live.
if [ "$STATUS" = "200" ] || [ "$STATUS" = "202" ] || [ "$STATUS" = "401" ] || [ "$STATUS" = "400" ]; then
echo "✅ Chat endpoint reachable (status: $STATUS)"
else
echo "⚠️ Chat endpoint returned unexpected status: $STATUS"
fi
notify:
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Deployment Summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Environment | ${{ env.ENVIRONMENT }} |" >> $GITHUB_STEP_SUMMARY
echo "| Status | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Triggered by | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY
echo "| Commit | ${{ github.sha }} |" >> $GITHUB_STEP_SUMMARY