name: Visual diff # Same as preview-smoke — waits for Vercel's Preview deployment, then captures # Playwright screenshots against it. UI-paths-only trigger to keep cost down. # Paths are tcg-vault-specific (pages router, JS). on: pull_request: branches: [main] paths: - 'pages/**' # Exclude API-only edits — they don't render UI, so they can't move # any visual-diff pixels. Order matters: GitHub Actions evaluates the # `paths:` list with minimatch and applies `!`-prefixed exclusions # only after they've already matched a prior include. Keep this entry # immediately AFTER `pages/**`. # Surfaced by `tighten-visual-diff-path-filter` after PR #19 # (cors-tighten) and PR #20 (add-rate-limiting) both falsely # triggered Screenshot diff at ~55s/PR. - '!pages/api/**' - 'components/**' - 'styles/**' - 'tailwind.config.js' - 'postcss.config.js' # Baseline-only PRs (specs + committed snapshots) should still run # visual diff now that home.png is in tests/visual/__screenshots__/. - 'tests/visual/**' concurrency: group: visual-diff-${{ github.event.pull_request.number }} cancel-in-progress: true # `deployments: read` + `statuses: read` are required by wait-for-vercel-preview # (see preview-smoke.yml for context). `pull-requests: write` is required by the # final github-script step that posts the "Visual Diff" comment back to the PR; # without it the API returns 403 even though the screenshots upload fine. permissions: contents: read deployments: read pull-requests: write statuses: read jobs: gate: name: Should run? runs-on: [self-hosted, axiom] outputs: should_run: ${{ steps.check.outputs.should_run }} steps: - id: check env: # See preview-smoke.yml for the rationale: ${{ github.event.* }} # inlined into shell is a syntax-error + injection vector. 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::Visual diff skipped on fork PR (bypass secret unavailable to forks)" elif echo "$PR_BODY" | grep -qE 'pipeline:.*skip.*\bvisual\b'; then echo "should_run=false" >> $GITHUB_OUTPUT echo "::notice::Visual diff skipped via pipeline directive" else echo "should_run=true" >> $GITHUB_OUTPUT fi visual: name: Screenshot diff needs: gate if: needs.gate.outputs.should_run == 'true' runs-on: [self-hosted, axiom] timeout-minutes: 20 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 # See preview-smoke.yml for the no-`set-bypass-cookie` rationale. 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 browsers — see preview-smoke.yml for rationale. - 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: Capture screenshots (PR) run: npx playwright test --project=visual --update-snapshots=none env: BASE_URL: ${{ steps.vercel.outputs.url }} VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} continue-on-error: true - name: Upload screenshots + diffs if: always() uses: actions/upload-artifact@v4 with: name: visual-diff path: | tests/visual/__screenshots__/ test-results/ retention-days: 7 - name: Comment on PR with diff link if: always() uses: actions/github-script@v7 with: script: | const run = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: `## Visual Diff\n\nScreenshots and diffs uploaded as artifacts: [view run](${run})\n\nIf intentional changes: update snapshots locally with \`npx playwright test --project=visual --update-snapshots\` and commit.` });