|
| 1 | +name: GPU Tests from PR Comment |
| 2 | + |
| 3 | +# Lets maintainers (admin / write access) run GPU tests on a PR by commenting: |
| 4 | +# /diffusers-bot pytest <args> |
| 5 | +# e.g. `/diffusers-bot pytest tests/models/test_modeling_common.py -k "some_test"`. |
| 6 | + |
| 7 | + |
| 8 | +on: |
| 9 | + issue_comment: |
| 10 | + types: [created] |
| 11 | + |
| 12 | +# Default to read-only; jobs that comment opt into `pull-requests: write` explicitly. |
| 13 | +permissions: |
| 14 | + contents: read |
| 15 | + |
| 16 | +concurrency: |
| 17 | + # A newer command on the same PR supersedes an in-flight one. |
| 18 | + group: diffusers-bot-${{ github.event.issue.number }} |
| 19 | + cancel-in-progress: true |
| 20 | + |
| 21 | +env: |
| 22 | + DIFFUSERS_IS_CI: yes |
| 23 | + OMP_NUM_THREADS: 8 |
| 24 | + MKL_NUM_THREADS: 8 |
| 25 | + HF_XET_HIGH_PERFORMANCE: 1 |
| 26 | + PYTEST_TIMEOUT: 600 |
| 27 | + # Force version overrides across every `uv pip install`: pin tokenizers and the |
| 28 | + # torch/torchvision/torchaudio set baked into the image so `-U` installs can't bump |
| 29 | + # torch and break torchvision's C++ ABI. Re-written into the file in the install step. |
| 30 | + UV_OVERRIDE: /tmp/uv-overrides.txt |
| 31 | + |
| 32 | +jobs: |
| 33 | + gate: |
| 34 | + name: Authorize & launch |
| 35 | + # Only react to `/diffusers-bot pytest …` comments on open PRs. |
| 36 | + if: | |
| 37 | + github.event.issue.pull_request && |
| 38 | + github.event.issue.state == 'open' && |
| 39 | + startsWith(github.event.comment.body, '/diffusers-bot pytest') |
| 40 | + runs-on: ubuntu-22.04 |
| 41 | + permissions: |
| 42 | + pull-requests: write |
| 43 | + outputs: |
| 44 | + pytest_args: ${{ steps.parse.outputs.pytest_args }} |
| 45 | + comment_id: ${{ steps.comment.outputs.comment_id }} |
| 46 | + steps: |
| 47 | + - name: Check commenter permission |
| 48 | + id: auth |
| 49 | + env: |
| 50 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 51 | + REPO: ${{ github.repository }} |
| 52 | + COMMENTER: ${{ github.event.comment.user.login }} |
| 53 | + run: | |
| 54 | + PERM=$(gh api "repos/${REPO}/collaborators/${COMMENTER}/permission" --jq '.permission' 2>/dev/null || echo "none") |
| 55 | + echo "Commenter @${COMMENTER} has permission: ${PERM}" |
| 56 | + if [[ "$PERM" == "admin" || "$PERM" == "write" ]]; then |
| 57 | + echo "authorized=true" >> "$GITHUB_OUTPUT" |
| 58 | + else |
| 59 | + echo "authorized=false" >> "$GITHUB_OUTPUT" |
| 60 | + fi |
| 61 | +
|
| 62 | + - name: Reject unauthorized commenter |
| 63 | + if: steps.auth.outputs.authorized != 'true' |
| 64 | + env: |
| 65 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 66 | + REPO: ${{ github.repository }} |
| 67 | + PR: ${{ github.event.issue.number }} |
| 68 | + COMMENTER: ${{ github.event.comment.user.login }} |
| 69 | + run: | |
| 70 | + gh api -X POST "repos/${REPO}/issues/${PR}/comments" \ |
| 71 | + -f body="🚫 Sorry @${COMMENTER}, you're not authorized to run \`/diffusers-bot\`. Only maintainers with write or admin access can trigger GPU tests." >/dev/null |
| 72 | + echo "::error::Only maintainers with write/admin access can run /diffusers-bot." |
| 73 | + exit 1 |
| 74 | +
|
| 75 | + - name: Acknowledge with 👀 |
| 76 | + env: |
| 77 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 78 | + REPO: ${{ github.repository }} |
| 79 | + COMMENT_ID: ${{ github.event.comment.id }} |
| 80 | + run: | |
| 81 | + gh api -X POST "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content="eyes" >/dev/null |
| 82 | +
|
| 83 | + - name: Parse pytest args |
| 84 | + id: parse |
| 85 | + env: |
| 86 | + COMMENT_BODY: ${{ github.event.comment.body }} |
| 87 | + run: | |
| 88 | + # Use only the first line of the comment, strip the command prefix. |
| 89 | + FIRST_LINE=$(printf '%s' "$COMMENT_BODY" | head -n1) |
| 90 | + ARGS="${FIRST_LINE#/diffusers-bot pytest}" |
| 91 | + # Trim surrounding whitespace/CR. |
| 92 | + ARGS="$(printf '%s' "$ARGS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" |
| 93 | + echo "pytest_args=${ARGS}" >> "$GITHUB_OUTPUT" |
| 94 | +
|
| 95 | + - name: Post "running" comment |
| 96 | + id: comment |
| 97 | + env: |
| 98 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 99 | + REPO: ${{ github.repository }} |
| 100 | + PR: ${{ github.event.issue.number }} |
| 101 | + COMMENTER: ${{ github.event.comment.user.login }} |
| 102 | + PYTEST_ARGS: ${{ steps.parse.outputs.pytest_args }} |
| 103 | + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 104 | + run: | |
| 105 | + BODY="⏳ Running \`pytest ${PYTEST_ARGS}\` on a GPU runner — [view logs](${RUN_URL}). |
| 106 | +
|
| 107 | + Triggered by @${COMMENTER}." |
| 108 | + CID=$(gh api -X POST "repos/${REPO}/issues/${PR}/comments" -f body="$BODY" --jq '.id') |
| 109 | + echo "comment_id=${CID}" >> "$GITHUB_OUTPUT" |
| 110 | +
|
| 111 | + gpu_tests: |
| 112 | + name: Run pytest on GPU |
| 113 | + needs: gate |
| 114 | + runs-on: |
| 115 | + group: aws-g4dn-2xlarge |
| 116 | + container: |
| 117 | + image: diffusers/diffusers-pytorch-cuda |
| 118 | + options: --gpus all --shm-size "16gb" --ipc host |
| 119 | + # Least privilege: this job checks out and runs untrusted fork code, so it gets no |
| 120 | + # write token. Comment writes happen only in `gate`/`report`. |
| 121 | + permissions: |
| 122 | + contents: read |
| 123 | + defaults: |
| 124 | + run: |
| 125 | + shell: bash |
| 126 | + steps: |
| 127 | + - name: Checkout PR head |
| 128 | + uses: actions/checkout@v6 |
| 129 | + with: |
| 130 | + # Works for forks too — no fork credentials needed. |
| 131 | + ref: refs/pull/${{ github.event.issue.number }}/head |
| 132 | + fetch-depth: 2 |
| 133 | + |
| 134 | + - name: NVIDIA-SMI |
| 135 | + run: nvidia-smi |
| 136 | + |
| 137 | + - name: Install dependencies |
| 138 | + run: | |
| 139 | + printf 'tokenizers<0.23.0\ntorch==2.10.0\ntorchvision==0.25.0\ntorchaudio==2.10.0\n' > "$UV_OVERRIDE" |
| 140 | + uv pip install -e ".[quality,training,test]" |
| 141 | + uv pip install peft@git+https://github.com/huggingface/peft.git |
| 142 | + uv pip uninstall accelerate && uv pip install -U accelerate@git+https://github.com/huggingface/accelerate.git |
| 143 | + uv pip uninstall transformers huggingface_hub && UV_PRERELEASE=allow uv pip install -U transformers@git+https://github.com/huggingface/transformers.git |
| 144 | +
|
| 145 | + - name: Environment |
| 146 | + run: diffusers-cli env |
| 147 | + |
| 148 | + - name: Run pytest |
| 149 | + env: |
| 150 | + # No secrets here: this step runs untrusted fork code (pytest imports the PR's |
| 151 | + # conftest.py/plugins), so exposing a token would let a malicious PR exfiltrate |
| 152 | + # it. Public Hub models download without auth; gated-repo tests are unsupported. |
| 153 | + # https://pytorch.org/docs/stable/notes/randomness.html#avoiding-nondeterministic-algorithms |
| 154 | + CUBLAS_WORKSPACE_CONFIG: :16:8 |
| 155 | + # Forwarded via env (not interpolated into the script) to avoid breakage on |
| 156 | + # quotes/special characters in a legitimate command. |
| 157 | + PYTEST_ARGS: ${{ needs.gate.outputs.pytest_args }} |
| 158 | + run: | |
| 159 | + eval "pytest --make-reports=tests_bot_gpu $PYTEST_ARGS" |
| 160 | +
|
| 161 | + - name: Failure short reports |
| 162 | + if: ${{ failure() }} |
| 163 | + run: | |
| 164 | + cat reports/tests_bot_gpu_stats.txt || true |
| 165 | + cat reports/tests_bot_gpu_failures_short.txt || true |
| 166 | +
|
| 167 | + - name: Test suite reports artifacts |
| 168 | + if: ${{ always() }} |
| 169 | + uses: actions/upload-artifact@v6 |
| 170 | + with: |
| 171 | + name: bot_gpu_test_reports |
| 172 | + path: reports |
| 173 | + |
| 174 | + report: |
| 175 | + name: Report status |
| 176 | + needs: [gate, gpu_tests] |
| 177 | + # Always run so the comment is updated on success, failure, or cancellation — |
| 178 | + # but only if `gate` actually posted a comment to update. |
| 179 | + if: ${{ always() && needs.gate.outputs.comment_id != '' }} |
| 180 | + runs-on: ubuntu-22.04 |
| 181 | + permissions: |
| 182 | + pull-requests: write |
| 183 | + steps: |
| 184 | + - name: Update comment with final status |
| 185 | + env: |
| 186 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 187 | + REPO: ${{ github.repository }} |
| 188 | + CID: ${{ needs.gate.outputs.comment_id }} |
| 189 | + RESULT: ${{ needs.gpu_tests.result }} |
| 190 | + PYTEST_ARGS: ${{ needs.gate.outputs.pytest_args }} |
| 191 | + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} |
| 192 | + run: | |
| 193 | + case "$RESULT" in |
| 194 | + success) EMOJI="✅"; MSG="passed";; |
| 195 | + failure) EMOJI="❌"; MSG="failed";; |
| 196 | + cancelled) EMOJI="⚠️"; MSG="was cancelled";; |
| 197 | + *) EMOJI="⚠️"; MSG="did not run (${RESULT})";; |
| 198 | + esac |
| 199 | + BODY="${EMOJI} \`pytest ${PYTEST_ARGS}\` ${MSG} on GPU — [view logs](${RUN_URL})." |
| 200 | + gh api -X PATCH "repos/${REPO}/issues/comments/${CID}" -f body="$BODY" |
0 commit comments