diff --git a/.github/workflows/pr_comment_gpu_tests.yml b/.github/workflows/bot_pytest.yml similarity index 58% rename from .github/workflows/pr_comment_gpu_tests.yml rename to .github/workflows/bot_pytest.yml index d4b44f41e92c..924c6dbd6f66 100644 --- a/.github/workflows/pr_comment_gpu_tests.yml +++ b/.github/workflows/bot_pytest.yml @@ -1,77 +1,24 @@ -name: GPU Tests from PR Comment - -# Lets maintainers (admin / write access) run GPU tests on a PR by commenting: -# /diffusers-bot pytest -# e.g. `/diffusers-bot pytest tests/models/test_modeling_common.py -k "some_test"`. +name: "Bot: Pytest" +# Reusable workflow called by diffusers_bot.yml for `/diffusers-bot pytest `. +# Runs pytest on a GPU runner and reports the result back on the PR. on: - issue_comment: - types: [created] - -# Default to read-only; jobs that comment opt into `pull-requests: write` explicitly. -permissions: - contents: read - -concurrency: - # A newer command on the same PR supersedes an in-flight one. - group: diffusers-bot-${{ github.event.issue.number }} - cancel-in-progress: true - -env: - DIFFUSERS_IS_CI: yes - OMP_NUM_THREADS: 8 - MKL_NUM_THREADS: 8 - HF_XET_HIGH_PERFORMANCE: 1 - PYTEST_TIMEOUT: 600 - # Force version overrides across every `uv pip install`: pin the - # torch/torchvision/torchaudio set baked into the image so `-U` installs can't bump - # torch and break torchvision's C++ ABI. Re-written into the file in the install step. - UV_OVERRIDE: /tmp/uv-overrides.txt + workflow_call: + inputs: + pytest_args: + required: true + type: string jobs: gate: - name: Authorize & launch - # Only react to `/diffusers-bot pytest …` comments on open PRs. - if: | - github.event.issue.pull_request && - github.event.issue.state == 'open' && - startsWith(github.event.comment.body, '/diffusers-bot pytest') + name: Acknowledge runs-on: ubuntu-22.04 permissions: pull-requests: write outputs: - pytest_args: ${{ steps.parse.outputs.pytest_args }} comment_id: ${{ steps.comment.outputs.comment_id }} steps: - - name: Check commenter permission - id: auth - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - PERM=$(gh api "repos/${REPO}/collaborators/${COMMENTER}/permission" --jq '.permission' 2>/dev/null || echo "none") - echo "Commenter @${COMMENTER} has permission: ${PERM}" - if [[ "$PERM" == "admin" || "$PERM" == "write" ]]; then - echo "authorized=true" >> "$GITHUB_OUTPUT" - else - echo "authorized=false" >> "$GITHUB_OUTPUT" - fi - - - name: Reject unauthorized commenter - if: steps.auth.outputs.authorized != 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR: ${{ github.event.issue.number }} - COMMENTER: ${{ github.event.comment.user.login }} - run: | - gh api -X POST "repos/${REPO}/issues/${PR}/comments" \ - -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 - echo "::error::Only maintainers with write/admin access can run /diffusers-bot." - exit 1 - - name: Acknowledge with 👀 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -80,18 +27,6 @@ jobs: run: | gh api -X POST "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content="eyes" >/dev/null - - name: Parse pytest args - id: parse - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - # Use only the first line of the comment, strip the command prefix. - FIRST_LINE=$(printf '%s' "$COMMENT_BODY" | head -n1) - ARGS="${FIRST_LINE#/diffusers-bot pytest}" - # Trim surrounding whitespace/CR. - ARGS="$(printf '%s' "$ARGS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" - echo "pytest_args=${ARGS}" >> "$GITHUB_OUTPUT" - - name: Post "running" comment id: comment env: @@ -99,7 +34,7 @@ jobs: REPO: ${{ github.repository }} PR: ${{ github.event.issue.number }} COMMENTER: ${{ github.event.comment.user.login }} - PYTEST_ARGS: ${{ steps.parse.outputs.pytest_args }} + PYTEST_ARGS: ${{ inputs.pytest_args }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | BODY="⏳ Running \`pytest ${PYTEST_ARGS}\` on a GPU runner — [view logs](${RUN_URL}). @@ -108,9 +43,14 @@ jobs: CID=$(gh api -X POST "repos/${REPO}/issues/${PR}/comments" -f body="$BODY" --jq '.id') echo "comment_id=${CID}" >> "$GITHUB_OUTPUT" - gpu_tests: + gpu: name: Run pytest on GPU needs: gate + # A newer command on the same PR supersedes an in-flight one. Scoped to this job + # only so the superseded run's `report` still updates its comment. + concurrency: + group: diffusers-bot-pytest-${{ github.event.issue.number }} + cancel-in-progress: true runs-on: group: aws-g4dn-2xlarge container: @@ -120,6 +60,16 @@ jobs: # write token. Comment writes happen only in `gate`/`report`. permissions: contents: read + env: + DIFFUSERS_IS_CI: yes + OMP_NUM_THREADS: 8 + MKL_NUM_THREADS: 8 + HF_XET_HIGH_PERFORMANCE: 1 + PYTEST_TIMEOUT: 600 + # Force version overrides across every `uv pip install`: pin the + # torch/torchvision/torchaudio set baked into the image so `-U` installs can't bump + # torch and break torchvision's C++ ABI. Re-written into the file in the install step. + UV_OVERRIDE: /tmp/uv-overrides.txt defaults: run: shell: bash @@ -154,7 +104,7 @@ jobs: CUBLAS_WORKSPACE_CONFIG: :16:8 # Forwarded via env (not interpolated into the script) to avoid breakage on # quotes/special characters in a legitimate command. - PYTEST_ARGS: ${{ needs.gate.outputs.pytest_args }} + PYTEST_ARGS: ${{ inputs.pytest_args }} run: | eval "pytest --make-reports=tests_bot_gpu $PYTEST_ARGS" @@ -173,7 +123,7 @@ jobs: report: name: Report status - needs: [gate, gpu_tests] + needs: [gate, gpu] # Always run so the comment is updated on success, failure, or cancellation — # but only if `gate` actually posted a comment to update. if: ${{ always() && needs.gate.outputs.comment_id != '' }} @@ -186,8 +136,8 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} CID: ${{ needs.gate.outputs.comment_id }} - RESULT: ${{ needs.gpu_tests.result }} - PYTEST_ARGS: ${{ needs.gate.outputs.pytest_args }} + RESULT: ${{ needs.gpu.result }} + PYTEST_ARGS: ${{ inputs.pytest_args }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | case "$RESULT" in diff --git a/.github/workflows/serge_review.yml b/.github/workflows/bot_review.yml similarity index 69% rename from .github/workflows/serge_review.yml rename to .github/workflows/bot_review.yml index 9f23ac8c72f8..512e96fd104b 100644 --- a/.github/workflows/serge_review.yml +++ b/.github/workflows/bot_review.yml @@ -1,14 +1,17 @@ -name: Claude AI Review with inline comments +name: "Bot: Review" -# Instead of running the ai-reviewer GitHub Action inline, this workflow acts as -# a thin, VPN-side relay to the Serge GitHub App hosted at +# Reusable workflow called by diffusers_bot.yml for `/diffusers-bot review`. +# +# A thin, VPN-side relay to the Serge GitHub App hosted at # https://serge.huggingface.tech/. The App's /webhook endpoint sits behind a VPN # that GitHub's own webhook delivery cannot reach, so a runner inside the VPN # re-delivers the triggering comment event to the App. # # The relay reproduces a genuine GitHub App webhook delivery: # - body: the original event payload with `installation.id` injected (the App -# needs it to mint an installation token; Actions payloads omit it) +# needs it to mint an installation token; Actions payloads omit it) and the +# `/diffusers-bot review` mention rewritten to the `@askserge` mention the +# App matches on # - X-Hub-Signature-256: HMAC-SHA256 of that exact body using the App's # webhook secret (verified at webapp.py:_verify_webhook_signature) # - X-GitHub-Event: the original event name (issue_comment / pull_request_review_comment) @@ -17,34 +20,18 @@ name: Claude AI Review with inline comments # App identity, so this job needs no checkout and no write permissions. on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -permissions: - contents: read + workflow_call: + secrets: + SERGE_WEBHOOK_SECRET: + required: true + SERGE_INSTALLATION_ID: + required: true jobs: - forward-to-serge-app: - if: | - ( - github.event_name == 'issue_comment' && - github.event.issue.pull_request && - github.event.issue.state == 'open' && - contains(github.event.comment.body, '@askserge') && - (github.event.comment.author_association == 'MEMBER' || - github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'COLLABORATOR') - ) || ( - github.event_name == 'pull_request_review_comment' && - contains(github.event.comment.body, '@askserge') && - (github.event.comment.author_association == 'MEMBER' || - github.event.comment.author_association == 'OWNER' || - github.event.comment.author_association == 'COLLABORATOR') - ) + relay: + name: Relay to Serge concurrency: - group: claude-ai-review-${{ github.event.issue.number || github.event.pull_request.number }} + group: diffusers-bot-review-${{ github.event.issue.number || github.event.pull_request.number }} cancel-in-progress: false runs-on: group: aws-general-8-plus @@ -71,11 +58,11 @@ jobs: exit 1 fi - # Inject installation.id into the original event payload, compact form. + # Inject installation.id and translate the mention, compact form. # The signed bytes and the POSTed bytes must be byte-identical, so we # write the body to a file and reuse it for both the HMAC and the POST. jq -c --argjson iid "${INSTALLATION_ID}" \ - '. + {installation: {id: $iid}}' \ + '. + {installation: {id: $iid}} | .comment.body |= sub("/diffusers-bot review"; "@askserge")' \ "${GITHUB_EVENT_PATH}" > payload.json SIG="sha256=$(openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" payload.json | awk '{print $NF}')" diff --git a/.github/workflows/bot_style.yml b/.github/workflows/bot_style.yml new file mode 100644 index 000000000000..177ac6b4cf96 --- /dev/null +++ b/.github/workflows/bot_style.yml @@ -0,0 +1,305 @@ +name: "Bot: Style" + +# Reusable workflow called by diffusers_bot.yml for `/diffusers-bot style`. +# Runs `make style && make quality` on the PR branch, then pushes the fixes. +# +# Security model: the `run` job checks out and executes untrusted fork code with +# a read-only token. Style fixes are exported as a patch artifact. The `push` +# job re-validates the PR head SHA, downloads the patch, and pushes via a +# GitHub App token — never exposing write credentials to untrusted code. + +on: + workflow_call: + secrets: + HF_BOT_STYLE_APP_ID: + required: true + HF_BOT_STYLE_SECRET_PEM: + required: true + +jobs: + run: + name: Run style + runs-on: ubuntu-latest + permissions: + pull-requests: read + contents: read + outputs: + headRepoFullName: ${{ steps.pr_info.outputs.headRepoFullName }} + headRef: ${{ steps.pr_info.outputs.headRef }} + headSha: ${{ steps.pr_info.outputs.headSha }} + prNumber: ${{ steps.pr_info.outputs.prNumber }} + steps: + - name: Extract PR details + id: pr_info + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const prNumber = context.payload.issue.number; + console.log(`PR number: "${prNumber}"`); + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + core.setOutput('headRepoFullName', pr.head.repo.full_name); + core.setOutput('headRef', pr.head.ref); + core.setOutput('headSha', pr.head.sha); + core.setOutput('baseRef', pr.base.ref); + core.setOutput('prNumber', prNumber); + + console.log('PR Details:', { + number: prNumber, + headRepo: pr.head.repo.full_name, + headRef: pr.head.ref, + headSha: pr.head.sha, + baseRef: pr.base.ref + }); + + - name: Check out PR branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ steps.pr_info.outputs.headRepoFullName }} + ref: ${{ steps.pr_info.outputs.headSha }} + fetch-depth: 0 + persist-credentials: false + + - name: Verify checked-out SHA + env: + HEAD_SHA: ${{ steps.pr_info.outputs.headSha }} + run: | + if [ "$(git rev-parse HEAD)" != "$HEAD_SHA" ]; then + echo "❌ Checked-out SHA does not match expected HEAD SHA! Abort!"; + exit 1; + fi + + - name: Verify PR head SHA is unchanged before running untrusted code + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + EXPECTED_HEAD_SHA: ${{ steps.pr_info.outputs.headSha }} + with: + script: | + const prNumber = context.payload.issue.number; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + if (pr.head.sha !== process.env.EXPECTED_HEAD_SHA) { + core.setFailed( + `❌ PR head changed during the workflow bootstrap. Expected ${process.env.EXPECTED_HEAD_SHA}, got ${pr.head.sha}. Aborting.` + ); + } + + - name: Check commit was pushed before the triggering comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + HEAD_SHA: ${{ steps.pr_info.outputs.headSha }} + HEAD_REPO_FULL_NAME: ${{ steps.pr_info.outputs.headRepoFullName }} + COMMENT_DATE: ${{ github.event.comment.created_at }} + with: + script: | + const headSha = process.env.HEAD_SHA; + const [headOwner, headRepo] = process.env.HEAD_REPO_FULL_NAME.split('/'); + const commentTimestamp = new Date(process.env.COMMENT_DATE).getTime(); + + const query = ` + query($owner: String!, $repo: String!, $sha: GitObjectID!) { + repository(owner: $owner, name: $repo) { + object(oid: $sha) { + ... on Commit { + pushedDate + } + } + } + } + `; + const result = await github.graphql(query, { owner: headOwner, repo: headRepo, sha: headSha }); + + const pushedDate = result.repository.object?.pushedDate; + if (!pushedDate) { + core.warning(`⚠️ pushedDate unavailable for commit ${headSha} (likely created via API/web UI). Skipping timestamp check.`); + return; + } + + const pushTimestamp = new Date(pushedDate).getTime(); + console.log(`Push timestamp (server-side): ${pushedDate}, Comment date: ${process.env.COMMENT_DATE}`); + if (pushTimestamp >= commentTimestamp) { + core.setFailed(`❌ Commit ${headSha} was pushed at or after the /diffusers-bot style comment. Aborting.`); + } else { + console.log('✅ Push timestamp check passed.'); + } + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[quality]" + + - name: Run style and quality checks + run: make style && make quality + + - name: Export style fixes as patch artifact + env: + STYLE_BOT_OUTPUT_DIR: ${{ runner.temp }}/style-bot-output + run: | + mkdir -p "$STYLE_BOT_OUTPUT_DIR" + if [ -n "$(git status --porcelain)" ]; then + git add -A + git diff --cached --binary > "$STYLE_BOT_OUTPUT_DIR/style-fixes.patch" + echo "changes_present=true" > "$STYLE_BOT_OUTPUT_DIR/metadata.env" + else + echo "changes_present=false" > "$STYLE_BOT_OUTPUT_DIR/metadata.env" + fi + + - name: Upload style fixes artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: style-bot-output-${{ github.run_id }} + path: ${{ runner.temp }}/style-bot-output + if-no-files-found: error + + push: + name: Push style fixes + needs: run + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + steps: + - name: Generate bot token + id: generate_token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v1 + with: + app-id: ${{ secrets.HF_BOT_STYLE_APP_ID }} + private-key: ${{ secrets.HF_BOT_STYLE_SECRET_PEM }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + + - name: Re-validate PR head SHA before trusted steps + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + EXPECTED_HEAD_SHA: ${{ needs.run.outputs.headSha }} + with: + script: | + const prNumber = context.payload.issue.number; + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + if (pr.head.sha !== process.env.EXPECTED_HEAD_SHA) { + core.setFailed( + `❌ PR head changed after the untrusted style run. Expected ${process.env.EXPECTED_HEAD_SHA}, got ${pr.head.sha}. Aborting.` + ); + } + + - name: Check out reviewed PR SHA + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: ${{ needs.run.outputs.headRepoFullName }} + ref: ${{ needs.run.outputs.headSha }} + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + persist-credentials: false + + - name: Verify checked-out SHA + env: + HEAD_SHA: ${{ needs.run.outputs.headSha }} + run: | + if [ "$(git rev-parse HEAD)" != "$HEAD_SHA" ]; then + echo "❌ Checked-out SHA does not match expected HEAD SHA! Abort!" + exit 1 + fi + + - name: Download style fixes artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: style-bot-output-${{ github.run_id }} + path: ${{ runner.temp }}/style-bot-output + + - name: Comment on PR with workflow run link + id: init_comment + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + prNumber: ${{ needs.run.outputs.prNumber }} + with: + script: | + const prNumber = parseInt(process.env.prNumber, 10); + const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` + + const { data: botComment } = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: `Style fix is beginning .... [View the workflow run here](${runUrl}).` + }); + core.setOutput('comment_id', botComment.id); + + - name: Apply style fixes patch + id: apply_patch + env: + STYLE_BOT_OUTPUT_DIR: ${{ runner.temp }}/style-bot-output + run: | + if grep -qx 'changes_present=true' "$STYLE_BOT_OUTPUT_DIR/metadata.env"; then + git apply --binary "$STYLE_BOT_OUTPUT_DIR/style-fixes.patch" + echo "changes_present=true" >> $GITHUB_OUTPUT + else + echo "No changes to apply." + echo "changes_present=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + id: commit_and_push + env: + HEADREPOFULLNAME: ${{ needs.run.outputs.headRepoFullName }} + HEADREF: ${{ needs.run.outputs.headRef }} + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + CHANGES_PRESENT: ${{ steps.apply_patch.outputs.changes_present }} + run: | + echo "HEADREPOFULLNAME: $HEADREPOFULLNAME, HEADREF: $HEADREF" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # 'origin' must point at the contributor's fork so the push lands on the PR branch. + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/$HEADREPOFULLNAME.git" + + if [ "$CHANGES_PRESENT" = "true" ] && [ -n "$(git status --porcelain)" ]; then + git add . + git commit -m "Apply style fixes" + git -c lfs.locksverify=false push origin HEAD:$HEADREF + echo "changes_pushed=true" >> $GITHUB_OUTPUT + else + echo "No changes to commit." + echo "changes_pushed=false" >> $GITHUB_OUTPUT + fi + + - name: Prepare final comment message + id: prepare_final_comment + env: + CHANGES_PUSHED: ${{ steps.commit_and_push.outputs.changes_pushed }} + run: | + if [ "$CHANGES_PUSHED" = 'true' ]; then + echo "final_comment=Style bot fixed some files and pushed the changes." >> $GITHUB_OUTPUT + else + echo "final_comment=Style fix runs successfully without any file modified." >> $GITHUB_OUTPUT + fi + + - name: Comment on PR + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + env: + COMMENT_ID: ${{ steps.init_comment.outputs.comment_id }} + FINAL_COMMENT: ${{ steps.prepare_final_comment.outputs.final_comment }} + with: + script: | + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: parseInt(process.env.COMMENT_ID, 10), + body: process.env.FINAL_COMMENT + }); diff --git a/.github/workflows/diffusers_bot.yml b/.github/workflows/diffusers_bot.yml new file mode 100644 index 000000000000..15e26fae696f --- /dev/null +++ b/.github/workflows/diffusers_bot.yml @@ -0,0 +1,119 @@ +name: Diffusers Bot + +# Maintainer commands, triggered by commenting on an open PR: +# +# /diffusers-bot style run `make style && make quality` and push the fixes to the PR branch +# /diffusers-bot review request a Serge AI review (also works from an inline review comment) +# /diffusers-bot pytest run `pytest ` on a GPU runner, e.g. +# `/diffusers-bot pytest tests/models/test_modeling_common.py -k "some_test"` +# +# Auth is checked once in the `authorize` job. Each subcommand lives in its +# own reusable workflow file under .github/workflows/bot_*.yml. + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +# Default to read-only; calling jobs grant extra permissions to the sub-workflows +# that need them. +permissions: + contents: read + +jobs: + # ── Shared gate: authorize commenter & parse subcommand ────────────────────── + authorize: + name: Authorize + if: | + ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.issue.state == 'open' && + contains(github.event.comment.body, '/diffusers-bot') + ) || ( + github.event_name == 'pull_request_review_comment' && + contains(github.event.comment.body, '/diffusers-bot') + ) + runs-on: ubuntu-latest + permissions: + pull-requests: read + contents: read + outputs: + is_authorized: ${{ steps.check.outputs.is_authorized }} + subcommand: ${{ steps.check.outputs.subcommand }} + pytest_args: ${{ steps.check.outputs.pytest_args }} + steps: + - name: Authorize and parse command + id: check + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const commenter = context.payload.comment.user.login; + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: commenter + }); + const authorized = ['admin', 'write'].includes(permission.permission); + console.log(`User ${commenter}: permission=${permission.permission}, authorized=${authorized}`); + core.setOutput('is_authorized', authorized); + + const body = context.payload.comment.body; + const firstLine = body.split('\n')[0]; + + let subcommand = 'unknown'; + let pytestArgs = ''; + + if (firstLine.startsWith('/diffusers-bot pytest')) { + subcommand = 'pytest'; + pytestArgs = firstLine.replace('/diffusers-bot pytest', '').trim(); + } else if (body.includes('/diffusers-bot style')) { + subcommand = 'style'; + } else if (body.includes('/diffusers-bot review')) { + subcommand = 'review'; + } + + console.log(`Subcommand: ${subcommand}, pytestArgs: ${pytestArgs}`); + core.setOutput('subcommand', subcommand); + core.setOutput('pytest_args', pytestArgs); + + # ── Dispatch to sub-workflows ──────────────────────────────────────────────── + style: + needs: authorize + if: | + needs.authorize.outputs.subcommand == 'style' && + needs.authorize.outputs.is_authorized == 'true' && + github.event_name == 'issue_comment' + permissions: + contents: read + pull-requests: write + uses: ./.github/workflows/bot_style.yml + secrets: + HF_BOT_STYLE_APP_ID: ${{ secrets.HF_BOT_STYLE_APP_ID }} + HF_BOT_STYLE_SECRET_PEM: ${{ secrets.HF_BOT_STYLE_SECRET_PEM }} + + review: + needs: authorize + if: | + needs.authorize.outputs.subcommand == 'review' && + needs.authorize.outputs.is_authorized == 'true' + permissions: + contents: read + uses: ./.github/workflows/bot_review.yml + secrets: + SERGE_WEBHOOK_SECRET: ${{ secrets.SERGE_WEBHOOK_SECRET }} + SERGE_INSTALLATION_ID: ${{ secrets.SERGE_INSTALLATION_ID }} + + pytest: + needs: authorize + if: | + needs.authorize.outputs.subcommand == 'pytest' && + needs.authorize.outputs.is_authorized == 'true' && + github.event_name == 'issue_comment' + permissions: + contents: read + pull-requests: write + uses: ./.github/workflows/bot_pytest.yml + with: + pytest_args: ${{ needs.authorize.outputs.pytest_args }} diff --git a/.github/workflows/pr_style_bot.yml b/.github/workflows/pr_style_bot.yml deleted file mode 100644 index 8513e7609c48..000000000000 --- a/.github/workflows/pr_style_bot.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: PR Style Bot - -on: - issue_comment: - types: [created] - -permissions: - pull-requests: write - contents: read - -jobs: - style: - uses: huggingface/huggingface_hub/.github/workflows/style-bot-action.yml@e2867e92c07d15e1bf18994d0a945ef5ad6b8d65 - with: - python_quality_dependencies: "[quality]" - secrets: - app_id: ${{ secrets.HF_BOT_STYLE_APP_ID }} - app_private_key: ${{ secrets.HF_BOT_STYLE_SECRET_PEM }}