name: Baseline Checks

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

concurrency:
  group: baseline-checks-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint-web:
    name: Lint Web
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run lint
        run: pnpm lint:web

  verify-docs:
    name: Verify Docs
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run docs verification
        run: pnpm verify:docs

  verify-api-mcp:
    name: Verify API and MCP
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run API contract verification
        run: pnpm verify:api-contracts

      - name: Run MCP package verification
        run: pnpm verify:vrdex-mcp

  typecheck-web:
    name: Typecheck Web
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run typecheck
        run: pnpm typecheck:web

      - name: Run web unit tests
        run: pnpm test:web

  typecheck-backend:
    name: Typecheck Backend
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Setup Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run backend typecheck
        run: pnpm typecheck:backend

      - name: Run backend unit tests
        run: pnpm test:backend

      - name: Run temporal runtime tests
        run: pnpm test:temporal-runtime

      - name: Run temporal inference worker tests
        run: pnpm test:temporal-inference

  verify-backend-local:
    name: Verify Backend Local
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Verify backend bootstrap
        run: pnpm verify:backend:local

      - name: Check generated backend files
        run: git diff --exit-code -- convex/_generated

  restream-local-checks:
    name: Restream Local Checks
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Check restream-relevant changes
        id: changes
        env:
          BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
        run: |
          set -euo pipefail

          base="$BASE_SHA"
          if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
            base="HEAD^"
          fi

          changed_files="$(git diff --name-only "$base" HEAD)"
          restream_files="$(printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/(baseline-checks|terraform)\.yml|apps/discord-gateway/.*|workers/restream/.*|scripts/restream-(ffmpeg-proof\.mjs|local-validation\.ts)|convex/(_eventMediaControl|schema)\.ts|convex/README\.md|tests/backend/event-media-control\.test\.ts|docs/(backend/event-schema\.md|deployment/(discord-gateway|restream-worker)\.md|engineering/restream-ffmpeg-proof\.md)|infra/terraform/(README\.md|restream-worker/.*)|package\.json|pnpm-lock\.yaml)$' || true)"

          if [ -z "$restream_files" ]; then
            echo "enabled=false" >> "$GITHUB_OUTPUT"
            {
              echo "## Restream Local Checks"
              echo "Skipped because no restream-relevant files changed."
            } >> "$GITHUB_STEP_SUMMARY"
            exit 0
          fi

          echo "enabled=true" >> "$GITHUB_OUTPUT"
          {
            echo "## Restream Local Checks"
            echo "Changed restream-relevant files:"
            printf -- '- `%s`\n' $restream_files
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Setup pnpm
        if: steps.changes.outputs.enabled == 'true'
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        if: steps.changes.outputs.enabled == 'true'
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        if: steps.changes.outputs.enabled == 'true'
        run: pnpm install --frozen-lockfile

      - name: Install FFmpeg
        if: steps.changes.outputs.enabled == 'true'
        run: |
          sudo apt-get update
          sudo apt-get install -y ffmpeg

      - name: Typecheck Discord Gateway
        if: steps.changes.outputs.enabled == 'true'
        run: pnpm typecheck:discord-gateway

      - name: Test Discord Gateway
        if: steps.changes.outputs.enabled == 'true'
        run: pnpm test:discord-gateway

      - name: Build hosted restream worker image
        if: steps.changes.outputs.enabled == 'true'
        run: docker build -f workers/restream/Dockerfile -t vrdex-restream-worker:ci .

      - name: Dry-run hosted restream worker image
        if: steps.changes.outputs.enabled == 'true'
        run: |
          docker run --rm \
            -e CONVEX_URL=https://example.invalid \
            -e VRDEX_RESTREAM_BENCHMARK_MODE=dry-run \
            -e VRDEX_RESTREAM_QUALITY_GATE=1080p60 \
            -e VRDEX_RESTREAM_MAX_CONCURRENT_WORKERS=10 \
            -e VRDEX_RESTREAM_MAX_SESSION_SECONDS=43200 \
            -e VRDEX_RESTREAM_KILL_SWITCH_SSM_PARAMETER=/vrdex/restream/hosted-worker/enabled \
            -e VRDEX_RESTREAM_SECRET_REF_NAMES=event-media/vrcdn/main-output \
            vrdex-restream-worker:ci

      - name: Run local restream validation
        if: steps.changes.outputs.enabled == 'true'
        run: pnpm proof:restream:local

      - name: Check local restream validation
        if: steps.changes.outputs.enabled == 'true'
        run: pnpm check:restream:local

  build-web:
    name: Build Web
    needs:
      - lint-web
      - verify-docs
      - verify-api-mcp
      - typecheck-web
      - typecheck-backend
      - verify-backend-local
      - restream-local-checks
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run build
        run: pnpm build:web

  build-storybook:
    name: Build Storybook
    needs: [lint-web, verify-docs, typecheck-web]
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Run Storybook build
        run: pnpm build:storybook

  deploy-convex-production:
    name: Deploy Convex Production
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    needs: [build-web]
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Check Convex deploy key
        id: gate
        env:
          CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY_PROD }}
        run: |
          if [ -z "$CONVEX_DEPLOY_KEY" ]; then
            echo "enabled=false" >> "$GITHUB_OUTPUT"
            {
              echo "## Convex production deploy"
              echo "Skipped because CONVEX_DEPLOY_KEY_PROD is not configured."
            } >> "$GITHUB_STEP_SUMMARY"
            exit 0
          fi

          echo "enabled=true" >> "$GITHUB_OUTPUT"

      - name: Setup pnpm
        if: steps.gate.outputs.enabled == 'true'
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        if: steps.gate.outputs.enabled == 'true'
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        if: steps.gate.outputs.enabled == 'true'
        run: pnpm install --frozen-lockfile

      - name: Provision PostHog claim delivery when configured
        if: steps.gate.outputs.enabled == 'true'
        env:
          CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY_PROD }}
          POSTHOG_PROJECT_API_KEY: ${{ secrets.TERRAFORM_POSTHOG_PUBLIC_KEY }}
        run: |
          set -euo pipefail
          if [ -z "$POSTHOG_PROJECT_API_KEY" ]; then
            echo "::notice::TERRAFORM_POSTHOG_PUBLIC_KEY is absent; analytics provisioning was skipped and existing Convex configuration was left unchanged."
            exit 0
          fi
          if ! printf '%s' "$POSTHOG_PROJECT_API_KEY" | grep -qE '^phc_[A-Za-z0-9_-]+$'; then
            echo "::error::TERRAFORM_POSTHOG_PUBLIC_KEY is not a PostHog project key."
            exit 1
          fi
          printf '%s' "$POSTHOG_PROJECT_API_KEY" | pnpm exec convex env set POSTHOG_PROJECT_API_KEY

      - name: Deploy Convex functions and schema
        if: steps.gate.outputs.enabled == 'true'
        env:
          CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY_PROD }}
        run: pnpm exec convex deploy --yes --typecheck=enable

      - name: Run deploy-time migrations
        if: steps.gate.outputs.enabled == 'true'
        env:
          CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY_PROD }}
        run: pnpm exec convex run migrations:runAll --prod

  playwright-data-flow:
    name: Playwright Data Flow
    if: github.event_name == 'pull_request'
    needs: [build-web]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Run mutation-backed profile submission flow
        id: flow
        env:
          PLAYWRIGHT_RECORD_VIDEO: "true"
          VRDEX_ENABLE_E2E_HELPERS: "true"
          VRDEX_E2E_BROWSER_TOKEN: local-playwright-token
          VRDEX_E2E_CONVEX_SECRET: local-convex-e2e-secret
        run: pnpm --filter web exec playwright test --grep @flow --project=desktop-chromium --workers=1

      - name: Write data-flow summary
        if: always()
        env:
          FLOW_OUTCOME: ${{ steps.flow.outcome }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts
          cat > apps/web/playwright-artifacts/data-flow-summary.md <<EOF
          ## Playwright data-flow preview

          Outcome: ${FLOW_OUTCOME}

          Captured flow:
          - test-gated profile submission form
          - gated helper rejection without the Playwright token
          - Convex profile creation
          - submission success state
          - public profile page readback
          - discovery search readback

          Run: ${RUN_URL}
          EOF

      - name: Upload Playwright data-flow artifacts
        id: upload
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: playwright-data-flow
          if-no-files-found: error
          retention-days: 7
          path: |
            apps/web/playwright-report
            apps/web/test-results
            apps/web/playwright-artifacts

  playwright-hosted-data-flow:
    name: Playwright Hosted Data Flow
    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
    needs: [build-web]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Check hosted E2E configuration
        id: gate
        env:
          HOSTED_BASE_URL: ${{ vars.VRDEX_HOSTED_E2E_BASE_URL }}
          HOSTED_BROWSER_TOKEN: ${{ secrets.VRDEX_HOSTED_E2E_BROWSER_TOKEN }}
        run: |
          if [ -z "$HOSTED_BASE_URL" ] || [ -z "$HOSTED_BROWSER_TOKEN" ]; then
            echo "enabled=false" >> "$GITHUB_OUTPUT"
            {
              echo "## Playwright hosted data-flow"
              echo "Skipped because VRDEX_HOSTED_E2E_BASE_URL or VRDEX_HOSTED_E2E_BROWSER_TOKEN is not configured."
            } >> "$GITHUB_STEP_SUMMARY"
            exit 0
          fi

          echo "enabled=true" >> "$GITHUB_OUTPUT"

      - name: Setup pnpm
        if: steps.gate.outputs.enabled == 'true'
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        if: steps.gate.outputs.enabled == 'true'
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        if: steps.gate.outputs.enabled == 'true'
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        if: steps.gate.outputs.enabled == 'true'
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Run hosted mutation-backed profile submission flow
        if: steps.gate.outputs.enabled == 'true'
        id: hosted
        env:
          PLAYWRIGHT_BASE_URL: ${{ vars.VRDEX_HOSTED_E2E_BASE_URL }}
          PLAYWRIGHT_RECORD_VIDEO: "true"
          PLAYWRIGHT_SKIP_WEBSERVERS: "true"
          VRDEX_ENABLE_E2E_EXTENDED_PROFILE_FLOW: ${{ vars.VRDEX_HOSTED_E2E_EXTENDED_PROFILE_FLOW }}
          VRDEX_ENABLE_E2E_AUTH_HELPERS: ${{ vars.VRDEX_HOSTED_E2E_AUTH_HELPERS }}
          VRDEX_ENABLE_E2E_ADAPTER_HELPERS: ${{ vars.VRDEX_HOSTED_E2E_ADAPTER_HELPERS }}
          VRDEX_ENABLE_E2E_DEVELOPER_CREDENTIALS: ${{ vars.VRDEX_HOSTED_E2E_DEVELOPER_CREDENTIALS }}
          VRDEX_E2E_BROWSER_TOKEN: ${{ secrets.VRDEX_HOSTED_E2E_BROWSER_TOKEN }}
          # Staging Clerk *development* instance. `clerkSetup()` refuses a
          # production secret key outright, and `check-vercel-env.mjs` already
          # requires the test tier on every non-production Vercel build, so these
          # are the same instance the target itself runs on.
          #
          # The switch is separate from the keys on purpose: with it unset the
          # auth specs skip, so merging this cannot turn the lane red before the
          # secrets exist. With it set to true, missing keys fail the run.
          VRDEX_ENABLE_E2E_CLERK_AUTH: ${{ vars.VRDEX_HOSTED_E2E_CLERK_AUTH }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.VRDEX_HOSTED_E2E_CLERK_PUBLISHABLE_KEY }}
          CLERK_SECRET_KEY: ${{ secrets.VRDEX_HOSTED_E2E_CLERK_SECRET_KEY }}
          VRDEX_E2E_RUN_ID: pr-${{ github.event.pull_request.number }}-${{ github.run_id }}-${{ github.run_attempt }}
        run: pnpm test:e2e:hosted

      # Separate from the step above because it is the only suite that needs both
      # projects. The account surfaces have a distinct mobile layout, and
      # `test:e2e:hosted` pins desktop, so folding these in there would capture
      # desktop only. `always()` so a failure above still produces the signed-in
      # screenshots — they are most useful on the run that broke something.
      #
      # The three PLAYWRIGHT_* overrides are not optional. Playwright clears the
      # output directory and both reports when a run starts, so without them this
      # second run in the job would delete the first one's traces, videos, HTML
      # report and results.json before the upload step collects them — the exact
      # failure #213 fixed for the staging deploy, arriving here by the same route.
      # The uploaded artifact would show only this step, under a name that says
      # hosted data-flow.
      # Gated on the Clerk flag like the auth-session contract step, and for the
      # same reason. With the flag unset — the documented rollout default — every
      # test in the suite skips, Playwright exits 0, and the step outcome is
      # `success`: a summary line reading "Signed-in account captures: success"
      # over zero screenshots, which is the green-over-no-coverage ambiguity this
      # summary exists to remove. Gated, the outcome is `skipped` and says so.
      #
      # Only the flag needs this. Enabled-but-unusable already fails loudly —
      # `clerkTestAuthAvailability` throws on missing keys and on auth helpers
      # being off for a hosted target, rather than skipping.
      - name: Capture signed-in account screenshots
        if: always() && steps.gate.outputs.enabled == 'true' && vars.VRDEX_HOSTED_E2E_CLERK_AUTH == 'true'
        id: account-visual
        env:
          PLAYWRIGHT_OUTPUT_DIR: test-results-account-visual
          PLAYWRIGHT_HTML_REPORT_DIR: playwright-report-account-visual
          PLAYWRIGHT_JSON_REPORT_FILE: playwright-artifacts/results-account-visual.json
          PLAYWRIGHT_BASE_URL: ${{ vars.VRDEX_HOSTED_E2E_BASE_URL }}
          PLAYWRIGHT_SKIP_WEBSERVERS: "true"
          VRDEX_ENABLE_E2E_AUTH_HELPERS: ${{ vars.VRDEX_HOSTED_E2E_AUTH_HELPERS }}
          VRDEX_E2E_BROWSER_TOKEN: ${{ secrets.VRDEX_HOSTED_E2E_BROWSER_TOKEN }}
          VRDEX_ENABLE_E2E_CLERK_AUTH: ${{ vars.VRDEX_HOSTED_E2E_CLERK_AUTH }}
          NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.VRDEX_HOSTED_E2E_CLERK_PUBLISHABLE_KEY }}
          CLERK_SECRET_KEY: ${{ secrets.VRDEX_HOSTED_E2E_CLERK_SECRET_KEY }}
          VRDEX_E2E_RUN_ID: pr-${{ github.event.pull_request.number }}-${{ github.run_id }}-${{ github.run_attempt }}
        run: pnpm test:e2e:hosted:account-visual

      - name: Write hosted data-flow summary
        if: always() && steps.gate.outputs.enabled == 'true'
        env:
          HOSTED_OUTCOME: ${{ steps.hosted.outcome }}
          # Reported separately rather than folded into HOSTED_OUTCOME. They are
          # two Playwright runs against two different suites, and a summary that
          # said `success` while the account captures failed would contradict the
          # job it is attached to — which is the failure mode this whole summary
          # was added to prevent.
          ACCOUNT_VISUAL_OUTCOME: ${{ steps.account-visual.outcome }}
          HOSTED_BASE_URL: ${{ vars.VRDEX_HOSTED_E2E_BASE_URL }}
          HOSTED_EXTENDED_PROFILE_FLOW: ${{ vars.VRDEX_HOSTED_E2E_EXTENDED_PROFILE_FLOW }}
          HOSTED_AUTH_HELPERS: ${{ vars.VRDEX_HOSTED_E2E_AUTH_HELPERS }}
          HOSTED_ADAPTER_HELPERS: ${{ vars.VRDEX_HOSTED_E2E_ADAPTER_HELPERS }}
          HOSTED_DEVELOPER_CREDENTIALS: ${{ vars.VRDEX_HOSTED_E2E_DEVELOPER_CREDENTIALS }}
          HOSTED_CLERK_AUTH: ${{ vars.VRDEX_HOSTED_E2E_CLERK_AUTH }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts
          cat > apps/web/playwright-artifacts/hosted-data-flow-summary.md <<EOF
          ## Playwright hosted data-flow

          Outcome: ${HOSTED_OUTCOME}

          Signed-in account captures: ${ACCOUNT_VISUAL_OUTCOME:-not run}

          Target: ${HOSTED_BASE_URL}

          Hosted extended profile flow: ${HOSTED_EXTENDED_PROFILE_FLOW:-skipped}

          Hosted auth helpers: ${HOSTED_AUTH_HELPERS:-skipped}

          Hosted adapter helpers: ${HOSTED_ADAPTER_HELPERS:-skipped}

          Hosted developer credentials: ${HOSTED_DEVELOPER_CREDENTIALS:-skipped}

          Clerk test auth: ${HOSTED_CLERK_AUTH:-skipped}

          The flags above report configuration, not coverage. The auth-backed
          specs need every one of their flags *and* Clerk test auth: with
          \`VRDEX_HOSTED_E2E_CLERK_AUTH\` unset they skip, and only the captured
          flow below ran. With it set, they fail rather than skip when the Clerk
          keys are missing.

          Captured flow:
          - hosted test-gated profile submission form
          - hosted Convex profile creation
          - hosted public profile page readback
          - hosted discovery search readback
          - hosted E2E cleanup

          Run: ${RUN_URL}
          EOF

      - name: Upload hosted data-flow artifacts
        id: upload
        if: always() && steps.gate.outputs.enabled == 'true'
        uses: actions/upload-artifact@v7
        with:
          name: playwright-hosted-data-flow
          if-no-files-found: warn
          retention-days: 7
          path: |
            apps/web/playwright-report
            apps/web/test-results
            apps/web/playwright-report-account-visual
            apps/web/test-results-account-visual
            apps/web/playwright-artifacts

  playwright-public-preview:
    name: Playwright Public Preview
    if: github.event_name == 'pull_request'
    needs: [build-web]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Capture public route screenshots
        id: visual
        run: pnpm test:e2e:visual

      - name: Write visual summary
        if: always()
        env:
          VISUAL_OUTCOME: ${{ steps.visual.outcome }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts

          # Counted from the run, not asserted. This previously stated "all public
          # route checks passed" unconditionally — on a failed run, and on a run
          # where a whole lane skipped. A skipped Playwright file still exits 0,
          # and `clerk-auth.ts` records where that leads: the auth-session matrix
          # lane reported green for months over a spec that ran nothing, with a
          # summary describing coverage that never executed. A summary that cannot
          # be wrong is not evidence, and this job exists to be evidence.
          counts=$(python3 - <<'PY'
          import json, pathlib
          report = pathlib.Path("apps/web/playwright-artifacts/results.json")
          if not report.exists():
              print("unknown unknown unknown unknown unknown")
              raise SystemExit
          data = json.loads(report.read_text(encoding="utf-8"))
          # `flaky` is Playwright's status for a test that failed an attempt and
          # then passed; the run exits successfully. Folding it into `failed`
          # would print failures next to `Outcome: success`, which is its own kind
          # of summary nobody can trust. Counted on its own instead, because a
          # retry that passed is worth seeing without being alarming.
          tally = {"passed": 0, "failed": 0, "skipped": 0, "flaky": 0}
          buckets = {"expected": "passed", "skipped": "skipped", "flaky": "flaky"}
          def walk(suite):
              for spec in suite.get("specs", []):
                  for test in spec.get("tests", []):
                      tally[buckets.get(test.get("status", ""), "failed")] += 1
              for child in suite.get("suites", []):
                  walk(child)
          for suite in data.get("suites", []):
              walk(suite)
          # A discovery, config, or global-setup failure lands in the report's
          # top-level `errors` and creates no test status at all, so counting only
          # `suites` can print `Outcome: failure` beside `0 failed` — the same
          # untrustworthy pairing this whole summary exists to remove.
          print(tally["passed"], tally["failed"], tally["skipped"], tally["flaky"], len(data.get("errors", [])))
          PY
          )
          passed=$(echo "$counts" | cut -d' ' -f1)
          failed=$(echo "$counts" | cut -d' ' -f2)
          skipped=$(echo "$counts" | cut -d' ' -f3)
          flaky=$(echo "$counts" | cut -d' ' -f4)
          fatal=$(echo "$counts" | cut -d' ' -f5)

          {
            echo "## Playwright public screenshot preview"
            echo ""
            echo "Outcome: ${VISUAL_OUTCOME}"
            echo ""
            echo "Tests: ${passed} passed, ${failed} failed, ${skipped} skipped, ${flaky} flaky."
            if [ "$fatal" != "0" ] && [ "$fatal" != "unknown" ]; then
              echo ""
              echo "**${fatal} fatal error(s)** before or outside any test — discovery,"
              echo "configuration, or global setup. These produce no test status, so the"
              echo "counts above do not reflect them."
            fi
            if [ "$skipped" != "0" ] && [ "$skipped" != "unknown" ]; then
              echo ""
              echo "Skipped tests produced no screenshots."
            fi
            echo ""
            echo "Run: ${RUN_URL}"
          } > apps/web/playwright-artifacts/visual-summary.md

      - name: Upload Playwright artifacts
        id: upload
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: playwright-public-preview
          if-no-files-found: error
          retention-days: 7
          path: |
            apps/web/playwright-report
            apps/web/test-results
            apps/web/playwright-artifacts

  playwright-image-diff:
    name: Playwright Image Diff
    if: github.event_name == 'pull_request'
    needs: [build-web]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Compare public route screenshots to baselines
        id: snapshots
        run: pnpm test:e2e:snapshots

      - name: Write image-diff summary
        if: always()
        env:
          DIFF_OUTCOME: ${{ steps.snapshots.outcome }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts
          cat > apps/web/playwright-artifacts/image-diff-summary.md <<EOF
          ## Playwright image diff

          Outcome: ${DIFF_OUTCOME}

          Baselines: committed public route screenshots for desktop and mobile Chromium.

          Run: ${RUN_URL}
          EOF

      - name: Find changed baseline images
        if: always()
        env:
          BASE_REF: ${{ github.base_ref }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          set -euo pipefail
          git fetch --no-tags --depth=1 origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"
          git fetch --no-tags --depth=1 origin "pull/${PR_NUMBER}/head:refs/remotes/origin/pr-head"
          git diff --name-status "refs/remotes/origin/${BASE_REF}" "refs/remotes/origin/pr-head" -- 'apps/web/e2e/__screenshots__/desktop-chromium/*.png' 'apps/web/e2e/__screenshots__/mobile-chromium/*.png' > apps/web/playwright-artifacts/changed-baselines.txt
          cat apps/web/playwright-artifacts/changed-baselines.txt

      - name: Upload Playwright image-diff artifacts
        id: upload
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: playwright-image-diff
          if-no-files-found: error
          retention-days: 7
          path: |
            apps/web/playwright-report
            apps/web/test-results
            apps/web/playwright-artifacts
            apps/web/e2e/__screenshots__

  storybook-component-preview:
    name: Storybook Component Preview
    if: github.event_name == 'pull_request'
    needs: [build-storybook]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Capture Storybook component screenshots
        id: visual
        run: pnpm test:storybook:visual

      - name: Write Storybook visual summary
        if: always()
        env:
          VISUAL_OUTCOME: ${{ steps.visual.outcome }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts
          cat > apps/web/playwright-artifacts/storybook-visual-summary.md <<EOF
          ## Storybook component screenshot preview

          Outcome: ${VISUAL_OUTCOME}

          Screenshots: component primitive stories captured on desktop and mobile.

          Run: ${RUN_URL}
          EOF

      - name: Upload Storybook visual artifacts
        id: upload
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: storybook-component-preview
          if-no-files-found: error
          retention-days: 7
          path: |
            apps/web/playwright-report-storybook
            apps/web/test-results
            apps/web/playwright-artifacts

  storybook-image-diff:
    name: Storybook Image Diff
    if: github.event_name == 'pull_request'
    needs: [build-storybook]
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Setup pnpm
        uses: pnpm/action-setup@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: 24
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright Chromium
        working-directory: apps/web
        run: pnpm exec playwright install --with-deps chromium

      - name: Compare Storybook component screenshots to baselines
        id: snapshots
        run: pnpm test:storybook:snapshots

      - name: Write Storybook image-diff summary
        if: always()
        env:
          DIFF_OUTCOME: ${{ steps.snapshots.outcome }}
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          mkdir -p apps/web/playwright-artifacts
          cat > apps/web/playwright-artifacts/storybook-image-diff-summary.md <<EOF
          ## Storybook component image diff

          Outcome: ${DIFF_OUTCOME}

          Baselines: committed Storybook primitive screenshots for desktop and mobile Chromium.

          Run: ${RUN_URL}
          EOF

      - name: Find changed Storybook baseline images
        if: always()
        env:
          BASE_REF: ${{ github.base_ref }}
          PR_NUMBER: ${{ github.event.pull_request.number }}
        run: |
          set -euo pipefail
          git fetch --no-tags --depth=1 origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}"
          git fetch --no-tags --depth=1 origin "pull/${PR_NUMBER}/head:refs/remotes/origin/pr-head"
          git diff --name-status "refs/remotes/origin/${BASE_REF}" "refs/remotes/origin/pr-head" -- 'apps/web/e2e/__screenshots__/storybook-*/*.png' > apps/web/playwright-artifacts/changed-storybook-baselines.txt
          cat apps/web/playwright-artifacts/changed-storybook-baselines.txt

      - name: Upload Storybook image-diff artifacts
        id: upload
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: storybook-image-diff
          if-no-files-found: error
          retention-days: 7
          path: |
            apps/web/playwright-report-storybook
            apps/web/test-results
            apps/web/playwright-artifacts
            apps/web/e2e/__screenshots__/storybook-*

  pr-verification-report:
    name: PR Verification Report
    if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
    needs:
      - playwright-data-flow
      - playwright-hosted-data-flow
      - playwright-public-preview
      - playwright-image-diff
      - storybook-component-preview
      - storybook-image-diff
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      actions: read
      contents: read
      issues: write
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Test PR verification report helper
        run: node --test .github/scripts/pr-verification-report.test.cjs

      - name: Update consolidated PR verification report
        uses: actions/github-script@v9
        env:
          NEEDS_JSON: ${{ toJSON(needs) }}
        with:
          script: |
            const { updatePrVerificationReport } = require(
              "./.github/scripts/pr-verification-report.cjs",
            );
            await updatePrVerificationReport({
              core,
              context,
              github,
              needs: JSON.parse(process.env.NEEDS_JSON),
            });
