fix(#260): restore admin portal CORS access to AI backend #13
Workflow file for this run
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 Production | |
| on: | |
| pull_request: | |
| branches: [master] | |
| types: [closed] | |
| workflow_dispatch: | |
| inputs: | |
| skip_tests: | |
| description: 'Skip tests before deployment (NOT RECOMMENDED)' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: | |
| - 'true' | |
| - 'false' | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| jobs: | |
| validate-promotion: | |
| name: Validate Promotion Event | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_deploy: ${{ steps.promotion-check.outputs.should_deploy }} | |
| trigger_reason: ${{ steps.promotion-check.outputs.trigger_reason }} | |
| steps: | |
| - name: Evaluate deployment trigger | |
| id: promotion-check | |
| shell: bash | |
| run: | | |
| SHOULD_DEPLOY="false" | |
| TRIGGER_REASON="not-eligible" | |
| HEAD_REF="${{ github.event.pull_request.head.ref }}" | |
| if [ "${{ github.event_name }}" == "pull_request" ] && \ | |
| [ "${{ github.event.pull_request.merged }}" == "true" ] && \ | |
| [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ | |
| [ "$HEAD_REF" == "staging" ]; then | |
| SHOULD_DEPLOY="true" | |
| TRIGGER_REASON="merged-staging-to-master-pr" | |
| elif [ "${{ github.event_name }}" == "pull_request" ] && \ | |
| [ "${{ github.event.pull_request.merged }}" == "true" ] && \ | |
| [ "${{ github.event.pull_request.base.ref }}" == "master" ] && \ | |
| [[ "$HEAD_REF" == hotfix/* ]]; then | |
| SHOULD_DEPLOY="true" | |
| TRIGGER_REASON="merged-hotfix-to-master-pr" | |
| elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| SHOULD_DEPLOY="true" | |
| TRIGGER_REASON="manual-dispatch" | |
| fi | |
| echo "should_deploy=$SHOULD_DEPLOY" >> "$GITHUB_OUTPUT" | |
| echo "trigger_reason=$TRIGGER_REASON" >> "$GITHUB_OUTPUT" | |
| echo "Promotion check result: $SHOULD_DEPLOY" | |
| echo "Reason: $TRIGGER_REASON" | |
| - name: Skip notice | |
| if: steps.promotion-check.outputs.should_deploy != 'true' | |
| run: | | |
| echo "No production deployment triggered." | |
| echo "This workflow deploys on merged PRs from staging/hotfix -> master, or manual dispatch." | |
| pre-deployment-checks: | |
| name: Pre-Deployment Validation | |
| runs-on: ubuntu-latest | |
| needs: [validate-promotion] | |
| if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && (github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests != 'true') }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| version: "latest" | |
| enable-cache: true | |
| - name: Create virtual environment | |
| run: uv venv .venv | |
| - name: Install dependencies | |
| run: | | |
| source .venv/bin/activate | |
| uv pip install -r coaching/requirements.txt | |
| uv pip install -r coaching/requirements-dev.txt | |
| shell: bash | |
| - name: Run Ruff Linting | |
| run: | | |
| source .venv/bin/activate | |
| python -m ruff check . --exclude=".venv,venv,__pycache__,.pytest_cache" | |
| shell: bash | |
| - name: Run MyPy Type Checking | |
| run: | | |
| source .venv/bin/activate | |
| python -m mypy coaching/src/ shared/ --config-file=pyproject.toml | |
| shell: bash | |
| - name: Run Unit Tests | |
| run: | | |
| source .venv/bin/activate | |
| python -m pytest coaching/tests/unit/ -v --cov=coaching/src --cov-fail-under=70 | |
| shell: bash | |
| env: | |
| PYTHONPATH: coaching:shared:. | |
| deploy-infrastructure: | |
| name: Deploy Infrastructure | |
| runs-on: ubuntu-latest | |
| needs: [validate-promotion, pre-deployment-checks] | |
| if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && (needs.pre-deployment-checks.result == 'success' || (github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests == 'true')) }} | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Pulumi Python dependencies | |
| working-directory: infrastructure/pulumi | |
| run: pip install -r requirements.txt | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-region: us-east-1 | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| - name: Deploy Infrastructure | |
| uses: pulumi/actions@v5 | |
| with: | |
| command: up | |
| stack-name: prod | |
| work-dir: infrastructure/pulumi | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_REGION: us-east-1 | |
| deploy-coaching: | |
| name: Deploy to Production | |
| runs-on: ubuntu-latest | |
| needs: [validate-promotion, deploy-infrastructure] | |
| if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && needs.deploy-infrastructure.result == 'success' }} | |
| permissions: | |
| id-token: write | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install Pulumi Python dependencies | |
| working-directory: coaching/pulumi | |
| run: pip install -r requirements.txt | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-region: us-east-1 | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| - name: Deploy Coaching Service | |
| uses: pulumi/actions@v5 | |
| with: | |
| command: up | |
| stack-name: prod | |
| work-dir: coaching/pulumi | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_REGION: us-east-1 | |
| - name: Get API Gateway URL | |
| id: api-url | |
| working-directory: coaching/pulumi | |
| run: | | |
| URL=$(pulumi stack output customDomainUrl --stack prod) | |
| echo "url=$URL" >> $GITHUB_OUTPUT | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| - name: Lambda Runtime State Check | |
| working-directory: coaching/pulumi | |
| run: | | |
| echo "Validating Lambda runtime state..." | |
| LAMBDA_ARN=$(pulumi stack output lambdaArn --stack prod) | |
| LAMBDA_NAME=${LAMBDA_ARN##*:function:} | |
| for ATTEMPT in {1..18}; do | |
| STATE=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.State" --output text) | |
| REASON=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.StateReason" --output text) | |
| UPDATE_STATUS=$(aws lambda get-function --function-name "$LAMBDA_NAME" --region us-east-1 --query "Configuration.LastUpdateStatus" --output text) | |
| echo "Attempt $ATTEMPT: state=$STATE, updateStatus=$UPDATE_STATUS, reason=$REASON" | |
| if [ "$STATE" == "Active" ] && [ "$UPDATE_STATUS" == "Successful" ]; then | |
| echo "✅ Lambda state is Active and update status is Successful" | |
| exit 0 | |
| fi | |
| sleep 10 | |
| done | |
| echo "❌ Lambda did not reach Active/Successful in expected time window" | |
| exit 1 | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| - name: Create GitHub Release | |
| uses: actions/create-release@v1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag_name: v${{ github.run_number }} | |
| release_name: Release v${{ github.run_number }} | |
| body: | | |
| Production deployment of PurposePath Coaching API | |
| **Deployment Details:** | |
| - Environment: Production | |
| - Stack: prod | |
| - API URL: ${{ steps.api-url.outputs.url }} | |
| - Deployed at: ${{ github.event.pull_request.merged_at || github.event.repository.updated_at }} | |
| - Commit: ${{ github.event.pull_request.merge_commit_sha || github.sha }} | |
| draft: false | |
| prerelease: false | |
| continue-on-error: true | |
| - name: Deployment Summary | |
| run: | | |
| echo "## 🚀 Production Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Deployment successful" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Environment:** Production" >> $GITHUB_STEP_SUMMARY | |
| echo "**Stack:** prod" >> $GITHUB_STEP_SUMMARY | |
| echo "**API URL:** ${{ steps.api-url.outputs.url }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Region:** us-east-1" >> $GITHUB_STEP_SUMMARY | |
| echo "**Release:** v${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Deployed at:** $(date -u)" >> $GITHUB_STEP_SUMMARY | |
| smoke-tests: | |
| name: Post-Deployment Smoke Tests | |
| runs-on: ubuntu-latest | |
| needs: [validate-promotion, deploy-coaching] | |
| if: ${{ needs.validate-promotion.outputs.should_deploy == 'true' && needs.deploy-coaching.result == 'success' }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Install Pulumi CLI | |
| uses: pulumi/actions@v5 | |
| with: | |
| pulumi-version: 'latest' | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| aws-region: us-east-1 | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| - name: Get API Gateway URL | |
| id: api-url | |
| working-directory: coaching/pulumi | |
| run: | | |
| URL=$(pulumi stack output customDomainUrl --stack prod) | |
| echo "url=$URL" >> $GITHUB_OUTPUT | |
| env: | |
| PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} | |
| - name: Health Check | |
| run: | | |
| echo "Testing API health endpoint..." | |
| HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${{ steps.api-url.outputs.url }}/health || echo "000") | |
| if [ "$HTTP_CODE" == "200" ] || [ "$HTTP_CODE" == "404" ]; then | |
| echo "✅ API is responding (HTTP $HTTP_CODE)" | |
| else | |
| echo "⚠️ API returned HTTP $HTTP_CODE - Investigation needed" | |
| exit 1 | |
| fi | |
| - name: CORS Preflight Check | |
| run: | | |
| echo "Testing CORS preflight behavior..." | |
| TARGET="${{ steps.api-url.outputs.url }}/api/v1/health" | |
| ORIGINS=( | |
| "https://dev.purposepath.app" | |
| "https://staging.purposepath.app" | |
| "https://preprod.purposepath.app" | |
| "https://www.purposepath.app" | |
| "https://purposepath.app" | |
| ) | |
| for ORIGIN in "${ORIGINS[@]}"; do | |
| echo "Checking origin: $ORIGIN" | |
| CORS_HEADERS=$(curl -s -D - -o /dev/null -X OPTIONS "$TARGET" \ | |
| -H "Origin: $ORIGIN" \ | |
| -H "Access-Control-Request-Method: GET" \ | |
| -H "Access-Control-Request-Headers: Authorization,Content-Type,Baggage,Sentry-Trace") | |
| ALLOW_ORIGIN=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-origin"{print $2}' | tail -n 1) | |
| ALLOW_CREDENTIALS=$(echo "$CORS_HEADERS" | tr -d '\r' | awk -F': ' 'tolower($1)=="access-control-allow-credentials"{print $2}' | tail -n 1) | |
| if [ "$ALLOW_ORIGIN" != "$ORIGIN" ]; then | |
| echo "❌ Invalid Access-Control-Allow-Origin: '$ALLOW_ORIGIN' (expected '$ORIGIN')" | |
| exit 1 | |
| fi | |
| if [ "$ALLOW_CREDENTIALS" != "true" ]; then | |
| echo "❌ Invalid Access-Control-Allow-Credentials: '$ALLOW_CREDENTIALS' (expected 'true')" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ CORS preflight returned expected headers for all production frontend origins" | |
| - name: Smoke Test Summary | |
| run: | | |
| echo "## Smoke Tests - Production" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Health check passed" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ API is responsive" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ CORS preflight check passed" >> $GITHUB_STEP_SUMMARY | |
| echo "✅ Production deployment verified" >> $GITHUB_STEP_SUMMARY | |
| notify-deployment: | |
| name: Notify Team | |
| runs-on: ubuntu-latest | |
| needs: [validate-promotion, deploy-coaching, smoke-tests] | |
| if: ${{ always() && needs.validate-promotion.outputs.should_deploy == 'true' }} | |
| steps: | |
| - name: Deployment Status | |
| run: | | |
| if [ "${{ needs.deploy-coaching.result }}" == "success" ] && [ "${{ needs.smoke-tests.result }}" == "success" ]; then | |
| echo "✅ Production deployment completed successfully" | |
| else | |
| echo "❌ Production deployment encountered issues" | |
| exit 1 | |
| fi |