open-design/.github/workflows/visual-pr-capture.yml
Marc Chan f294ab4915
chore(ci): add visual regression PR workflow (#2372)
* Add visual regression PR workflow

* Allow manual visual PR comments

* Post visual comments for same-repo PRs

* fix(ci): surface R2 lookup failures in visual report

Generated-By: looper 0.8.1 (runner=fixer, agent=opencode)

* Align visual workflow names
2026-05-20 15:05:59 +08:00

204 lines
7.7 KiB
YAML

name: visual-pr-capture
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'apps/web/**'
- '.github/actions/visual-screenshot/**'
- '.github/workflows/visual-*.yml'
- 'e2e/package.json'
- 'e2e/playwright.visual.config.ts'
- 'e2e/lib/playwright/**'
- 'e2e/scripts/playwright.ts'
- 'e2e/scripts/visual-report.ts'
- 'e2e/ui/visual-*.test.ts'
- 'pnpm-lock.yaml'
permissions:
actions: read
contents: read
pull-requests: write
concurrency:
group: visual-pr-capture-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
capture:
name: Capture PR visual screenshots
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Prepare visual screenshot environment
uses: ./.github/actions/visual-screenshot
- name: Capture PR screenshots
id: capture
continue-on-error: true
env:
OD_VISUAL_OUTPUT_DIR: ui/reports/visual-screenshots
run: |
pnpm -C e2e exec tsx scripts/playwright.ts clean
pnpm -C e2e exec playwright test -c playwright.visual.config.ts
- name: Write capture manifest
run: |
mkdir -p e2e/ui/reports/visual-report
cat > e2e/ui/reports/visual-report/manifest.json <<'JSON'
{
"pr_number": "${{ github.event.pull_request.number }}",
"head_sha": "${{ github.event.pull_request.head.sha }}",
"base_sha": "${{ github.event.pull_request.base.sha }}",
"run_id": "${{ github.run_id }}",
"capture_outcome": "${{ steps.capture.outcome }}"
}
JSON
- name: Upload PR visual artifact
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: visual-pr-capture-${{ github.event.pull_request.number }}-${{ github.run_id }}
path: |
e2e/ui/reports/visual-screenshots
e2e/ui/reports/visual-results.json
e2e/ui/reports/visual-report/manifest.json
if-no-files-found: ignore
retention-days: 7
comment_same_repo:
name: Comment PR visual screenshots
needs: [capture]
if: ${{ always() && !github.event.pull_request.draft && github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.8
with:
version: 10.33.2
- name: Setup Node.js
uses: actions/setup-node@v6.4.0
with:
node-version: 24
package-manager-cache: false
- name: Resolve pnpm store path
id: pnpm-store
shell: bash
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store cache
uses: actions/cache/restore@v5.0.5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download PR visual artifact
uses: actions/download-artifact@v8
with:
run-id: ${{ github.run_id }}
path: visual-artifact
merge-multiple: true
github-token: ${{ github.token }}
- name: Validate capture manifest
id: manifest
shell: bash
env:
EXPECTED_PR_NUMBER: ${{ github.event.pull_request.number }}
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha }}
EXPECTED_RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
manifest="visual-artifact/visual-report/manifest.json"
if [ ! -f "$manifest" ]; then
manifest="visual-artifact/manifest.json"
fi
if [ ! -f "$manifest" ]; then
echo "Capture manifest not found" >&2
find visual-artifact -maxdepth 4 -type f >&2
exit 1
fi
pr_number="$(jq -r '.pr_number' "$manifest")"
head_sha="$(jq -r '.head_sha' "$manifest")"
base_sha="$(jq -r '.base_sha' "$manifest")"
run_id="$(jq -r '.run_id' "$manifest")"
capture_outcome="$(jq -r '.capture_outcome // "success"' "$manifest")"
if [ "$pr_number" != "$EXPECTED_PR_NUMBER" ] || [ "$head_sha" != "$EXPECTED_HEAD_SHA" ] || [ "$base_sha" != "$EXPECTED_BASE_SHA" ] || [ "$run_id" != "$EXPECTED_RUN_ID" ]; then
echo "Capture manifest does not match the current pull_request event." >&2
jq . "$manifest" >&2
exit 1
fi
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT"
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"
echo "capture_outcome=$capture_outcome" >> "$GITHUB_OUTPUT"
- name: Build visual diff report
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
R2_PUBLIC_ORIGIN: ${{ vars.R2_PUBLIC_ORIGIN }}
CLOUDFLARE_R2_RELEASES_AK: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
CLOUDFLARE_R2_RELEASES_SK: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
run: |
pnpm -C e2e exec tsx scripts/visual-report.ts compare-pr \
--pr-number "${{ steps.manifest.outputs.pr_number }}" \
--run-id "${{ steps.manifest.outputs.run_id }}" \
--head-sha "${{ steps.manifest.outputs.head_sha }}" \
--base-sha "${{ steps.manifest.outputs.base_sha }}" \
--capture-outcome "${{ steps.manifest.outputs.capture_outcome }}" \
--screenshots ../visual-artifact/visual-screenshots \
--comment-out ui/reports/visual-report/comment.md \
--manifest-out ui/reports/visual-report/report-manifest.json
- name: Upsert PR comment
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.manifest.outputs.pr_number }}
run: |
set -euo pipefail
body_path="e2e/ui/reports/visual-report/comment.md"
comments_json="$RUNNER_TEMP/comments.json"
gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" > "$comments_json"
comment_id="$(jq -r '.[] | select(.body | contains("<!-- visual-regression-bot -->")) | .id' "$comments_json" | tail -n 1)"
if [ -n "$comment_id" ]; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" --field "body=$(cat "$body_path")"
else
gh api --method POST "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" --field "body=$(cat "$body_path")"
fi
- name: Upload visual report artifact
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: visual-pr-report-${{ github.event.pull_request.number }}-${{ github.run_id }}
path: e2e/ui/reports/visual-report
if-no-files-found: ignore
retention-days: 7