Skip to content

test(smoke): prod post-deploy Playwright smoke (5 flows) + rollback-on-red (#227) - #253

Merged
thomasluizon merged 5 commits into
mainfrom
feature/phase2-prod-smoke
Jun 24, 2026
Merged

test(smoke): prod post-deploy Playwright smoke (5 flows) + rollback-on-red (#227)#253
thomasluizon merged 5 commits into
mainfrom
feature/phase2-prod-smoke

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Closes #227

Sanctioned ~5-test web Playwright smoke suite that runs against prod immediately after each deploy, with Vercel Instant Rollback as the safety net (red → roll back the just-shipped prod deployment). Per the re-scoped issue (QA env #211 aborted): no QA→prod promotion gate; the gate is post-deploy + rollback.

Paired backend PR (cross-repo #227): thomasluizon/orbit-api#217 — the prod-gated smoke-auth bypass that lets the suite authenticate. Both must merge, and the prod secrets must be set, before the suite can pass against prod.

The 5 smoke flows (apps/web/e2e/*.spec.ts)

  1. Signup/login — drives the real passwordless OTP UI (email → 6-digit code → session) and asserts it reaches the app shell. (auth.spec.ts)
  2. Create a habit — Today FAB → create modal → title → submit → row appears. (create-habit.spec.ts)
  3. Log a habit — create, then toggle its status and assert it flips to done. (log-habit.spec.ts)
  4. Astra creates a habit — send a chat message, confirm the pending-operation card if shown, then assert the habit exists server-side via the BFF. (astra-create-habit.spec.ts)
  5. Open paywall/upgrade renders the plan selector + checkout CTA. (paywall.spec.ts)

Post-deploy workflow (.github/workflows/smoke-prod.yml) — NEW file

On push to main (web deploys via Vercel Git integration on the same push):

  1. Poll the Vercel REST API for the production deployment of $GITHUB_SHA until READY (fails fast on ERROR/CANCELED).
  2. Run the smoke suite against SMOKE_BASE_URL (the live prod alias).
  3. On red: vercel rollback <previous-prod-deployment> (verified flags: --non-interactive --timeout), then surface the failure. Captures the previous prod deployment up-front so the rollback target is deterministic.
  4. Uploads the Playwright HTML report as an artifact.

Heads-up documented in the workflow: after a Vercel rollback, auto-assignment of production domains is paused — prod is frozen at the last-good deploy until someone vercel promotes a healthy one. That's the intended fail-safe; fix forward, then promote.

No prod pollution

  • Every habit the suite creates is uniquely named per run (timestamp + nonce), so overlapping runs never collide.
  • A dedicated, disposable smoke account is used. Global setup resets it (POST /api/profile/reset wipes all user data) and marks onboarding complete; global teardown resets it again. Net: the account returns to a clean slate every run, reused indefinitely, with zero residue in real prod data.

CI secrets an operator must add

Secret Purpose
SMOKE_BASE_URL Prod web origin to smoke (e.g. https://app.useorbit.org).
SMOKE_TEST_EMAIL The dedicated disposable smoke account email (must match the backend's SMOKE_TEST_EMAIL).
SMOKE_TEST_CODE The fixed OTP code the backend bypass accepts (must match the backend's SMOKE_TEST_CODE).
VERCEL_TOKEN Vercel auth for deploy-wait + rollback.
VERCEL_ORG_ID Vercel team id (teamId).
VERCEL_PROJECT_ID Vercel web project id.

Auth (resolved → Option 1, paired PR orbit-api#217)

Prod login is passwordless email OTP with no prod test bypass on main. The suite's auth lives behind a single seam (e2e/support/auth.ts) and uses the real verify-code UI with a fixed code accepted only for one pinned prod email behind a high-entropy SMOKE_TEST_CODE secret — wired by the paired backend PR. The suite will not pass against prod until orbit-api#217 merges and SMOKE_TEST_EMAIL/SMOKE_TEST_CODE are set in both prod (API) and CI.

Policy compliance (CLAUDE.md “Testing”)

  • This is the only sanctioned E2E. Kept to 5 specs.
  • retries: 0 — fix-or-delete on flake, never retry-to-green. forbidOnly in CI so a stray .only can't silently shrink the suite.
  • Minimal data-testids added to load-bearing web elements (locale-proof selectors that survive copy/i18n edits) — an allowed app-code touch.

Validation

  • npx playwright test --list → all 7 tests (5 smoke + setup + cleanup) discoverable.
  • tsc --noEmit (apps/web) → clean.
  • eslint on all new/changed files → 0 errors (specs honor local/no-comments; config file is eslint-ignored by project config).
  • Suite was not run against prod from here (no prod creds; must not hit prod) — that's by design.

🤖 Generated with Claude Code

…n-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) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jun 24, 2026 1:00am

Request Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR 253 Code Review: APPROVE. No Critical/High findings. 3 Medium findings: (1) smokeEnv.baseURL in apps/web/e2e/support/env.ts:13 is exported but never used - playwright.config.ts reads SMOKE_BASE_URL directly; remove it. (2) .github/workflows/smoke-prod.yml has no paths: filter, so mobile-only pushes trigger unnecessary smoke runs that could cause spurious rollbacks; add paths filter for apps/web/** and packages/shared/**. (3) STORAGE_STATE_PATH in apps/web/e2e/support/env.ts:18 is exported but hardcoded again in playwright.config.ts:43,53 - import and reuse it. None of these block merge. Approved.

GET /api/habits returns PaginatedResponse<T> ({ items, page, pageSize,
totalCount, totalPages }) — the smoke helper read body.habits (always
undefined), so listHabitTitles() always returned [] and the
Astra-create-habit poll could never satisfy .toContain(title), timing out
at 60s every run. Matches the app's own consumer (use-habit-queries.ts
reads firstPage.items).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@thomasluizon thomasluizon left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #253 (orbit-ui-mobile)

Scope: PR #253 — prod post-deploy Playwright smoke (5 flows) + rollback-on-red (#227)
Recommendation: APPROVE (one High found during review, fixed in 24572a6)

Summary

Web-only Playwright smoke suite (the five sanctioned flows: auth, create habit, log
habit, Astra-creates-habit, paywall) + smoke-prod.yml with rollback-on-red, plus
zero-logic data-testid/id hooks added to app components for targeting. Walked the
full rubric; every gated subagent is N/A (web-only E2E; no shared types, DTOs, i18n, or
design-token changes). One High-severity correctness defect was found and fixed; one
automated selector finding was a false positive and is documented below so it is not
re-raised.

Findings

Critical

None.

High

[HIGH] listHabitTitles read the wrong response field → Astra smoke can never pass — FIXED in 24572a6
· dimension: Correctness (#1)
· location: apps/web/e2e/support/api.ts:23-24
· issue: GET /api/habits returns PaginatedResponse<T> ({ items, page, pageSize,
  totalCount, totalPages }); the helper read body.habits, which is always undefined.
  listHabitTitles() therefore always returned [].
· risk: The Astra-creates-habit poll (expect.poll(...).toContain(title), 60s) can never
  become true → that flow fails on every run → one of five gating flows is permanently
  red, wedging the prod-promotion gate (or inviting delete-to-green, against the
  fix-or-delete-same-day rule).
· fix: Read body.items. Verified against the app's own consumer
  (apps/web/hooks/use-habit-queries.ts:53,56 reads firstPage.items) and the API DTO
  (Orbit.Application/Common/PaginatedResponse.cs). Applied in 24572a6.
· reference: CLAUDE.md "API contract — never invent fields the API doesn't return"; rubric #1.

Medium

None.

Low / Info

[INFO] Automated selector sweep false positive — no action
· An in-review selector-existence agent reported [data-testid="paywall-checkout"] as
  MISSING and "dataTestId not a PillButton prop." False positive: the agent read the
  main checkout (which lacks this PR's changes), not the PR branch. On the branch,
  pill-button.tsx defines dataTestId?: string (:18), destructures it (:42), and renders
  data-testid={dataTestId} (:55); upgrade/page.tsx:724 passes
  dataTestId="paywall-checkout"; paywall.spec.ts:7 targets it. Selector exists.
· All other selectors verified present on the branch: data-habit-title,
  habit-status-toggle, chat-input/chat-send, tour-fab-button, #habit-form-title,
  #login-email, data-code-index, data-bottom-nav, habit-create-submit,
  auth-send-code/verify-code, pending-op-confirm.

Subagents

Agent Verdict
parity-checker N/A (web-only E2E + zero-logic data-testid props; mobile has no E2E)
i18n-syncer N/A (no user-facing strings / locale changes)
contract-aligner N/A (no shared types / endpoints changed)
security-reviewer N/A (no orbit-api code)

Validation

Check Result
Lint N/A (e2e config eslint-ignored per PR; fix is a type-safe field rename)
Type check PASS (cast property rename; `body.items: HabitListItem[]
Tests N/A (smoke runs post-deploy against prod, not in unit CI)
Build (api) N/A

What's good

  • Suite is exactly the five sanctioned flows; retries: 0, forbidOnly on CI, ≤5 specs — compliant with the Testing policy.
  • DOM-assert flows (create/log habit) target stable data-* hooks, not classes.
  • Selector hooks are zero-logic data-testid/id additions — no behavior change, correctly not requiring a mobile mirror.

Recommendation

APPROVE. The one High (wrong habits field) is fixed in 24572a6; a re-run is not
required — the fix is a verified field rename matching the live API shape. Backward-compat
guard N/A. Merge note: #227 auto-closes via this PR; ensure the paired orbit-api#217
(smoke-auth bypass) merges so the prod smoke account can authenticate.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #253

Scope: PR #253test(smoke): prod post-deploy Playwright smoke (5 flows) + rollback-on-red
Recommendation: APPROVE

Summary

This PR ships the sanctioned post-deploy Playwright smoke suite (5 flows) against prod, wired to Vercel Instant Rollback on failure. The implementation is well-structured: a clean support/ seam for auth, API helpers, and env config; correct workers: 1 + retries: 0 per policy; forbidOnly in CI; and a thorough CI workflow with secret validation, deploy-wait polling, rollback capture, and artifact upload. No logic changes to production code — all app touches are additive data-testid attributes used only by the test harness.

One dead-export finding (smokeEnv.baseURL) is flagged at Medium — it never causes a test failure but violates rule 2 and may mislead future authors. Everything else is clean.

Findings

Critical

None

High

None

Medium

[MEDIUM] smokeEnv.baseURL is exported but never consumed
· dimension: Dead / stale code (#2)
· location: orbit-ui-mobile/apps/web/e2e/support/env.ts:13
· issue: smokeEnv.baseURL = required('SMOKE_BASE_URL') is validated and exported, but no test
  file or support module imports or uses smokeEnv.baseURL. The playwright.config.ts already
  reads process.env.SMOKE_BASE_URL directly and throws if it is missing (line 4-8), so the
  required() call in env.ts is a duplicate guard with no consumer.
· risk: Dead code — ships a speculative export that signals "the base URL is in smokeEnv" but
  callers never use it. A future author might use smokeEnv.baseURL and bypass the
  playwright.config.ts baseURL path, creating split-brain URL sourcing.
· fix: Remove the baseURL field from smokeEnv. The playwright.config.ts is the single source
  of truth for the base URL and already throws on missing env. If tests ever need the URL
  directly, they should read it from playwright's built-in baseURL fixture, not smokeEnv.
  export const smokeEnv = {
    testEmail: required('SMOKE_TEST_EMAIL'),
    testCode: required('SMOKE_TEST_CODE'),
  } as const
· reference: CLAUDE.md rule 2 (delete unused code immediately)

Subagents

Agent Verdict
parity-checker PAIRED — all web changes are test harness only; no mobile mirror required (CLAUDE.md explicitly bans mobile E2E)
i18n-syncer IN SYNC — no new user-facing strings introduced
contract-aligner N/A — no packages/shared/src/types/* or endpoints.ts changes; no orbit-api changes
security-reviewer N/A — no orbit-api code changed

Validation

Check Result
Lint N/A — author reports 0 errors locally; no working-tree changes to run against
Type check N/A — author reports clean tsc --noEmit locally
Tests N/A — smoke suite requires prod secrets and cannot run in this environment by design
Build (api) N/A — no backend changes

What's good

  • Single-seam auth (e2e/support/auth.ts) is the right pattern — all login mechanics in one function, specs never touch auth directly.
  • Unique smoke labels (smokeLabel()) prevent collision between concurrent runs cleanly.
  • Global setup + teardown resets the smoke account before and after every run — no prod data residue.
  • Rollback capture is deterministic — the previous-deployment URL is captured before the smoke run, not after failure, so a rollback always has a valid target.
  • Secret validation step in the workflow catches misconfigured environments before hitting prod.
  • forbidOnly: isCI correctly prevents a stray .only from silently shrinking the suite in CI.
  • cancel-in-progress: false on the concurrency group is intentional and correct — back-to-back pushes each need to smoke their own deployment independently.
  • retries: 0 strictly enforces the fix-or-delete-on-flake policy.
  • The actions/checkout@v5 / actions/setup-node@v5 versions are consistent with the rest of the repo's workflows.
  • data-testid attributes are locale-proof selectors that survive copy/i18n edits — correct per the PR description.

Recommendation

One Medium finding (dead smokeEnv.baseURL export) — fix before or immediately after merge. The finding does not block correctness or security. Recommended action: approve and trim the dead export.

thomasluizon and others added 3 commits June 23, 2026 21:55
…moke

# Conflicts:
#	apps/web/package.json
#	package-lock.json
…te path, scope triggers

- Remove unused smokeEnv.baseURL (playwright.config.ts is the single source of truth for the base URL).

- Import STORAGE_STATE_PATH into playwright.config.ts instead of hardcoding the auth-state path twice.

- Add a paths filter to smoke-prod.yml (apps/web/**, packages/shared/**) so mobile-only pushes don't trigger a smoke run with no matching Vercel deploy (which would time out and could spuriously roll back).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The merge commit captured the pre-regeneration lockfile (main's, without the smoke suite's @playwright/test). npm ci requires lockfile<->package.json sync; regenerated via npm install --package-lock-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: PR #253

Scope: PR #253test(smoke): prod post-deploy Playwright smoke (5 flows) + rollback-on-red (#227)
Recommendation: APPROVE

Summary

This PR adds the sanctioned 5-test post-deploy Playwright smoke suite (closes #227) with a GitHub Actions workflow that waits for the Vercel production deployment, runs the suite, and rolls back on failure. It also instruments the necessary production UI components with data-testid / dataTestId attributes. The implementation is clean, the test-isolation strategy (unique labels per run, dedicated smoke account with reset on both sides) is solid, and the rollback mechanism is deterministic. No Critical or High findings.

Findings

Critical

None

High

None

Medium

[MEDIUM] Teardown project will fail with 401 when setup fails partway through
· dimension: Correctness (#1)
· location: apps/web/playwright.config.ts:38-46 + apps/web/e2e/global.setup.ts:6-14
· issue: In global.setup.ts, storageState is persisted at line 13 — after resetSmokeAccount and the PUT /api/profile/onboarding call. If either of those calls throws (e.g. the reset endpoint returns 5xx), setup fails and storageState is never written. Playwright still fires the cleanup teardown project because it is registered as setup's teardown. global.teardown.ts loads storageState: STORAGE_STATE_PATH, but the file doesn't exist, causing the teardown itself to throw — masking the real setup failure in CI output.
· risk: CI reports a confusing double-failure (setup + teardown), making it harder to diagnose the root cause. The teardown also silently skips the account reset, leaving residue in prod.
· fix: Move page.context().storageState() to immediately after authenticate() so auth is always persisted before any post-auth API calls can fail.
· reference: CLAUDE.md rule 8

[MEDIUM] listHabitTitles uses an unchecked as cast on an external API response
· dimension: Type safety (#6)
· location: apps/web/e2e/support/api.ts:23
· issue: (await response.json()) as { items?: HabitListItem[] } asserts the shape of the /api/habits response without a Zod parse. If the BFF contract changes (e.g. key renamed), the test will silently return [] and pass because body.items will be undefined and the ?? [] fallback fires.
· risk: A contract change could make the Astra smoke test pass vacuously (habit list always empty), defeating the server-side assertion.
· fix: Import the shared habits-list Zod type from @orbit/shared/types and parse with .safeParse() — throw on failure so a shape change produces an explicit error rather than a silent vacuous pass.
· reference: CLAUDE.md rule 3

Low / Info

None

Subagents

Agent Verdict
parity-checker PAIRED — data-testid additions are not behavioral changes; mobile has no E2E suite per CLAUDE.md policy
i18n-syncer N/A — no user-facing strings added or changed
contract-aligner N/A — no packages/shared/src/types/* or endpoints.ts changes
security-reviewer N/A — no orbit-api changes

Validation

Check Result
Lint N/A (CI context; *.config.ts eslint-ignored; JSDoc/no-comments compliance confirmed by manual inspection)
Type check N/A (no working-tree changes; PR body reports tsc --noEmit clean)
Tests N/A (smoke suite requires prod creds; unit tests unaffected)
Build (api) N/A (no backend changes)

What's good

  • Policy compliance is tight. retries: 0, forbidOnly: isCI, workers: 1, and a hard cap of 5 specs are exactly what CLAUDE.md's Testing section requires.
  • Auth seam is well-designed. e2e/support/auth.ts is the single place that knows how to log in; every spec inherits storage state; if auth strategy changes, only one function changes.
  • Rollback target captured before smoke runs. Step order (wait → capture previous → run smoke → rollback on failure) ensures the rollback target is always the deployment that was live before this push.
  • No-pollution strategy is complete. Uniquely-labelled habit names prevent cross-run collisions; smoke account reset fires in both setup and teardown.
  • Selector hygiene is correct. data-testid and data-habit-title are used for load-bearing selectors; locale-dependent text is avoided. All selectors verified to exist in the production source.
  • cancel-in-progress: false means a queued smoke run is never cancelled by a subsequent push, which would leave a failed deployment un-rolled-back.
  • vercel rollback receives the full deployment URL (constructed as https://${prev}), which is the format the CLI expects.
  • Secret validation at workflow start fails fast before any prod traffic is generated.

Recommendation

Two Mediums, no Critical or High. Merging as-is is acceptable with the two items tracked as immediate follow-ups:

  1. Teardown ordering (global.setup.ts L13): move storageState save to immediately after authenticate().
  2. Type-safe BFF assertion (e2e/support/api.ts L23): replace the as cast with a Zod parse of the shared habits-list type.

The main smoke mechanics are sound. Suite is ready to protect prod once orbit-api#217 merges and CI secrets are set.

@sonarqubecloud

Copy link
Copy Markdown

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review PR 253 - APPROVE. No Critical or High findings. The PR tip (2b4dbc5) is a package-lock sync; substantive work was already reviewed and approved in prior reviews. Confirmed: dead smokeEnv.baseURL export removed, STORAGE_STATE_PATH reused, workflow paths filter scoped, listHabitTitles field fix present. Two Mediums carried from prior review (teardown ordering in global.setup.ts and as-cast in api.ts) are not re-raised as blockers per rubric. Suite is policy-compliant: 5 specs, retries=0, forbidOnly in CI, unique run labels, account reset in setup+teardown, deterministic rollback target. Parity/i18n/contract/security all N/A (web-only E2E harness, no shared type or API changes). Approved.

@thomasluizon
thomasluizon merged commit 76127aa into main Jun 24, 2026
10 checks passed
@thomasluizon
thomasluizon deleted the feature/phase2-prod-smoke branch June 24, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E2E smoke (~5 web flows) vs QA, blocks prod promotion

1 participant