name: Docs E2E Tests # "Docs E2E" is a required status check on master, so this workflow must # produce a check run on every PR — a `paths` trigger filter would leave # non-docs PRs waiting on a check that never reports. Path scoping happens # in the "Detect changed paths" step instead; when nothing docs-related # changed, the remaining steps are skipped and the check reports green. on: pull_request: types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] branches: ['master'] workflow_dispatch: inputs: base_url: description: 'Base URL to test against' required: false default: 'https://supabase.com' type: string page_paths: description: 'Comma-separated /docs/... paths to test (required for manual runs)' required: false default: '' type: string concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read statuses: read pull-requests: read env: CI: true jobs: e2e: name: Docs E2E if: github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false timeout-minutes: 30 runs-on: blacksmith-4vcpu-ubuntu-2404 steps: # Runs before checkout — reads the PR file list from the API. `docs` # mirrors the path scope this workflow used to have as a trigger filter; # `docs_components` scopes the global element scan to the markup it reads; # `docs_app` decides whether a Vercel docs preview exists to test against. - name: Detect changed paths id: changes if: github.event_name == 'pull_request' uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: filters: | docs: - 'apps/docs/content/guides/**/*.mdx' - 'apps/docs/content/troubleshooting/**/*.mdx' - 'apps/docs/content/_partials/**' - 'e2e/docs/**' - 'e2e/shared/**' - 'pnpm-lock.yaml' - '.github/workflows/docs-e2e.yml' docs_components: - 'apps/docs/app/**' - 'apps/docs/components/**' - 'apps/docs/features/ui/**' - 'apps/docs/layouts/**' - 'e2e/docs/**' - 'e2e/shared/**' - 'pnpm-lock.yaml' - '.github/workflows/docs-e2e.yml' docs_app: - 'apps/docs/**' - name: Decide which suites run id: gate env: EVENT_NAME: ${{ github.event_name }} DOCS: ${{ steps.changes.outputs.docs }} DOCS_COMPONENTS: ${{ steps.changes.outputs.docs_components }} run: | set -euo pipefail pages=false global_elements=false any=false if [ "$EVENT_NAME" = "workflow_dispatch" ]; then pages=true global_elements=true else if [ "$DOCS" = "true" ]; then pages=true; fi if [ "$DOCS_COMPONENTS" = "true" ]; then global_elements=true; fi fi if [ "$pages" = "true" ] || [ "$global_elements" = "true" ]; then any=true; fi { echo "pages=$pages" echo "global_elements=$global_elements" echo "any=$any" } >> "$GITHUB_OUTPUT" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: steps.gate.outputs.any == 'true' with: persist-credentials: false # Need full history on PRs so we can diff against the base branch. # Use string '0' — numeric 0 is falsy in GitHub Actions expressions. fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }} sparse-checkout: | e2e/docs e2e/shared scripts patches apps/docs/content/guides apps/docs/content/troubleshooting apps/docs/content/_partials apps/docs/scripts/federated-content/sources - name: Use Node.js if: steps.gate.outputs.any == 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' # Map changed owned content (guides, troubleshooting, partials) to page # URLs. Harness-only PRs resolve to skip=true and exit before Playwright. # This scope covers the page suite only; the global element suite scans a # fixed page list. - name: Resolve docs E2E scope id: scope if: steps.gate.outputs.any == 'true' env: EVENT_NAME: ${{ github.event_name }} BASE_REF: ${{ github.base_ref }} PAGE_PATHS_INPUT: ${{ inputs.page_paths }} run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then if [ -z "$PAGE_PATHS_INPUT" ]; then echo "skip=true" >> "$GITHUB_OUTPUT" echo "paths=" >> "$GITHUB_OUTPUT" echo "Manual run requires the page_paths input." exit 0 fi echo "skip=false" >> "$GITHUB_OUTPUT" printf 'paths=%s\n' "$PAGE_PATHS_INPUT" >> "$GITHUB_OUTPUT" exit 0 fi git diff --name-only --diff-filter=ACMR "origin/$BASE_REF"...HEAD \ | node --experimental-strip-types e2e/docs/scripts/resolve-docs-scope.ts - name: Skip Playwright (no in-scope pages) if: steps.scope.outputs.skip == 'true' run: echo "No in-scope docs pages changed; skipping the page suite." - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 if: steps.scope.outputs.skip == 'false' || steps.gate.outputs.global_elements == 'true' name: Install pnpm with: run_install: false - name: Enable pnpm store cache if: steps.scope.outputs.skip == 'false' || steps.gate.outputs.global_elements == 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version-file: '.nvmrc' cache: 'pnpm' # Vercel skips the docs preview when a PR only changes the harness # (e2e/docs, workflow), so wait for a preview only when apps/docs changed. # # Vercel's GitHub App stopped writing GitHub Deployment objects on # 2026-02-17 (broken app auth), so vercel/wait-for-deployment-action # times out polling that API even though the preview builds fine. # Poll the "Vercel – docs" commit status instead — Vercel keeps posting # those — then resolve the deployment it points to via Vercel's own API # to get the actual preview URL. See scripts/waitForVercelPreview.js. # A Vercel failure or timeout is not the author's problem, and the required # "Vercel – docs" check already reports it. Resolve no URL and skip below. - name: Wait for Vercel docs preview if: (steps.scope.outputs.skip == 'false' || steps.gate.outputs.global_elements == 'true') && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.changes.outputs.docs_app == 'true' id: deployment continue-on-error: true run: node scripts/waitForVercelPreview.js env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} VERCEL_STATUS_CONTEXT: 'Vercel – docs' VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} # The two suites disagree on one case: with no preview the page suite has # nothing to test, while the global element suite can fall back to production. - name: Resolve base URL if: steps.scope.outputs.skip == 'false' || steps.gate.outputs.global_elements == 'true' id: base-url env: EVENT_NAME: ${{ github.event_name }} BASE_URL_INPUT: ${{ inputs.base_url }} DEPLOYMENT_URL: ${{ steps.deployment.outputs.deployment-url }} PAGE_PATHS: ${{ steps.scope.outputs.paths }} PAGES_SKIP: ${{ steps.scope.outputs.skip }} DOCS_APP_CHANGED: ${{ steps.changes.outputs.docs_app }} RUN_GLOBAL: ${{ steps.gate.outputs.global_elements }} run: | set -euo pipefail PAGES=false if [ "$PAGES_SKIP" = "false" ]; then PAGES=true fi if [ "$EVENT_NAME" = "workflow_dispatch" ]; then printf 'url=%s\n' "$BASE_URL_INPUT" >> "$GITHUB_OUTPUT" # Non-production targets are previews, which may need the bypass. if [ "$BASE_URL_INPUT" = "https://supabase.com" ]; then echo "use_bypass=false" >> "$GITHUB_OUTPUT" else echo "use_bypass=true" >> "$GITHUB_OUTPUT" fi echo "should_test=$PAGES" >> "$GITHUB_OUTPUT" echo "global_should_test=true" >> "$GITHUB_OUTPUT" exit 0 fi if [ -n "$DEPLOYMENT_URL" ]; then printf 'url=%s\n' "$DEPLOYMENT_URL" >> "$GITHUB_OUTPUT" echo "use_bypass=true" >> "$GITHUB_OUTPUT" echo "should_test=$PAGES" >> "$GITHUB_OUTPUT" echo "global_should_test=$RUN_GLOBAL" >> "$GITHUB_OUTPUT" exit 0 fi # A change that ships no markup leaves production serving the same # global elements this would test. GLOBAL=false if [ "$RUN_GLOBAL" = "true" ] && [ "$DOCS_APP_CHANGED" != "true" ]; then GLOBAL=true fi if [ "$GLOBAL" = "true" ]; then echo "url=https://supabase.com" >> "$GITHUB_OUTPUT" else echo "url=" >> "$GITHUB_OUTPUT" fi echo "use_bypass=false" >> "$GITHUB_OUTPUT" # Production is not a substitute for the page suite: it lacks pages # this pull request adds, so testing it fails a required check for a # valid change. echo "should_test=false" >> "$GITHUB_OUTPUT" echo "global_should_test=$GLOBAL" >> "$GITHUB_OUTPUT" if [ "$RUN_GLOBAL" = "true" ] && [ "$GLOBAL" != "true" ]; then echo "::warning::No Vercel docs preview URL for this pull request, so nothing is serving the global elements it changes. Skipping that scan rather than scanning production, which still has the old markup." fi if [ "$PAGES" != "true" ]; then exit 0 fi echo "::warning::No Vercel docs preview URL for this pull request, so there is nothing serving its content to test. Skipping Playwright rather than testing production, which does not have pages this pull request adds." { echo "### Docs E2E skipped: no preview to test against" echo echo "Nothing is serving this pull request's content, and production is not a" echo "substitute — pages it adds do not exist there yet." echo echo "Fork pull requests reach this path because they run without repository" echo "secrets. A maintainer can run the suite against the preview manually:" echo echo '```' echo "gh workflow run docs-e2e.yml \\" echo " -f base_url= \\" echo " -f page_paths=$PAGE_PATHS" echo '```' } >> "$GITHUB_STEP_SUMMARY" - name: Install dependencies if: steps.base-url.outputs.should_test == 'true' || steps.base-url.outputs.global_should_test == 'true' run: pnpm install --frozen-lockfile --filter=e2e-docs... - name: Install Playwright Chromium if: steps.base-url.outputs.should_test == 'true' || steps.base-url.outputs.global_should_test == 'true' run: pnpm -C e2e/docs exec playwright install chromium --with-deps --only-shell - name: Run docs E2E if: steps.base-url.outputs.should_test == 'true' working-directory: e2e/docs run: pnpm run e2e:docs env: PLAYWRIGHT_BASE_URL: ${{ steps.base-url.outputs.url }} DOCS_E2E_PAGE_PATHS: ${{ steps.scope.outputs.paths }} VERCEL_AUTOMATION_BYPASS_SECRET: ${{ steps.base-url.outputs.use_bypass == 'true' && secrets.VERCEL_AUTOMATION_BYPASS_DOCS || '' }} - name: Run docs global elements E2E if: steps.base-url.outputs.global_should_test == 'true' working-directory: e2e/docs run: pnpm run e2e:docs:global-elements env: PLAYWRIGHT_BASE_URL: ${{ steps.base-url.outputs.url }} VERCEL_AUTOMATION_BYPASS_SECRET: ${{ steps.base-url.outputs.use_bypass == 'true' && secrets.VERCEL_AUTOMATION_BYPASS_DOCS || '' }} - name: Upload Playwright report if: failure() && (steps.base-url.outputs.should_test == 'true' || steps.base-url.outputs.global_should_test == 'true') uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: docs-playwright-report path: | e2e/docs/playwright-report/ e2e/docs/playwright-report-global-elements/ e2e/docs/test-results/ retention-days: 7