feat: add admin panel app, api, and deploy workflows #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy FPAI Admin Backend to AWS Lambda | |
| on: | |
| push: | |
| branches: ["main", "prod"] | |
| paths: | |
| - "backend-admin/**" | |
| - ".github/workflows/deploy-admin-backend-aws.yml" | |
| workflow_dispatch: | |
| permissions: | |
| id-token: write | |
| contents: read | |
| concurrency: | |
| group: deploy-fpai-admin-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' }} | |
| jobs: | |
| lint: | |
| name: Lint Admin Backend | |
| 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" | |
| - name: Install linting tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install ruff | |
| - name: Lint Python files | |
| working-directory: backend-admin | |
| run: | | |
| ruff check . --output-format=github || true | |
| ruff check . --select=E,F,W --exit-zero | |
| - name: Validate Python syntax | |
| working-directory: backend-admin | |
| run: python -m py_compile admin_handler.py | |
| test: | |
| name: Admin Backend 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" | |
| - name: Install test dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install pytest boto3 | |
| - name: Run admin tests | |
| working-directory: backend-admin | |
| run: | | |
| pytest -v tests/test_admin_handler.py tests/test_admin_template_security.py | |
| validate-sam: | |
| name: Validate Admin 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 template | |
| working-directory: backend-admin | |
| run: | | |
| sam validate --lint | |
| echo "✅ Admin SAM template is valid" | |
| validate-routes: | |
| name: Validate Admin Routes | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.DEPLOY_REF }} | |
| - name: Validate route definitions | |
| working-directory: backend-admin | |
| run: | | |
| python << 'EOF' | |
| import ast | |
| import re | |
| import sys | |
| with open("admin_handler.py", "r", encoding="utf-8") as f: | |
| content = f.read() | |
| tree = ast.parse(content) | |
| route_literals = re.findall(r'path\\s*==\\s*["\\']([^"\\']+)["\\']', content) | |
| expected_routes = [ | |
| "/admin/overview", | |
| "/admin/jobs", | |
| "/admin/db", | |
| "/admin/activity", | |
| "/admin/errors", | |
| "/admin/chats/recent", | |
| ] | |
| missing_routes = [r for r in expected_routes if r not in route_literals] | |
| if missing_routes: | |
| print(f"❌ Missing admin routes in handler: {missing_routes}") | |
| sys.exit(1) | |
| if "/admin/chats/{job_id}" not in content: | |
| print("❌ Missing /admin/chats/{job_id} route handling") | |
| sys.exit(1) | |
| functions = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)} | |
| expected_functions = { | |
| "authenticate_admin", | |
| "handle_admin_overview", | |
| "handle_admin_jobs", | |
| "handle_admin_db", | |
| "handle_admin_activity", | |
| "handle_admin_errors", | |
| "handle_admin_chats_recent", | |
| "handle_admin_chat_detail", | |
| "handler", | |
| } | |
| missing_functions = sorted(expected_functions - functions) | |
| if missing_functions: | |
| print(f"❌ Missing admin functions: {missing_functions}") | |
| sys.exit(1) | |
| print("✅ Admin route validation complete") | |
| EOF | |
| quality-gate: | |
| name: Quality Gate | |
| needs: [lint, test, validate-sam, validate-routes] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: All checks passed | |
| run: | | |
| echo "## ✅ All Admin Backend Checks Passed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Proceeding to deployment..." >> $GITHUB_STEP_SUMMARY | |
| deploy: | |
| needs: quality-gate | |
| runs-on: ubuntu-latest | |
| environment: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }} | |
| env: | |
| ADMIN_USER_POOL_ARN: ${{ vars.ADMIN_USER_POOL_ARN }} | |
| ADMIN_ALLOWED_ORIGIN: ${{ vars.ADMIN_ALLOWED_ORIGIN || 'https://admin.fpai.io' }} | |
| ADMIN_ALLOWED_ORIGINS: ${{ vars.ADMIN_ALLOWED_ORIGINS || vars.ADMIN_ALLOWED_ORIGIN || 'https://admin.fpai.io' }} | |
| ADMIN_EMAIL_ALLOWLIST: ${{ vars.ADMIN_EMAIL_ALLOWLIST }} | |
| ADMIN_GROUP_ALLOWLIST: ${{ vars.ADMIN_GROUP_ALLOWLIST }} | |
| BACKEND_STACK_NAME: ${{ vars.BACKEND_STACK_NAME }} | |
| 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" | |
| - 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: Validate required admin config | |
| run: | | |
| if [ -z "${ADMIN_USER_POOL_ARN}" ]; then | |
| echo "::error::Missing ADMIN_USER_POOL_ARN GitHub environment variable" | |
| exit 1 | |
| fi | |
| if [ -z "${ADMIN_EMAIL_ALLOWLIST}" ]; then | |
| echo "::error::Missing ADMIN_EMAIL_ALLOWLIST GitHub environment variable" | |
| exit 1 | |
| fi | |
| - name: Who am I (debug) | |
| run: aws sts get-caller-identity | |
| - name: Resolve backend resource names | |
| id: resolve | |
| run: | | |
| set -euo pipefail | |
| STACK_NAME="${BACKEND_STACK_NAME:-fpai-backend-${{ env.ENVIRONMENT }}}" | |
| echo "Using backend stack: $STACK_NAME" | |
| get_resource_id() { | |
| aws cloudformation describe-stack-resource \ | |
| --stack-name "$STACK_NAME" \ | |
| --logical-resource-id "$1" \ | |
| --query 'StackResourceDetail.PhysicalResourceId' \ | |
| --output text | |
| } | |
| SESSIONS_TABLE_NAME="$(get_resource_id SessionsTable)" | |
| SESSION_ITEMS_TABLE_NAME="$(get_resource_id SessionItemsTable)" | |
| JOBS_TABLE_NAME="$(get_resource_id JobsTable)" | |
| JOB_EVENTS_TABLE_NAME="$(get_resource_id JobEventsTable)" | |
| USERS_TABLE_NAME="$(get_resource_id UsersTable)" | |
| APP_FUNCTION_NAME="$(aws cloudformation describe-stacks \ | |
| --stack-name "$STACK_NAME" \ | |
| --query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' \ | |
| --output text)" | |
| if [ -z "$APP_FUNCTION_NAME" ] || [ "$APP_FUNCTION_NAME" = "None" ]; then | |
| echo "::error::Could not resolve FunctionName output from backend stack $STACK_NAME" | |
| exit 1 | |
| fi | |
| echo "sessions_table_name=$SESSIONS_TABLE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "session_items_table_name=$SESSION_ITEMS_TABLE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "jobs_table_name=$JOBS_TABLE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "job_events_table_name=$JOB_EVENTS_TABLE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "users_table_name=$USERS_TABLE_NAME" >> "$GITHUB_OUTPUT" | |
| echo "app_function_name=$APP_FUNCTION_NAME" >> "$GITHUB_OUTPUT" | |
| echo "app_log_group=/aws/lambda/$APP_FUNCTION_NAME" >> "$GITHUB_OUTPUT" | |
| - name: SAM Build | |
| working-directory: backend-admin | |
| run: sam build --cached --parallel | |
| - name: SAM Deploy | |
| working-directory: backend-admin | |
| run: | | |
| ADMIN_ALLOWED_ORIGIN_CLEAN=$(echo "${ADMIN_ALLOWED_ORIGIN}" | tr -d '[:space:]') | |
| ADMIN_ALLOWED_ORIGINS_CLEAN=$(echo "${ADMIN_ALLOWED_ORIGINS}" | tr -d '[:space:]') | |
| ADMIN_EMAIL_ALLOWLIST_CLEAN=$(echo "${ADMIN_EMAIL_ALLOWLIST}" | tr -d '[:space:]') | |
| ADMIN_GROUP_ALLOWLIST_CLEAN=$(echo "${ADMIN_GROUP_ALLOWLIST}" | tr -d '[:space:]') | |
| sam deploy \ | |
| --config-env ${{ env.ENVIRONMENT }} \ | |
| --no-confirm-changeset \ | |
| --no-fail-on-empty-changeset \ | |
| --resolve-s3 \ | |
| --capabilities CAPABILITY_IAM \ | |
| --parameter-overrides "Environment=${{ env.ENVIRONMENT }} AdminUserPoolArn=${ADMIN_USER_POOL_ARN} AdminAllowedOrigin=${ADMIN_ALLOWED_ORIGIN_CLEAN} AdminAllowedOrigins=${ADMIN_ALLOWED_ORIGINS_CLEAN} AdminEmailAllowlist=${ADMIN_EMAIL_ALLOWLIST_CLEAN} AdminGroupAllowlist=${ADMIN_GROUP_ALLOWLIST_CLEAN} SessionsTableName=${{ steps.resolve.outputs.sessions_table_name }} SessionItemsTableName=${{ steps.resolve.outputs.session_items_table_name }} JobsTableName=${{ steps.resolve.outputs.jobs_table_name }} JobEventsTableName=${{ steps.resolve.outputs.job_events_table_name }} UsersTableName=${{ steps.resolve.outputs.users_table_name }} AppFunctionName=${{ steps.resolve.outputs.app_function_name }} AppLogGroup=${{ steps.resolve.outputs.app_log_group }}" | |
| - name: Get Admin API Endpoint | |
| id: get-endpoint | |
| run: | | |
| ENDPOINT=$(aws cloudformation describe-stacks \ | |
| --stack-name fpai-admin-backend-${{ env.ENVIRONMENT }} \ | |
| --query 'Stacks[0].Outputs[?OutputKey==`AdminApiEndpoint`].OutputValue' \ | |
| --output text) | |
| echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT" | |
| echo "Admin API Endpoint: $ENDPOINT" | |
| - name: Health Check (unauthorized expected) | |
| run: | | |
| ENDPOINT="${{ steps.get-endpoint.outputs.endpoint }}" | |
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$ENDPOINT/admin/overview" || echo "000") | |
| if [ "$STATUS" = "401" ] || [ "$STATUS" = "403" ]; then | |
| echo "✅ Admin API is reachable (status: $STATUS)" | |
| exit 0 | |
| fi | |
| echo "❌ Admin API returned unexpected status: $STATUS" | |
| exit 1 | |
| notify: | |
| needs: deploy | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Deployment Summary | |
| run: | | |
| echo "## Admin Backend 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 |