name: Visual regression on: push: branches: - main pull_request: types: [opened, synchronize, reopened, labeled] pull_request_target: types: [opened, synchronize, reopened, labeled] concurrency: # A synchronize action emits both pull_request and pull_request_target. Keep # same-repository and fork runs separate. group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.sha }} cancel-in-progress: true permissions: contents: read jobs: visual-regression: # Same-repository PRs use the unprivileged event. Organization members' # fork PRs are trusted; every other fork revision needs safe-to-deploy. if: | github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && ( github.event.action != 'labeled' || github.event.label.name == 'visual-change-approved' ) ) || ( github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository && ( ( contains(fromJSON('["OWNER", "MEMBER"]'), github.event.pull_request.author_association) && ( github.event.action != 'labeled' || github.event.label.name == 'visual-change-approved' ) ) || ( github.event.action == 'labeled' && ( github.event.label.name == 'safe-to-deploy' || ( github.event.label.name == 'visual-change-approved' && contains(github.event.pull_request.labels.*.name, 'safe-to-deploy') ) ) ) ) ) timeout-minutes: 7 runs-on: ubuntu-latest environment: name: development steps: - uses: actions/checkout@v5 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha || github.sha }} allow-unsafe-pr-checkout: ${{ github.event_name == 'pull_request_target' }} - uses: ./.github/actions/submodules-checkout with: submodules-ssh-key: ${{ secrets.PRIVATE_GITHUB_DEPLOY_TOKEN }} - uses: ./.github/actions/ci-setup - name: Cache Playwright Chromium uses: actions/cache@v5 with: path: | ~/.cache/ms-playwright/chromium-* ~/.cache/ms-playwright/chromium_headless_shell-* ~/.cache/ms-playwright/ffmpeg-* key: playwright-chromium-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - name: Install pinned visual browser run: | pnpm --filter @webstudio-is/sdk-components-animation exec playwright install chromium playwright_browser_path="$(pnpm --filter @webstudio-is/sdk-components-animation exec node -e 'console.log(require("playwright").chromium.executablePath())')" test -x "$playwright_browser_path" echo "WEBSTUDIO_BROWSER_PATH=$playwright_browser_path" >> "$GITHUB_ENV" - name: Install visual comparison dependencies run: sudo apt-get install --no-install-recommends -y tesseract-ocr - name: Resolve visual baseline cache key id: visual-cache env: BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }} run: | echo "base=$(git merge-base HEAD "$BASE_REF")" >> "$GITHUB_OUTPUT" echo "head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" browser_version="$("$WEBSTUDIO_BROWSER_PATH" --version)" runtime_hash="$(pnpm tsx scripts/visual-regression/runtime-hash.ts "$browser_version")" echo "runtime=$runtime_hash" >> "$GITHUB_OUTPUT" - name: Restore visual baseline screenshots uses: actions/cache/restore@v5 with: path: .visual-regression-cache/${{ steps.visual-cache.outputs.base }}-* key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.base }}-${{ steps.visual-cache.outputs.runtime }} - name: Compare portable stories env: BASE_REF: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }} APPROVE_VISUAL_CHANGES: ${{ github.event_name == 'push' || (github.event.action == 'labeled' && github.event.label.name == 'visual-change-approved') }} run: | args=(--base "$BASE_REF") if [[ "$APPROVE_VISUAL_CHANGES" == "true" ]]; then args+=(--approve-visual-changes) fi pnpm visual-regression -- "${args[@]}" - name: Validate visual comparison report if: always() run: | node -e 'const report = require("./.visual-regression/report/report.json"); if (!Array.isArray(report.comparisons) || report.comparisons.length === 0) throw new Error("Visual comparison report has no story results.")' - name: Upload visual comparison report if: always() uses: actions/upload-artifact@v7 with: name: visual-regression-report path: .visual-regression/report if-no-files-found: error retention-days: 14 - name: Cache visual screenshots for dependent revisions if: always() uses: actions/cache/save@v5 with: path: .visual-regression-cache/${{ steps.visual-cache.outputs.head }}-* key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.head }}-${{ steps.visual-cache.outputs.runtime }} - name: Cache generated baseline screenshots if: always() uses: actions/cache/save@v5 with: path: .visual-regression-cache/${{ steps.visual-cache.outputs.base }}-* key: visual-baseline-v1-${{ runner.os }}-${{ steps.visual-cache.outputs.base }}-${{ steps.visual-cache.outputs.runtime }}