Merge pull request #1784 from kirill-markin/claude/agent-followups-p19 #1017
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: AWS/Web Release | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'apps/auth/**' | |
| - 'apps/backend/**' | |
| - 'apps/web/**' | |
| - 'apps/admin/**' | |
| - 'db/**' | |
| - 'infra/**' | |
| - 'scripts/checks/check-agent-api-smoke.sh' | |
| - 'scripts/checks/check-api-health.sh' | |
| - 'scripts/checks/check-demo-cognito-users.sh' | |
| - 'scripts/checks/check-multipart-completion-reconciliation-schedule.sh' | |
| - 'scripts/checks/check-mcp-smoke.sh' | |
| - 'scripts/checks/check-public-endpoints.sh' | |
| - 'scripts/deploy/deploy-admin.sh' | |
| - 'scripts/deploy/deploy-web.sh' | |
| - 'scripts/generate/generate-catalog-dump.sh' | |
| - 'scripts/generate/generate-global-metrics-snapshot.sh' | |
| - 'scripts/deploy/migrate-aws.sh' | |
| - 'scripts/generate/write-ci-cdk-context.py' | |
| - '.github/workflows/aws-web-release.yml' | |
| workflow_dispatch: | |
| concurrency: | |
| group: main-release | |
| cancel-in-progress: false | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: read | |
| id-token: write | |
| env: | |
| AWS_REGION: ${{ vars.AWS_REGION }} | |
| STACK_NAME: FlashcardsOpenSourceApp | |
| jobs: | |
| changes: | |
| name: Detect changed areas | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| aws_changed: ${{ steps.detect.outputs.aws_changed }} | |
| auth_changed: ${{ steps.detect.outputs.auth_changed }} | |
| backend_changed: ${{ steps.detect.outputs.backend_changed }} | |
| web_changed: ${{ steps.detect.outputs.web_changed }} | |
| admin_changed: ${{ steps.detect.outputs.admin_changed }} | |
| infra_changed: ${{ steps.detect.outputs.infra_changed }} | |
| changed_files: ${{ steps.detect.outputs.changed_files }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect changed files | |
| id: detect | |
| env: | |
| PUSH_BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| aws_release_script_pattern='^scripts/(checks/(check-agent-api-smoke|check-api-health|check-demo-cognito-users|check-mcp-smoke|check-multipart-completion-reconciliation-schedule|check-public-endpoints)\.sh|deploy/(deploy-admin|deploy-web|migrate-aws)\.sh|generate/(generate-catalog-dump\.sh|generate-global-metrics-snapshot\.sh|write-ci-cdk-context\.py))$' | |
| changed_files_path="${RUNNER_TEMP}/changed-files.txt" | |
| before_sha="${PUSH_BEFORE_SHA:-}" | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| printf '%s\n' 'workflow_dispatch (forced AWS deploy)' > "${changed_files_path}" | |
| elif [[ "${GITHUB_EVENT_NAME}" == "push" ]] && [[ -n "${before_sha}" ]] && [[ "${before_sha}" != "0000000000000000000000000000000000000000" ]]; then | |
| git diff --name-only "${before_sha}" "${GITHUB_SHA}" > "${changed_files_path}" | |
| elif git rev-parse HEAD^ >/dev/null 2>&1; then | |
| git diff --name-only HEAD^ HEAD > "${changed_files_path}" | |
| else | |
| git ls-files > "${changed_files_path}" | |
| fi | |
| aws_changed=false | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| aws_changed=true | |
| elif grep -Eq "^(apps/auth/|apps/backend/|apps/web/|apps/admin/|db/|infra/|\\.github/workflows/aws-web-release\\.yml$)" "${changed_files_path}" || grep -Eq "${aws_release_script_pattern}" "${changed_files_path}"; then | |
| aws_changed=true | |
| fi | |
| auth_changed=false | |
| backend_changed=false | |
| web_changed=false | |
| admin_changed=false | |
| infra_changed=false | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| auth_changed=true | |
| backend_changed=true | |
| web_changed=true | |
| admin_changed=true | |
| infra_changed=true | |
| else | |
| if grep -Eq '^(apps/auth/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then | |
| auth_changed=true | |
| fi | |
| if grep -Eq '^(apps/backend/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then | |
| backend_changed=true | |
| fi | |
| if grep -Eq '^(apps/web/|apps/backend/src/scheduling/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then | |
| web_changed=true | |
| fi | |
| if grep -Eq '^(apps/admin/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then | |
| admin_changed=true | |
| fi | |
| if grep -Eq '^(infra/|\.github/workflows/aws-web-release\.yml$)' "${changed_files_path}"; then | |
| infra_changed=true | |
| fi | |
| fi | |
| { | |
| echo "aws_changed=${aws_changed}" | |
| echo "auth_changed=${auth_changed}" | |
| echo "backend_changed=${backend_changed}" | |
| echo "web_changed=${web_changed}" | |
| echo "admin_changed=${admin_changed}" | |
| echo "infra_changed=${infra_changed}" | |
| echo 'changed_files<<EOF' | |
| cat "${changed_files_path}" | |
| echo 'EOF' | |
| } >> "${GITHUB_OUTPUT}" | |
| - name: Summarize changed areas | |
| env: | |
| CHANGED_FILES: ${{ steps.detect.outputs.changed_files }} | |
| run: | | |
| { | |
| echo "## Release scope" | |
| echo "" | |
| echo "- AWS changed: \`${{ steps.detect.outputs.aws_changed }}\`" | |
| echo "- Auth changed: \`${{ steps.detect.outputs.auth_changed }}\`" | |
| echo "- Backend changed: \`${{ steps.detect.outputs.backend_changed }}\`" | |
| echo "- Web changed: \`${{ steps.detect.outputs.web_changed }}\`" | |
| echo "- Admin changed: \`${{ steps.detect.outputs.admin_changed }}\`" | |
| echo "- Infra changed: \`${{ steps.detect.outputs.infra_changed }}\`" | |
| echo "" | |
| echo "### Changed files" | |
| echo "" | |
| if [[ -n "${CHANGED_FILES}" ]]; then | |
| printf '%s\n' "${CHANGED_FILES}" | |
| else | |
| echo "No changed files detected." | |
| fi | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| pre_deploy_checks: | |
| name: Pre-deploy checks | |
| needs: changes | |
| if: ${{ needs.changes.outputs.aws_changed == 'true' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 | |
| services: | |
| postgres: | |
| image: postgres:18 | |
| env: | |
| POSTGRES_DB: postgres | |
| POSTGRES_PASSWORD: postgres-integration-${{ github.run_id }}-${{ github.run_attempt }} | |
| POSTGRES_USER: postgres_integration | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U postgres_integration -d postgres" | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: | | |
| apps/auth/package-lock.json | |
| apps/backend/package-lock.json | |
| apps/web/package-lock.json | |
| apps/admin/package-lock.json | |
| infra/aws/package-lock.json | |
| - name: Install dependencies | |
| run: | | |
| npm ci --prefix apps/auth | |
| npm ci --prefix apps/backend | |
| npm ci --prefix apps/web | |
| npm ci --prefix apps/admin | |
| npm ci --prefix infra/aws | |
| - name: Build auth app | |
| if: ${{ needs.changes.outputs.auth_changed == 'true' }} | |
| run: npm run build --prefix apps/auth | |
| - name: Test auth app | |
| if: ${{ needs.changes.outputs.auth_changed == 'true' }} | |
| run: npm run test --prefix apps/auth | |
| - name: Lint backend | |
| if: ${{ needs.changes.outputs.backend_changed == 'true' }} | |
| run: npm run lint --prefix apps/backend | |
| - name: Build backend | |
| if: ${{ needs.changes.outputs.backend_changed == 'true' }} | |
| run: npm run build --prefix apps/backend | |
| - name: Test backend MCP protocol | |
| if: ${{ needs.changes.outputs.backend_changed == 'true' }} | |
| run: npm run test:mcp --prefix apps/backend | |
| - name: Test PostgreSQL 18 lifecycle and 0096 to 0097 security boundaries | |
| env: | |
| POSTGRES_INTEGRATION_ADMIN_URL: postgresql://postgres_integration:postgres-integration-${{ github.run_id }}-${{ github.run_attempt }}@127.0.0.1:5432/postgres?sslmode=disable | |
| run: npm run test:postgres-integration --prefix apps/backend | |
| - name: Build web app | |
| if: ${{ needs.changes.outputs.web_changed == 'true' }} | |
| run: npm run build --prefix apps/web | |
| - name: Build admin app | |
| if: ${{ needs.changes.outputs.admin_changed == 'true' }} | |
| run: npm run build --prefix apps/admin | |
| - name: Build infra | |
| if: ${{ needs.changes.outputs.infra_changed == 'true' }} | |
| run: npm run build --prefix infra/aws | |
| - name: Test infra | |
| if: ${{ needs.changes.outputs.infra_changed == 'true' }} | |
| run: npm run test --prefix infra/aws | |
| - name: Set up Docker QEMU for CDK bundling | |
| uses: docker/setup-qemu-action@v4 | |
| with: | |
| platforms: arm64 | |
| - name: Synthesize infra | |
| working-directory: infra/aws | |
| env: | |
| AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} | |
| CDK_CONTEXT_ALERT_EMAIL: ${{ vars.CDK_ALERT_EMAIL }} | |
| CDK_CONTEXT_ANALYTICS_ACCESS_ENABLED: ${{ vars.CDK_ANALYTICS_ACCESS_ENABLED }} | |
| CDK_CONTEXT_ANTHROPIC_API_KEY_SECRET_ARN: ${{ vars.CDK_ANTHROPIC_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_API_CERTIFICATE_ARN: ${{ vars.CDK_API_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1 }} | |
| CDK_CONTEXT_AUTH_CERTIFICATE_ARN: ${{ vars.CDK_AUTH_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_MCP_CERTIFICATE_ARN: ${{ vars.CDK_MCP_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_ADMIN_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_ADMIN_CERTIFICATE_ARN_US_EAST_1 }} | |
| CDK_CONTEXT_ADMIN_EMAILS: ${{ vars.CDK_ADMIN_EMAILS }} | |
| CDK_CONTEXT_DEMO_EMAIL_DOSTIP: ${{ vars.CDK_DEMO_EMAIL_DOSTIP }} | |
| CDK_CONTEXT_DEMO_PASSWORD_SECRET_ARN: ${{ vars.CDK_DEMO_PASSWORD_SECRET_ARN }} | |
| CDK_CONTEXT_DOMAIN_NAME: ${{ vars.CDK_DOMAIN_NAME }} | |
| CDK_CONTEXT_GITHUB_REPO: ${{ vars.CDK_GITHUB_REPO }} | |
| CDK_CONTEXT_GLOBAL_METRICS_VISIBLE: ${{ vars.CDK_GLOBAL_METRICS_VISIBLE }} | |
| CDK_CONTEXT_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP: ${{ vars.CDK_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP }} | |
| CDK_CONTEXT_LANGFUSE_BASE_URL: ${{ vars.CDK_LANGFUSE_BASE_URL }} | |
| CDK_CONTEXT_LANGFUSE_PUBLIC_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_PUBLIC_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_LANGFUSE_SECRET_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_SECRET_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_OPENAI_API_KEY_SECRET_ARN: ${{ vars.CDK_OPENAI_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_REGION: ${{ vars.AWS_REGION }} | |
| CDK_CONTEXT_RESEND_API_KEY_SECRET_ARN: ${{ vars.CDK_RESEND_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_RESEND_SENDER_EMAIL: ${{ vars.CDK_RESEND_SENDER_EMAIL }} | |
| CDK_CONTEXT_SENTRY_DSN_SECRET_ARN: ${{ vars.CDK_SENTRY_DSN_SECRET_ARN }} | |
| CDK_CONTEXT_SENTRY_ENVIRONMENT: ${{ vars.CDK_SENTRY_ENVIRONMENT }} | |
| CDK_CONTEXT_SENTRY_RELEASE: ${{ github.sha }} | |
| CDK_CONTEXT_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.CDK_SENTRY_TRACES_SAMPLE_RATE }} | |
| CDK_CONTEXT_SITE_BASE_URL: ${{ vars.CDK_SITE_BASE_URL }} | |
| CDK_CONTEXT_WEB_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_WEB_CERTIFICATE_ARN_US_EAST_1 }} | |
| run: | | |
| python3 ../../scripts/generate/write-ci-cdk-context.py \ | |
| --output cdk.context.local.json \ | |
| --aws-deploy-role-arn "${AWS_DEPLOY_ROLE_ARN}" | |
| npm run synth | |
| aws_deploy: | |
| name: Deploy AWS release | |
| needs: | |
| - changes | |
| - pre_deploy_checks | |
| if: ${{ needs.changes.outputs.aws_changed == 'true' && needs.pre_deploy_checks.result == 'success' }} | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-node@v7 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: | | |
| apps/auth/package-lock.json | |
| apps/backend/package-lock.json | |
| apps/web/package-lock.json | |
| apps/admin/package-lock.json | |
| infra/aws/package-lock.json | |
| - name: Install dependencies | |
| run: | | |
| npm ci --prefix apps/auth | |
| npm ci --prefix apps/backend | |
| npm ci --prefix apps/web | |
| npm ci --prefix apps/admin | |
| npm ci --prefix infra/aws | |
| - name: Build web app | |
| env: | |
| VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }} | |
| VITE_SENTRY_ENVIRONMENT: production | |
| VITE_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.VITE_SENTRY_TRACES_SAMPLE_RATE }} | |
| VITE_APP_BUILD: ${{ github.sha }} | |
| SENTRY_UPLOAD_SOURCEMAPS: ${{ vars.VITE_SENTRY_DSN != '' && 'true' || 'false' }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ vars.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ vars.SENTRY_WEB_PROJECT }} | |
| run: npm run build --prefix apps/web | |
| - name: Build admin app | |
| run: npm run build --prefix apps/admin | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v6.3.0 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: CDK deploy with migration-gated runtime and reconciliation schedule disabled | |
| working-directory: infra/aws | |
| env: | |
| AWS_DEPLOY_ROLE_ARN: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} | |
| CDK_CONTEXT_ALERT_EMAIL: ${{ vars.CDK_ALERT_EMAIL }} | |
| CDK_CONTEXT_ANALYTICS_ACCESS_ENABLED: ${{ vars.CDK_ANALYTICS_ACCESS_ENABLED }} | |
| CDK_CONTEXT_ANTHROPIC_API_KEY_SECRET_ARN: ${{ vars.CDK_ANTHROPIC_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_API_CERTIFICATE_ARN: ${{ vars.CDK_API_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_APEX_REDIRECT_CERTIFICATE_ARN_US_EAST_1 }} | |
| CDK_CONTEXT_AUTH_CERTIFICATE_ARN: ${{ vars.CDK_AUTH_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_MCP_CERTIFICATE_ARN: ${{ vars.CDK_MCP_CERTIFICATE_ARN }} | |
| CDK_CONTEXT_ADMIN_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_ADMIN_CERTIFICATE_ARN_US_EAST_1 }} | |
| CDK_CONTEXT_ADMIN_EMAILS: ${{ vars.CDK_ADMIN_EMAILS }} | |
| CDK_CONTEXT_DEMO_EMAIL_DOSTIP: ${{ vars.CDK_DEMO_EMAIL_DOSTIP }} | |
| CDK_CONTEXT_DEMO_PASSWORD_SECRET_ARN: ${{ vars.CDK_DEMO_PASSWORD_SECRET_ARN }} | |
| CDK_CONTEXT_DOMAIN_NAME: ${{ vars.CDK_DOMAIN_NAME }} | |
| CDK_CONTEXT_GITHUB_REPO: ${{ vars.CDK_GITHUB_REPO }} | |
| CDK_CONTEXT_GLOBAL_METRICS_VISIBLE: ${{ vars.CDK_GLOBAL_METRICS_VISIBLE }} | |
| CDK_CONTEXT_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP: ${{ vars.CDK_GUEST_AI_WEIGHTED_MONTHLY_TOKEN_CAP }} | |
| CDK_CONTEXT_LANGFUSE_BASE_URL: ${{ vars.CDK_LANGFUSE_BASE_URL }} | |
| CDK_CONTEXT_LANGFUSE_PUBLIC_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_PUBLIC_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_LANGFUSE_SECRET_KEY_SECRET_ARN: ${{ vars.CDK_LANGFUSE_SECRET_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_OPENAI_API_KEY_SECRET_ARN: ${{ vars.CDK_OPENAI_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_REGION: ${{ vars.AWS_REGION }} | |
| CDK_CONTEXT_RESEND_API_KEY_SECRET_ARN: ${{ vars.CDK_RESEND_API_KEY_SECRET_ARN }} | |
| CDK_CONTEXT_RESEND_SENDER_EMAIL: ${{ vars.CDK_RESEND_SENDER_EMAIL }} | |
| CDK_CONTEXT_SENTRY_DSN_SECRET_ARN: ${{ vars.CDK_SENTRY_DSN_SECRET_ARN }} | |
| CDK_CONTEXT_SENTRY_ENVIRONMENT: ${{ vars.CDK_SENTRY_ENVIRONMENT }} | |
| CDK_CONTEXT_SENTRY_RELEASE: ${{ github.sha }} | |
| CDK_CONTEXT_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.CDK_SENTRY_TRACES_SAMPLE_RATE }} | |
| CDK_CONTEXT_SITE_BASE_URL: ${{ vars.CDK_SITE_BASE_URL }} | |
| SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} | |
| SENTRY_ORG: ${{ vars.SENTRY_ORG }} | |
| SENTRY_PROJECT: ${{ vars.SENTRY_BACKEND_PROJECT }} | |
| SENTRY_RELEASE: ${{ github.sha }} | |
| SENTRY_UPLOAD_BACKEND_SOURCEMAPS: true | |
| CDK_CONTEXT_WEB_CERTIFICATE_ARN_US_EAST_1: ${{ vars.CDK_WEB_CERTIFICATE_ARN_US_EAST_1 }} | |
| run: | | |
| python3 ../../scripts/generate/write-ci-cdk-context.py \ | |
| --output cdk.context.local.json \ | |
| --aws-deploy-role-arn "${AWS_DEPLOY_ROLE_ARN}" | |
| npx cdk deploy --all --require-approval never \ | |
| -c generatedMediaPromotionScheduleState=DISABLED \ | |
| -c mediaBlobCleanupEnabled=false \ | |
| -c multipartCompletionReconciliationScheduleState=DISABLED | |
| - name: Verify required database migration | |
| run: | | |
| bash scripts/deploy/migrate-aws.sh \ | |
| --stack-name ${{ env.STACK_NAME }} \ | |
| --require-latest-migration | |
| - name: CDK deploy with reconciliation schedule enabled | |
| working-directory: infra/aws | |
| env: | |
| SENTRY_UPLOAD_BACKEND_SOURCEMAPS: false | |
| run: | | |
| npx cdk deploy --all --require-approval never \ | |
| -c generatedMediaPromotionScheduleState=ENABLED \ | |
| -c mediaBlobCleanupEnabled=true \ | |
| -c multipartCompletionReconciliationScheduleState=ENABLED | |
| - name: Verify cleanup-capable reconciliation schedules are enabled | |
| run: | | |
| bash scripts/checks/check-multipart-completion-reconciliation-schedule.sh \ | |
| --stack-name ${{ env.STACK_NAME }} \ | |
| --region ${{ env.AWS_REGION }} | |
| - name: Check demo Cognito users | |
| run: bash scripts/checks/check-demo-cognito-users.sh --stack-name ${{ env.STACK_NAME }} --region ${{ env.AWS_REGION }} | |
| - name: Seed global metrics snapshot | |
| run: bash scripts/generate/generate-global-metrics-snapshot.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Seed public catalog dump | |
| run: bash scripts/generate/generate-catalog-dump.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Deploy web assets | |
| run: bash scripts/deploy/deploy-web.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Deploy admin assets | |
| run: bash scripts/deploy/deploy-admin.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Check API health | |
| run: bash scripts/checks/check-api-health.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Check public custom domains | |
| run: bash scripts/checks/check-public-endpoints.sh --stack-name ${{ env.STACK_NAME }} | |
| - name: Summarize deployed release | |
| run: | | |
| { | |
| echo "## AWS deploy" | |
| echo "" | |
| echo "- Deployed SHA: \`${GITHUB_SHA}\`" | |
| echo "- Web post-deploy smoke: pending next job" | |
| echo "- Agent API post-deploy smoke: pending next job" | |
| echo "- MCP post-deploy smoke: pending next job" | |
| echo "" | |
| echo "Manual admin smoke remains required for \`https://admin.<domain>\`." | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| # Scope note: this job never loads the deployed web assets. It builds `dist` | |
| # from the merge commit, points `app.flashcards-open-source-app.com` at | |
| # 127.0.0.1 through `/etc/hosts`, and serves that fresh build over a | |
| # self-signed certificate. Only `auth.` and `api.` reach real deployed | |
| # infrastructure, so a green run means the merge commit's web client works | |
| # against the deployed backend, not that the hosted web deployment is healthy. | |
| web_smoke: | |
| name: Web post-deploy smoke | |
| needs: | |
| - changes | |
| - aws_deploy | |
| if: ${{ needs.changes.outputs.aws_changed == 'true' }} | |
| runs-on: ubuntu-24.04 | |
| container: | |
| image: mcr.microsoft.com/playwright:v1.63.0-noble | |
| options: --init --ipc=host | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| timeout-minutes: 5 | |
| - uses: actions/setup-node@v7 | |
| timeout-minutes: 5 | |
| with: | |
| node-version: 24 | |
| cache: npm | |
| cache-dependency-path: apps/web/package-lock.json | |
| - name: Install web dependencies | |
| timeout-minutes: 5 | |
| run: npm ci --prefix apps/web | |
| - name: Build web app for live smoke | |
| timeout-minutes: 5 | |
| env: | |
| VITE_APP_BASE_URL: https://app.flashcards-open-source-app.com | |
| VITE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1 | |
| VITE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com | |
| run: npm run build --prefix apps/web | |
| - name: Run live web smoke test | |
| id: live_smoke | |
| working-directory: apps/web | |
| shell: bash | |
| env: | |
| FLASHCARDS_E2E_APP_BASE_URL: https://app.flashcards-open-source-app.com | |
| FLASHCARDS_E2E_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com | |
| FLASHCARDS_LIVE_REVIEW_EMAIL: google-review@example.com | |
| run: | | |
| set -euo pipefail | |
| cert_dir="${RUNNER_TEMP}/web-live-e2e-cert" | |
| cert_path="${cert_dir}/app.flashcards-open-source-app.com.crt" | |
| key_path="${cert_dir}/app.flashcards-open-source-app.com.key" | |
| server_log_path="${RUNNER_TEMP}/web-live-e2e-server.log" | |
| mkdir -p "${cert_dir}" | |
| openssl req \ | |
| -x509 \ | |
| -newkey rsa:2048 \ | |
| -sha256 \ | |
| -nodes \ | |
| -days 1 \ | |
| -subj "/CN=app.flashcards-open-source-app.com" \ | |
| -addext "subjectAltName=DNS:app.flashcards-open-source-app.com" \ | |
| -keyout "${key_path}" \ | |
| -out "${cert_path}" | |
| printf '127.0.0.1 app.flashcards-open-source-app.com\n' | tee -a /etc/hosts >/dev/null | |
| env PATH="${PATH}" node scripts/serve-dist-https.mjs \ | |
| --host 0.0.0.0 \ | |
| --port 443 \ | |
| --dir dist \ | |
| --cert "${cert_path}" \ | |
| --key "${key_path}" >"${server_log_path}" 2>&1 & | |
| server_pid=$! | |
| cleanup() { | |
| kill "${server_pid}" >/dev/null 2>&1 || true | |
| } | |
| trap cleanup EXIT | |
| for _ in $(seq 1 30); do | |
| if curl --silent --show-error --fail --insecure https://app.flashcards-open-source-app.com/ >/dev/null; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| curl --silent --show-error --fail --insecure https://app.flashcards-open-source-app.com/ >/dev/null | |
| npm run test:e2e | |
| - name: Upload Playwright report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: web-playwright-report | |
| if-no-files-found: ignore | |
| path: apps/web/playwright-report | |
| - name: Upload Playwright test results | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: web-playwright-test-results | |
| if-no-files-found: ignore | |
| path: apps/web/test-results | |
| # The smoke already records console, page-error and network evidence, but it | |
| # only reaches the run when it is collected into an artifact of its own. | |
| - name: Collect live smoke failure diagnostics | |
| if: failure() | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| diagnostics_dir="web-live-smoke-failure-diagnostics" | |
| server_log_path="${RUNNER_TEMP}/web-live-e2e-server.log" | |
| mkdir -p "${diagnostics_dir}" | |
| if [[ -f "${server_log_path}" ]]; then | |
| cp "${server_log_path}" "${diagnostics_dir}/static-server.log" | |
| echo "::group::Static web server log" | |
| cat "${server_log_path}" | |
| echo "::endgroup::" | |
| else | |
| echo "No static web server log at ${server_log_path}." | |
| fi | |
| if [[ -d apps/web/test-results ]]; then | |
| find apps/web/test-results -type f \ | |
| \( -name 'failure-diagnostics.json' -o -name 'failure-summary.txt' \) \ | |
| -exec cp --parents {} "${diagnostics_dir}/" \; | |
| fi | |
| find "${diagnostics_dir}" -type f | |
| - name: Upload live smoke failure diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: web-live-smoke-failure-diagnostics | |
| if-no-files-found: warn | |
| path: web-live-smoke-failure-diagnostics | |
| - name: Summarize smoke result | |
| if: always() | |
| run: | | |
| { | |
| echo "## Web post-deploy smoke" | |
| echo "" | |
| echo "- Deployed SHA: \`${GITHUB_SHA}\`" | |
| echo "- Result: \`${{ steps.live_smoke.outcome }}\`" | |
| echo "- Artifacts: \`web-playwright-report\`, \`web-playwright-test-results\`, \`web-live-smoke-failure-diagnostics\` (uploaded on failure)" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| agent_api_smoke: | |
| name: Agent API post-deploy smoke | |
| needs: | |
| - changes | |
| - aws_deploy | |
| if: ${{ needs.changes.outputs.aws_changed == 'true' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run agent API smoke test | |
| id: agent_smoke | |
| env: | |
| FLASHCARDS_AGENT_SMOKE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com | |
| FLASHCARDS_AGENT_SMOKE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1 | |
| FLASHCARDS_AGENT_SMOKE_DEMO_EMAIL: google-review@example.com | |
| FLASHCARDS_AGENT_SMOKE_WORKSPACE_PREFIX: "E2E agent api " | |
| FLASHCARDS_AGENT_SMOKE_CONNECTION_LABEL_PREFIX: "E2E agent api " | |
| run: bash scripts/checks/check-agent-api-smoke.sh | |
| - name: Summarize smoke result | |
| if: always() | |
| run: | | |
| { | |
| echo "## Agent API post-deploy smoke" | |
| echo "" | |
| echo "- Deployed SHA: \`${GITHUB_SHA}\`" | |
| echo "- Result: \`${{ steps.agent_smoke.outcome }}\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| mcp_smoke: | |
| name: MCP post-deploy smoke | |
| needs: | |
| - changes | |
| - aws_deploy | |
| if: ${{ needs.changes.outputs.aws_changed == 'true' }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run MCP smoke test | |
| id: mcp_smoke | |
| env: | |
| FLASHCARDS_MCP_SMOKE_AUTH_BASE_URL: https://auth.flashcards-open-source-app.com | |
| FLASHCARDS_MCP_SMOKE_API_BASE_URL: https://api.flashcards-open-source-app.com/v1 | |
| FLASHCARDS_MCP_SMOKE_MCP_BASE_URL: https://mcp.flashcards-open-source-app.com | |
| FLASHCARDS_MCP_SMOKE_DEMO_EMAIL: google-review@example.com | |
| FLASHCARDS_MCP_SMOKE_WORKSPACE_PREFIX: "E2E mcp " | |
| FLASHCARDS_MCP_SMOKE_CONNECTION_LABEL_PREFIX: "E2E mcp " | |
| run: bash scripts/checks/check-mcp-smoke.sh | |
| - name: Summarize smoke result | |
| if: always() | |
| run: | | |
| { | |
| echo "## MCP post-deploy smoke" | |
| echo "" | |
| echo "- Deployed SHA: \`${GITHUB_SHA}\`" | |
| echo "- Result: \`${{ steps.mcp_smoke.outcome }}\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| aws_release_status: | |
| name: Resolve AWS release outcome | |
| needs: | |
| - changes | |
| - pre_deploy_checks | |
| - aws_deploy | |
| - web_smoke | |
| - agent_api_smoke | |
| - mcp_smoke | |
| if: ${{ always() }} | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| aws_release_status: ${{ steps.resolve.outputs.aws_release_status }} | |
| steps: | |
| - name: Resolve AWS release status | |
| id: resolve | |
| run: | | |
| set -euo pipefail | |
| aws_release_status="no_aws_changes" | |
| if [[ "${{ needs.changes.outputs.aws_changed }}" == "true" ]]; then | |
| if [[ "${{ needs.pre_deploy_checks.result }}" != "success" ]]; then | |
| aws_release_status="pre_deploy_checks_failed" | |
| elif [[ "${{ needs.aws_deploy.result }}" != "success" ]]; then | |
| aws_release_status="deployment_failed" | |
| elif [[ "${{ needs.web_smoke.result }}" != "success" ]]; then | |
| aws_release_status="web_smoke_failed" | |
| elif [[ "${{ needs.agent_api_smoke.result }}" != "success" ]]; then | |
| aws_release_status="agent_api_smoke_failed" | |
| elif [[ "${{ needs.mcp_smoke.result }}" != "success" ]]; then | |
| aws_release_status="mcp_smoke_failed" | |
| else | |
| aws_release_status="released" | |
| fi | |
| fi | |
| echo "aws_release_status=${aws_release_status}" >> "${GITHUB_OUTPUT}" | |
| - name: Summarize AWS release outcome | |
| run: | | |
| { | |
| echo "## AWS release outcome" | |
| echo "" | |
| echo "- AWS release status: \`${{ steps.resolve.outputs.aws_release_status }}\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| release_summary: | |
| name: AWS/Web release summary | |
| if: ${{ always() }} | |
| needs: | |
| - changes | |
| - pre_deploy_checks | |
| - aws_deploy | |
| - web_smoke | |
| - agent_api_smoke | |
| - mcp_smoke | |
| - aws_release_status | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Summarize release outcome | |
| run: | | |
| { | |
| echo "## AWS/Web release summary" | |
| echo "" | |
| echo "- AWS changed: \`${{ needs.changes.outputs.aws_changed }}\`" | |
| echo "- Pre-deploy checks job: \`${{ needs.pre_deploy_checks.result }}\`" | |
| echo "- AWS release status: \`${{ needs.aws_release_status.outputs.aws_release_status }}\`" | |
| echo "- AWS deploy job: \`${{ needs.aws_deploy.result }}\`" | |
| echo "- Web smoke job: \`${{ needs.web_smoke.result }}\`" | |
| echo "- Agent API smoke job: \`${{ needs.agent_api_smoke.result }}\`" | |
| echo "- MCP smoke job: \`${{ needs.mcp_smoke.result }}\`" | |
| echo "- Run details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| - name: Fail workflow when AWS/Web release is not healthy | |
| run: | | |
| set -euo pipefail | |
| aws_release_status="${{ needs.aws_release_status.outputs.aws_release_status }}" | |
| if [[ "${aws_release_status}" != "no_aws_changes" ]] && [[ "${aws_release_status}" != "released" ]]; then | |
| echo "AWS release finished in non-healthy state: ${aws_release_status}" >&2 | |
| exit 1 | |
| fi |