name: Preview smoke

# Waits for Vercel's per-PR Preview deployment to be ready, then runs
# Playwright smoke against its URL.
#
# Vercel's GitHub integration auto-deploys every push and posts a Deployment
# to the GitHub API once ready. We wait on that Deployment so we always hit
# the canonical preview URL Vercel just published.
#
# REQUIRES: @playwright/test installed. Until then, this workflow will fail
# on `npx playwright install`. See .convoys/ for the testing convoy.

on:
  pull_request:
    branches: [main]
    types: [labeled, opened, synchronize, reopened]
    # Skip the heavy Playwright smoke on doc-only PRs. Same set as
    # ci.yml's paths-ignore — see `slash-ci-minutes` convoy
    # (2026-06-04). Code/style/UI PRs still trigger smoke normally.
    paths-ignore:
      - '.convoys/**'
      - '**/*.md'
      - 'docs/**'
      - 'AGENTS.md'
      - '.cursor/**'
      - 'README.md'

concurrency:
  group: preview-smoke-${{ github.event.pull_request.number }}
  cancel-in-progress: true

# Default workflow token is read-only on this repo. The `wait-for-vercel-preview`
# action needs `deployments: read` to query Vercel's GitHub Deployment status,
# plus `statuses: read` because some Vercel deployments use commit statuses
# instead of the Deployments API. `pull-requests: read` lets it correlate the
# deployment back to this PR. Without these, the action 403s on the Checks API
# and the smoke job fails before Playwright even starts.
permissions:
  contents: read
  deployments: read
  pull-requests: read
  statuses: read

jobs:
  gate:
    name: Should run?
    runs-on: [self-hosted, axiom]
    outputs:
      should_run: ${{ steps.check.outputs.should_run }}
    steps:
      - name: Decide
        id: check
        env:
          # Route PR body + fork flag through env vars instead of inline
          # ${{ }} interpolation. Direct ${{ github.event.pull_request.body }}
          # in a shell command pastes arbitrary user-controlled text (parens,
          # backticks, pipes, heredocs) directly into the script — both a
          # syntax-error risk AND a shell-injection vector. Quoting the env
          # vars below makes both safe.
          PR_BODY: ${{ github.event.pull_request.body }}
          PR_IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
        run: |
          if [[ "$PR_IS_FORK" == "true" ]]; then
            echo "should_run=false" >> $GITHUB_OUTPUT
            echo "::notice::Smoke skipped on fork PR (bypass secret unavailable to forks)"
          elif echo "$PR_BODY" | grep -qE 'pipeline:.*skip.*\bsmoke\b'; then
            echo "should_run=false" >> $GITHUB_OUTPUT
            echo "::notice::Smoke skipped via pipeline directive"
          else
            echo "should_run=true" >> $GITHUB_OUTPUT
          fi

  smoke:
    name: Playwright smoke
    needs: gate
    if: needs.gate.outputs.should_run == 'true'
    runs-on: [self-hosted, axiom]
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

      - name: Wait for Vercel Preview deployment
        id: vercel
        uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          max_timeout: 120
          # NOTE: do NOT add `&x-vercel-set-bypass-cookie=true` here. Vercel
          # responds to that with a 307 + Set-Cookie (`_vercel_jwt`), but
          # axios in Node has no cookie jar — the cookie is dropped before
          # the followup request, which then 401s. For this one-shot
          # healthcheck the bare bypass query is enough; the cookie variant
          # belongs in the future Playwright config where the browser does
          # have a cookie jar.
          path: /?x-vercel-protection-bypass=${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm

      - name: Cache node_modules
        id: cache-node-modules
        uses: actions/cache@v4
        with:
          path: node_modules
          key: node-modules-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}

      - run: npm ci
        if: steps.cache-node-modules.outputs.cache-hit != 'true'

      # Cache Playwright's browser binaries (~/.cache/ms-playwright) keyed
      # by the resolved @playwright/test version in the lockfile. Saves
      # ~15-25s/run of chromium download + system-deps install. Cache
      # invalidates automatically on any Playwright version bump.
      - name: Resolve Playwright version
        id: pw-version
        run: |
          VERSION=$(node -p "require('./package-lock.json').packages['node_modules/@playwright/test'].version")
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
      - name: Cache Playwright browsers
        id: cache-playwright
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}-chromium

      - name: Install Playwright browsers
        run: |
          if [ "${{ steps.cache-playwright.outputs.cache-hit }}" = "true" ]; then
            npx playwright install-deps chromium
          else
            npx playwright install --with-deps chromium
          fi

      - name: Run smoke tests
        run: npx playwright test --project=smoke
        env:
          BASE_URL: ${{ steps.vercel.outputs.url }}
          VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}

      - name: Upload Playwright report on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
