|
| 1 | +name: Visual |
| 2 | + |
| 3 | +# Renders tests/visual/cases.toml under lavapipe |
| 4 | + |
| 5 | +on: |
| 6 | + push: |
| 7 | + branches: [ main ] |
| 8 | + pull_request: |
| 9 | + branches: [ main ] |
| 10 | + types: [ opened, synchronize, reopened, labeled, unlabeled ] |
| 11 | + |
| 12 | +permissions: |
| 13 | + contents: read |
| 14 | + actions: read |
| 15 | + |
| 16 | +concurrency: |
| 17 | + group: visual-${{ github.event.pull_request.number || github.sha }} |
| 18 | + cancel-in-progress: true |
| 19 | + |
| 20 | +env: |
| 21 | + CARGO_TERM_COLOR: always |
| 22 | + CARGO_INCREMENTAL: 0 |
| 23 | + CARGO_PROFILE_RELEASE_DEBUG: 0 |
| 24 | + WGPU_BACKEND: vulkan |
| 25 | + WGPU_ADAPTER_NAME: llvmpipe |
| 26 | + # Catch reads of uninitialized GPU memory instead of rendering whatever was there |
| 27 | + LVP_POISON_MEMORY: "true" |
| 28 | + # llvmpipe JITs for the host CPU |
| 29 | + LP_NATIVE_VECTOR_WIDTH: "256" |
| 30 | + ODIFF_VERSION: 4.5.0 |
| 31 | + |
| 32 | +jobs: |
| 33 | + visual: |
| 34 | + name: Visual regression |
| 35 | + # Only re-run on label changes that involve the override label. |
| 36 | + if: >- |
| 37 | + github.event_name == 'push' || |
| 38 | + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || |
| 39 | + github.event.label.name == 'deliberate-rendering-change' |
| 40 | + runs-on: ubuntu-24.04 |
| 41 | + timeout-minutes: 90 |
| 42 | + steps: |
| 43 | + - uses: actions/checkout@v4 |
| 44 | + with: |
| 45 | + submodules: true |
| 46 | + # PR checkouts are merge commits; HEAD^1 is the main commit they're compared against. |
| 47 | + fetch-depth: 2 |
| 48 | + persist-credentials: false |
| 49 | + |
| 50 | + - uses: ./.github/actions/setup |
| 51 | + |
| 52 | + - uses: ./.github/actions/install-mesa |
| 53 | + |
| 54 | + - uses: astral-sh/setup-uv@v6 |
| 55 | + |
| 56 | + - name: Install xvfb and odiff |
| 57 | + run: | |
| 58 | + sudo apt-get install -y --no-install-recommends xvfb |
| 59 | + npm install -g "odiff-bin@$ODIFF_VERSION" |
| 60 | +
|
| 61 | + - uses: actions/cache@v4 |
| 62 | + with: |
| 63 | + path: | |
| 64 | + ~/.cargo/bin/ |
| 65 | + ~/.cargo/registry/index/ |
| 66 | + ~/.cargo/registry/cache/ |
| 67 | + ~/.cargo/git/db/ |
| 68 | + target/ |
| 69 | + key: ${{ runner.os }}-cargo-visual-${{ hashFiles('**/Cargo.lock') }} |
| 70 | + restore-keys: | |
| 71 | + ${{ runner.os }}-cargo-visual- |
| 72 | + ${{ runner.os }}-cargo- |
| 73 | +
|
| 74 | + - name: Render |
| 75 | + run: xvfb-run -a -s "-screen 0 1920x1080x24" python3 tests/visual/visual.py render --out out/actual |
| 76 | + |
| 77 | + - uses: actions/upload-artifact@v7 |
| 78 | + with: |
| 79 | + name: visual-screenshots |
| 80 | + path: out/actual |
| 81 | + retention-days: ${{ github.event_name == 'push' && 90 || 14 }} |
| 82 | + |
| 83 | + - name: Fetch baseline from main |
| 84 | + if: github.event_name == 'pull_request' |
| 85 | + env: |
| 86 | + GH_TOKEN: ${{ github.token }} |
| 87 | + run: | |
| 88 | + sha=$(git rev-parse HEAD^1) |
| 89 | + latest() { |
| 90 | + gh run list -R "$GITHUB_REPOSITORY" --workflow visual.yml --branch main --event push \ |
| 91 | + --commit "$sha" --limit 1 --json databaseId,status,conclusion --jq '.[0] // empty' |
| 92 | + } |
| 93 | + run=$(latest) |
| 94 | + if [ -n "$run" ] && [ "$(jq -r .status <<< "$run")" != completed ]; then |
| 95 | + id=$(jq -r .databaseId <<< "$run") |
| 96 | + echo "Waiting for main's run $id to publish screenshots" |
| 97 | + gh run watch "$id" -R "$GITHUB_REPOSITORY" --interval 30 > /dev/null || true |
| 98 | + run=$(latest) |
| 99 | + fi |
| 100 | + if [ "$(jq -r '.conclusion // empty' <<< "$run")" = success ] && |
| 101 | + gh run download "$(jq -r .databaseId <<< "$run")" -R "$GITHUB_REPOSITORY" -n visual-screenshots -D out/baseline; then |
| 102 | + echo "Using screenshots from run $(jq -r .databaseId <<< "$run")" |
| 103 | + else |
| 104 | + echo "::notice::No screenshots published for main at $sha; cases are reported as new" |
| 105 | + fi |
| 106 | +
|
| 107 | + - name: Compare |
| 108 | + id: compare |
| 109 | + if: github.event_name == 'pull_request' |
| 110 | + env: |
| 111 | + ALLOW_CHANGES: ${{ contains(github.event.pull_request.labels.*.name, 'deliberate-rendering-change') && '--allow-changes' || '' }} |
| 112 | + run: | |
| 113 | + mkdir -p out/baseline |
| 114 | + python3 tests/visual/visual.py compare --baseline out/baseline --actual out/actual --out out/report $ALLOW_CHANGES |
| 115 | +
|
| 116 | + - uses: actions/upload-artifact@v7 |
| 117 | + id: report |
| 118 | + if: always() && hashFiles('out/report/report.html') != '' |
| 119 | + with: |
| 120 | + path: out/report/report.html |
| 121 | + archive: false |
| 122 | + retention-days: 14 |
| 123 | + |
| 124 | + - name: Summarize |
| 125 | + if: always() && hashFiles('out/report/summary.md') != '' |
| 126 | + env: |
| 127 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 128 | + REPORT_URL: ${{ steps.report.outputs.artifact-url }} |
| 129 | + run: | |
| 130 | + mkdir -p out/comment |
| 131 | + { |
| 132 | + cat out/report/summary.md |
| 133 | + echo |
| 134 | + echo "[Open the visual report]($REPORT_URL) · [workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)" |
| 135 | + } > out/comment/comment.md |
| 136 | + cat out/comment/comment.md >> "$GITHUB_STEP_SUMMARY" |
| 137 | + echo "$PR_NUMBER" > out/comment/pr |
| 138 | + jq '[.results[] | select(.status != "pass")] | length > 0' out/report/results.json > out/comment/notable |
| 139 | +
|
| 140 | + - uses: actions/upload-artifact@v7 |
| 141 | + if: always() && hashFiles('out/comment/comment.md') != '' |
| 142 | + with: |
| 143 | + name: visual-comment |
| 144 | + path: out/comment |
| 145 | + retention-days: 1 |
0 commit comments