Skip to content

fix(ci): bake dummy payment publishable keys into E2E build #552

fix(ci): bake dummy payment publishable keys into E2E build

fix(ci): bake dummy payment publishable keys into E2E build #552

Workflow file for this run

name: E2E Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Monday 6 AM UTC — full 3-browser run
workflow_dispatch:
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ vars.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ vars.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
jobs:
# ============================================
# STEP 1: Build once, cache for all shards
# ============================================
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build application
run: pnpm build
env:
GITHUB_ACTIONS: false
NEXT_PUBLIC_EMAILJS_PUBLIC_KEY: ${{ secrets.EMAILJS_PUBLIC_KEY }}
NEXT_PUBLIC_EMAILJS_SERVICE_ID: ${{ vars.NEXT_PUBLIC_EMAILJS_SERVICE_ID }}
NEXT_PUBLIC_EMAILJS_TEMPLATE_ID: ${{ vars.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID }}
# Payment publishable keys must be baked into the static build so
# featureFlags.stripeEnabled / paypalEnabled are true and the
# PaymentButton / provider tabs render. Commit 77e409d added a
# "graceful missing-payment-config" branch that hides these controls
# when env vars are empty, which broke all payment E2E tests. Real
# keys aren't required: tests that redirect to Stripe/PayPal are
# test.skip'd; the rest only assert UI render + consent flow.
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: pk_test_e2e_dummy_not_a_real_key
NEXT_PUBLIC_PAYPAL_CLIENT_ID: e2e_dummy_paypal_client_id
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: out/
retention-days: 1
# ============================================
# STEP 2: Smoke tests (~2 min) - FAST FEEDBACK
# ============================================
smoke:
name: Smoke Tests
runs-on: ubuntu-latest
needs: build
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright (chromium only)
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps chromium && break
echo "Retry $i: playwright install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
- name: Run smoke tests (sign-up only - auth tests run after auth-setup)
run: |
pnpm exec playwright test \
tests/e2e/auth/sign-up.spec.ts \
--project=signup \
--no-deps \
--reporter=list \
--timeout=30000
env:
CI: true
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
- name: Smoke test summary
if: always()
run: echo "## Smoke Tests Complete" >> "$GITHUB_STEP_SUMMARY"
# ============================================
# STEP 3: Rate-limiting tests (ordered, clean IP)
# ============================================
rate-limiting:
name: Rate-Limiting Tests
runs-on: ubuntu-latest
needs: smoke
timeout-minutes: 10
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright (chromium only)
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps chromium && break
echo "Retry $i: playwright install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
- name: Run rate-limiting tests (ordered)
run: pnpm test:e2e --project=rate-limiting --project=brute-force --project=signup --reporter=list --trace=on-first-retry
env:
CI: true
PLAYWRIGHT_BROWSER: chromium
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
# ============================================
# STEP 4: Auth Setup (runs ONCE, shares state)
# ============================================
auth-setup:
name: Auth Setup
runs-on: ubuntu-latest
needs: smoke
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright (chromium only for auth)
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps chromium && break
echo "Retry $i: playwright install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
- name: Run auth setup
run: pnpm exec playwright test --project=setup --reporter=list --timeout=180000
env:
CI: true
AUTH_SETUP_JOB: true
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
- name: Upload auth state
uses: actions/upload-artifact@v4
with:
name: auth-state
path: tests/e2e/fixtures/storage-state-auth.json
retention-days: 1
# ============================================
# STEP 5: Full E2E tests (sharded, parallel)
# ============================================
e2e:
name: E2E (${{ matrix.project }} ${{ matrix.shard }})
runs-on: ubuntu-latest
needs: [smoke, rate-limiting, auth-setup]
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
# ===== CHROMIUM (8 shards) =====
- { project: chromium-msg, browser: chromium, shard: 1/2 }
- { project: chromium-msg, browser: chromium, shard: 2/2 }
- { project: chromium-gen, browser: chromium, shard: 1/6 }
- { project: chromium-gen, browser: chromium, shard: 2/6 }
- { project: chromium-gen, browser: chromium, shard: 3/6 }
- { project: chromium-gen, browser: chromium, shard: 4/6 }
- { project: chromium-gen, browser: chromium, shard: 5/6 }
- { project: chromium-gen, browser: chromium, shard: 6/6 }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
env:
BROWSER: ${{ matrix.browser }}
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps "$BROWSER" && break
echo "Retry $i: $BROWSER install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Download auth state
uses: actions/download-artifact@v4
with:
name: auth-state
path: tests/e2e/fixtures/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
env:
CI: true
- name: Prime Supabase connection pool
env:
SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
run: |
# Wake Supabase free tier and warm the connection pool.
# Without this, the first query from each shard takes 10-30s.
curl -sf "${SUPABASE_URL}/rest/v1/user_profiles?select=id&limit=1" \
-H "apikey: ${SUPABASE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_KEY}" \
> /dev/null 2>&1 || true
echo "✓ Supabase connection pool primed"
- name: Stagger shards within batch to reduce Supabase contention
env:
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
SHARD_NUM=$(echo "$SHARD" | cut -d/ -f1)
# max-parallel: 8 already serializes browsers. Within each batch
# of 8, stagger by 5-10 seconds to spread the initial connection
# burst on Supabase.
if echo "$PROJECT" | grep -q "msg"; then
DELAY=$(((SHARD_NUM - 1) * 10))
else
DELAY=$(((SHARD_NUM - 1) * 5))
fi
echo "$PROJECT shard $SHARD_NUM: waiting ${DELAY}s"
sleep $DELAY
- name: Run E2E tests (${{ matrix.project }} ${{ matrix.shard }})
env:
CI: true
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
DEBUG: pw:api
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
# --no-deps: skip the 'setup' project dependency. The auth-setup
# job already ran setup once (with chromium) and uploaded the
# auth-state artifact, which we downloaded above. Without --no-deps,
# Playwright tries to re-run setup with the matrix browser, which
# fails because firefox/webkit shards don't install chromium.
pnpm exec playwright test \
--project="$PROJECT" \
--shard="$SHARD" \
--reporter=blob \
--trace=on-first-retry \
--no-deps
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.project }}-${{ strategy.job-index }}
path: blob-report/
retention-days: 1
# ============================================
# STEP 5b: Firefox E2E (waits for chromium to release Supabase)
# ============================================
e2e-firefox:
name: E2E (${{ matrix.project }} ${{ matrix.shard }})
runs-on: ubuntu-latest
needs: e2e
if: always() && !cancelled()
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- { project: firefox-msg, browser: firefox, shard: 1/2 }
- { project: firefox-msg, browser: firefox, shard: 2/2 }
- { project: firefox-gen, browser: firefox, shard: 1/6 }
- { project: firefox-gen, browser: firefox, shard: 2/6 }
- { project: firefox-gen, browser: firefox, shard: 3/6 }
- { project: firefox-gen, browser: firefox, shard: 4/6 }
- { project: firefox-gen, browser: firefox, shard: 5/6 }
- { project: firefox-gen, browser: firefox, shard: 6/6 }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
env:
BROWSER: ${{ matrix.browser }}
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps "$BROWSER" && break
echo "Retry $i: $BROWSER install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Download auth state
uses: actions/download-artifact@v4
with:
name: auth-state
path: tests/e2e/fixtures/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
env:
CI: true
- name: Prime Supabase connection pool
env:
SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
run: |
curl -sf "${SUPABASE_URL}/rest/v1/user_profiles?select=id&limit=1" \
-H "apikey: ${SUPABASE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_KEY}" \
> /dev/null 2>&1 || true
- name: Stagger shards within batch
env:
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
SHARD_NUM=$(echo "$SHARD" | cut -d/ -f1)
if echo "$PROJECT" | grep -q "msg"; then
DELAY=$(((SHARD_NUM - 1) * 10))
else
DELAY=$(((SHARD_NUM - 1) * 5))
fi
sleep $DELAY
- name: Run E2E tests
env:
CI: true
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
DEBUG: pw:api
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
pnpm exec playwright test \
--project="$PROJECT" \
--shard="$SHARD" \
--reporter=blob \
--trace=on-first-retry \
--no-deps
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.project }}-${{ strategy.job-index }}
path: blob-report/
retention-days: 1
# ============================================
# STEP 5c: WebKit E2E (waits for firefox to release Supabase)
# ============================================
e2e-webkit:
name: E2E (${{ matrix.project }} ${{ matrix.shard }})
runs-on: ubuntu-latest
needs: e2e-firefox
if: always() && !cancelled()
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- { project: webkit-msg, browser: webkit, shard: 1/2 }
- { project: webkit-msg, browser: webkit, shard: 2/2 }
- { project: webkit-gen, browser: webkit, shard: 1/6 }
- { project: webkit-gen, browser: webkit, shard: 2/6 }
- { project: webkit-gen, browser: webkit, shard: 3/6 }
- { project: webkit-gen, browser: webkit, shard: 4/6 }
- { project: webkit-gen, browser: webkit, shard: 5/6 }
- { project: webkit-gen, browser: webkit, shard: 6/6 }
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
env:
BROWSER: ${{ matrix.browser }}
run: |
for i in 1 2 3; do
pnpm exec playwright install --with-deps "$BROWSER" && break
echo "Retry $i: $BROWSER install failed, retrying in 10s..."
sleep 10
done
- name: Download build
uses: actions/download-artifact@v4
with:
name: build-output
path: out/
- name: Download auth state
uses: actions/download-artifact@v4
with:
name: auth-state
path: tests/e2e/fixtures/
- name: Start server
run: |
npx serve out -l 3000 &
sleep 5
npx wait-on http://localhost:3000 --timeout 60000
env:
CI: true
- name: Prime Supabase connection pool
env:
SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
run: |
curl -sf "${SUPABASE_URL}/rest/v1/user_profiles?select=id&limit=1" \
-H "apikey: ${SUPABASE_KEY}" \
-H "Authorization: Bearer ${SUPABASE_KEY}" \
> /dev/null 2>&1 || true
- name: Stagger shards within batch
env:
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
SHARD_NUM=$(echo "$SHARD" | cut -d/ -f1)
if echo "$PROJECT" | grep -q "msg"; then
DELAY=$(((SHARD_NUM - 1) * 10))
else
DELAY=$(((SHARD_NUM - 1) * 5))
fi
sleep $DELAY
- name: Run E2E tests
env:
CI: true
SKIP_WEBSERVER: true
BASE_URL: http://localhost:3000
DEBUG: pw:api
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
TEST_USER_PRIMARY_EMAIL: ${{ vars.TEST_USER_PRIMARY_EMAIL }}
TEST_USER_PRIMARY_PASSWORD: ${{ secrets.TEST_USER_PRIMARY_PASSWORD }}
TEST_USER_SECONDARY_EMAIL: ${{ vars.TEST_USER_SECONDARY_EMAIL }}
TEST_USER_SECONDARY_PASSWORD: ${{ secrets.TEST_USER_SECONDARY_PASSWORD }}
TEST_USER_TERTIARY_EMAIL: ${{ vars.TEST_USER_TERTIARY_EMAIL }}
TEST_USER_TERTIARY_PASSWORD: ${{ secrets.TEST_USER_TERTIARY_PASSWORD }}
PROJECT: ${{ matrix.project }}
SHARD: ${{ matrix.shard }}
run: |
pnpm exec playwright test \
--project="$PROJECT" \
--shard="$SHARD" \
--reporter=blob \
--trace=on-first-retry \
--no-deps
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v4
with:
name: blob-report-${{ matrix.project }}-${{ strategy.job-index }}
path: blob-report/
retention-days: 1
# ============================================
# STEP 6: Merge reports and generate summary
# ============================================
report:
name: Test Report
runs-on: ubuntu-latest
needs: [e2e, e2e-firefox, e2e-webkit]
if: always()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.16.1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download blob reports
uses: actions/download-artifact@v4
with:
pattern: blob-report-*
path: all-blob-reports
merge-multiple: true
- name: Merge reports
run: pnpm exec playwright merge-reports --reporter=html,github ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Generate summary
if: always()
env:
EVENT_NAME: ${{ github.event_name }}
run: |
echo "## E2E Test Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "$EVENT_NAME" = "pull_request" ]; then
echo "**PR Mode**: Chromium only (4 shards)" >> "$GITHUB_STEP_SUMMARY"
else
echo "**Full Mode**: Chromium + Firefox + WebKit (12 shards total)" >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Download the **playwright-report** artifact for detailed results." >> "$GITHUB_STEP_SUMMARY"