From 427840a3741e974fc33bb13397fef32b8a403025 Mon Sep 17 00:00:00 2001 From: Thomas Luizon Rodrigues Gregorio Date: Mon, 22 Jun 2026 19:08:50 -0300 Subject: [PATCH 1/4] test(smoke): prod post-deploy Playwright smoke (5 flows) + rollback-on-red (#227) Add a sanctioned ~5-test web Playwright smoke suite that runs against PROD immediately after each deploy, with Vercel Instant Rollback as the safety net. - playwright.config.ts: BASE_URL from SMOKE_BASE_URL, retries=0 (fix-or-delete, no retry-to-green), single chromium worker, setup/cleanup projects + storageState. - 5 specs: signup/login (OTP UI), create habit, log habit, Astra-creates-habit, open-paywall. - Setup signs in once via the real passwordless OTP UI behind a single auth seam, resets the smoke account, and marks onboarding complete; teardown resets it again so prod data is never polluted (POST /api/profile/reset). - smoke-prod.yml: on push to main, wait for the Vercel prod deployment of the commit to be READY, run the suite against prod, and on red roll back to the previous prod deployment (vercel rollback) and surface the failure. - Minimal data-testids added to load-bearing web elements (locale-proof selectors). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/smoke-prod.yml | 124 ++++++++++++++++++ .gitignore | 7 + apps/web/app/(app)/upgrade/page.tsx | 1 + apps/web/app/(auth)/login/code-step.tsx | 1 + apps/web/app/(auth)/login/email-step.tsx | 1 + .../web/app/(chat)/chat/chat-composer-bar.tsx | 2 + .../chat/pending-operation-card.tsx | 2 + .../components/habits/create-habit-modal.tsx | 1 + apps/web/components/habits/habit-row.tsx | 3 + apps/web/components/ui/pill-button.tsx | 3 + apps/web/e2e/astra-create-habit.spec.ts | 27 ++++ apps/web/e2e/auth.spec.ts | 11 ++ apps/web/e2e/create-habit.spec.ts | 20 +++ apps/web/e2e/global.setup.ts | 15 +++ apps/web/e2e/global.teardown.ts | 6 + apps/web/e2e/log-habit.spec.ts | 25 ++++ apps/web/e2e/paywall.spec.ts | 8 ++ apps/web/e2e/support/api.ts | 25 ++++ apps/web/e2e/support/auth.ts | 26 ++++ apps/web/e2e/support/env.ts | 18 +++ apps/web/e2e/support/unique.ts | 7 + apps/web/package.json | 2 + apps/web/playwright.config.ts | 57 ++++++++ package-lock.json | 64 +++++++++ 24 files changed, 456 insertions(+) create mode 100644 .github/workflows/smoke-prod.yml create mode 100644 apps/web/e2e/astra-create-habit.spec.ts create mode 100644 apps/web/e2e/auth.spec.ts create mode 100644 apps/web/e2e/create-habit.spec.ts create mode 100644 apps/web/e2e/global.setup.ts create mode 100644 apps/web/e2e/global.teardown.ts create mode 100644 apps/web/e2e/log-habit.spec.ts create mode 100644 apps/web/e2e/paywall.spec.ts create mode 100644 apps/web/e2e/support/api.ts create mode 100644 apps/web/e2e/support/auth.ts create mode 100644 apps/web/e2e/support/env.ts create mode 100644 apps/web/e2e/support/unique.ts create mode 100644 apps/web/playwright.config.ts diff --git a/.github/workflows/smoke-prod.yml b/.github/workflows/smoke-prod.yml new file mode 100644 index 000000000..107ec01d4 --- /dev/null +++ b/.github/workflows/smoke-prod.yml @@ -0,0 +1,124 @@ +name: Prod Smoke + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + base_url: + description: Override the prod origin to smoke (defaults to SMOKE_BASE_URL secret) + required: false + type: string + +concurrency: + group: prod-smoke + cancel-in-progress: false + +jobs: + smoke: + name: Post-deploy smoke + rollback on red + runs-on: ubuntu-latest + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} + SMOKE_BASE_URL: ${{ inputs.base_url || secrets.SMOKE_BASE_URL }} + SMOKE_TEST_EMAIL: ${{ secrets.SMOKE_TEST_EMAIL }} + SMOKE_TEST_CODE: ${{ secrets.SMOKE_TEST_CODE }} + + steps: + - uses: actions/checkout@v5 + + - name: Validate smoke secrets + run: | + set -euo pipefail + missing=() + for name in VERCEL_TOKEN VERCEL_ORG_ID VERCEL_PROJECT_ID SMOKE_BASE_URL SMOKE_TEST_EMAIL SMOKE_TEST_CODE; do + if [ -z "${!name:-}" ]; then missing+=("$name"); fi + done + if [ ${#missing[@]} -gt 0 ]; then + echo "::error::Missing required secrets: ${missing[*]}" >&2 + exit 1 + fi + + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - name: Install Playwright chromium + run: npx playwright install --with-deps chromium + + - name: Install Vercel CLI + run: npm i -g vercel@54 + + - name: Wait for Vercel production deployment of this commit + run: | + set -euo pipefail + deadline=$(( $(date +%s) + 900 )) + api="https://api.vercel.com/v6/deployments?projectId=${VERCEL_PROJECT_ID}&teamId=${VERCEL_ORG_ID}&target=production&sha=${GITHUB_SHA}&limit=1" + while :; do + body=$(curl -sS -H "Authorization: Bearer ${VERCEL_TOKEN}" "$api") + state=$(echo "$body" | jq -r '.deployments[0].readyState // .deployments[0].state // "NONE"') + url=$(echo "$body" | jq -r '.deployments[0].url // empty') + echo "deployment state for ${GITHUB_SHA}: ${state} (${url:-no-url})" + case "$state" in + READY) + break + ;; + ERROR|CANCELED|DELETED|BLOCKED) + echo "::error::Production deployment for ${GITHUB_SHA} ended in state ${state}; nothing healthy to smoke." >&2 + exit 1 + ;; + esac + if [ "$(date +%s)" -ge "$deadline" ]; then + echo "::error::Timed out waiting for the production deployment of ${GITHUB_SHA} to become READY." >&2 + exit 1 + fi + sleep 15 + done + + - name: Capture previous production deployment for rollback + id: prev_deploy + run: | + set -euo pipefail + api="https://api.vercel.com/v6/deployments?projectId=${VERCEL_PROJECT_ID}&teamId=${VERCEL_ORG_ID}&target=production&state=READY&limit=20" + body=$(curl -sS -H "Authorization: Bearer ${VERCEL_TOKEN}" "$api") + prev=$(echo "$body" | jq -r --arg sha "$GITHUB_SHA" ' + [.deployments[] | select((.meta.githubCommitSha // "") != $sha)] + | sort_by(-.created) | .[0].url // empty') + if [ -z "$prev" ]; then + echo "::warning::No previous production deployment found; rollback will be skipped if smoke fails." + else + echo "previous deployment to roll back to: https://${prev}" + fi + echo "previous_url=${prev:+https://$prev}" >> "$GITHUB_OUTPUT" + + - name: Run prod smoke suite + id: smoke + run: npm --workspace @orbit/web run test:smoke + + - name: Roll back the just-shipped deployment (smoke failed) + if: failure() && steps.smoke.outcome == 'failure' + env: + PREVIOUS_URL: ${{ steps.prev_deploy.outputs.previous_url }} + run: | + set -euo pipefail + if [ -z "${PREVIOUS_URL:-}" ]; then + echo "::error::Smoke failed but no previous production deployment was captured — manual rollback required at https://vercel.com." >&2 + exit 1 + fi + echo "::warning::Smoke failed against prod; rolling back to ${PREVIOUS_URL}." + vercel rollback "${PREVIOUS_URL}" --non-interactive --timeout 120s + echo "::error::Prod smoke failed; rolled back to ${PREVIOUS_URL}. Production auto-deploys are now PAUSED until someone promotes a healthy deployment (vercel promote). Fix forward, then promote." >&2 + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: apps/web/e2e/.report + if-no-files-found: ignore + retention-days: 14 diff --git a/.gitignore b/.gitignore index ba89a14fc..401f87aab 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,13 @@ Thumbs.db # Coverage coverage/ +# Playwright smoke (apps/web/e2e) +apps/web/e2e/.auth/ +apps/web/e2e/.report/ +apps/web/e2e/.results/ +/test-results/ +.cache/ms-playwright/ + # Temporary files .tmp/ temporary screenshots/ diff --git a/apps/web/app/(app)/upgrade/page.tsx b/apps/web/app/(app)/upgrade/page.tsx index d6d3763a6..cb6a89e64 100644 --- a/apps/web/app/(app)/upgrade/page.tsx +++ b/apps/web/app/(app)/upgrade/page.tsx @@ -721,6 +721,7 @@ function PricingSection({ onCheckout(selectedInterval)} leading={checkoutLoading ? diff --git a/apps/web/app/(auth)/login/email-step.tsx b/apps/web/app/(auth)/login/email-step.tsx index 9fa3e0c1f..7703d5122 100644 --- a/apps/web/app/(auth)/login/email-step.tsx +++ b/apps/web/app/(auth)/login/email-step.tsx @@ -53,6 +53,7 @@ export function EmailStep({ disabled={isSubmitting || !email.trim()} busy={isSubmitting} leading={isSubmitting ? : undefined} + dataTestId="auth-send-code" > {t('auth.sendCode')} diff --git a/apps/web/app/(chat)/chat/chat-composer-bar.tsx b/apps/web/app/(chat)/chat/chat-composer-bar.tsx index 01216e8ea..b7067eece 100644 --- a/apps/web/app/(chat)/chat/chat-composer-bar.tsx +++ b/apps/web/app/(chat)/chat/chat-composer-bar.tsx @@ -270,6 +270,7 @@ function ChatTextInputRow({