Skip to content

Merge pull request #1784 from kirill-markin/claude/agent-followups-p19 #1017

Merge pull request #1784 from kirill-markin/claude/agent-followups-p19

Merge pull request #1784 from kirill-markin/claude/agent-followups-p19 #1017

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