From cf925d51c7ce0c3d44c7186b2b2c37908ba51b5a Mon Sep 17 00:00:00 2001 From: Rajeev Jain Date: Wed, 8 Jul 2026 11:29:45 -0500 Subject: [PATCH] Allow forks to run benchmarks and comment Fork PRs get a read-only GITHUB_TOKEN, so the benchmark workflow's inline comment step fails with 403. Split into two workflows: the pull_request job runs benchmarks (read-only) and uploads results + PR number as an artifact; a new workflow_run job posts the comment from the default branch with the repo's write token. The comment job never checks out PR code, keeping the write token safe. Fixes #1547. --- .../workflows/asv-benchmarking-comment.yml | 82 +++++++++++++++++++ .github/workflows/asv-benchmarking-pr.yml | 63 +++----------- 2 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 .github/workflows/asv-benchmarking-comment.yml diff --git a/.github/workflows/asv-benchmarking-comment.yml b/.github/workflows/asv-benchmarking-comment.yml new file mode 100644 index 000000000..0b6befbd4 --- /dev/null +++ b/.github/workflows/asv-benchmarking-comment.yml @@ -0,0 +1,82 @@ +name: ASV Benchmarking (Comment) + +# Runs AFTER "ASV Benchmarking (PR)" completes. Because it is triggered by +# workflow_run, it executes from the default branch with the repository's own +# GITHUB_TOKEN (read-write), so it CAN post a comment even when the benchmark +# run came from a fork PR (where the token is read-only). It only downloads a +# results artifact and posts text -- it never checks out or executes PR/fork +# code, which keeps the write token safe. Fixes #1547. + +on: + workflow_run: + workflows: ["ASV Benchmarking (PR)"] + types: + - completed + +permissions: + issues: write + pull-requests: write + +jobs: + comment: + name: Post benchmark comment + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Download benchmark results artifact + uses: actions/download-artifact@v7 + with: + name: asv-benchmark-results-Linux + path: asv-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post or update result comment + uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + + // PR number and results were saved by the benchmark workflow. + const issue_number = parseInt( + fs.readFileSync('asv-results/pr_number.txt', 'utf8').trim(), 10); + const compareResults = fs.readFileSync( + 'asv-results/asv_compare_results.txt', 'utf8'); + const { owner, repo } = context.repo; + + const newComment = ` + ## ASV Benchmarking + +
+ Benchmark Comparison Results + + ${compareResults} +
+ `; + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + + const botComment = comments.find( + c => c.user.login === 'github-actions[bot]' + && c.body.includes('## ASV Benchmarking')); + + if (botComment) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: botComment.id, + body: newComment, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body: newComment, + }); + } diff --git a/.github/workflows/asv-benchmarking-pr.yml b/.github/workflows/asv-benchmarking-pr.yml index 5c23c85a4..cae4aa6f0 100644 --- a/.github/workflows/asv-benchmarking-pr.yml +++ b/.github/workflows/asv-benchmarking-pr.yml @@ -1,8 +1,12 @@ name: ASV Benchmarking (PR) +# This workflow runs untrusted fork code, so it only gets a read-only token and +# no write access. It must NOT post comments itself (forks get a read-only token +# -> 403). Instead it uploads the results + PR number as an artifact, and the +# separate asv-benchmarking-comment.yml (triggered via workflow_run) posts the +# comment with the repo's write token. See issue #1547. permissions: - issues: write - pull-requests: write + contents: read on: pull_request: @@ -55,6 +59,10 @@ jobs: asv compare --split ${{ github.event.pull_request.base.sha }} ${GITHUB_SHA} > asv_compare_results.txt working-directory: ${{ env.ASV_DIR }} + - name: Save PR number + if: always() + run: echo "${{ github.event.pull_request.number }}" > ${{ env.ASV_DIR }}/pr_number.txt + - uses: actions/upload-artifact@v7 if: always() with: @@ -62,53 +70,4 @@ jobs: path: | ${{ env.ASV_DIR }}/results ${{ env.ASV_DIR }}/asv_compare_results.txt - - - name: Post or update result comment - id: comment - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const compareResults = fs.readFileSync('${{ env.ASV_DIR }}/asv_compare_results.txt', 'utf8'); - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - - // Customize the comment content with your action results - const newComment = ` - ## ASV Benchmarking - -
- Benchmark Comparison Results - - ${compareResults} -
- `; - - // Fetch existing comments on the PR - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number, - }); - - // Find if there is an existing comment by this action - const botComment = comments.find(comment => comment.user.login === 'github-actions[bot]'); - - if (botComment) { - // Update the existing comment - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: botComment.id, - body: newComment, - }); - } else { - // Create a new comment - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: newComment, - }); - } + ${{ env.ASV_DIR }}/pr_number.txt