From 8859c6d06d378beb7d530fea99d00f1c5072f43f Mon Sep 17 00:00:00 2001 From: Erik Berlin Date: Sun, 25 Oct 2026 18:30:01 -0700 Subject: [PATCH] Keep the Perf workflow's write access out of the Duel job The Duel job checks out and runs the pull request's code, benchmarks included, so it no longer has pull-requests: write, and its checkout keeps no credentials. It uploads its report, and a Comment job of its own, which checks nothing out and runs none of the branch's code, posts the comment. .duel.rb also watches the benchmarks, itself and the Perf workflow, so a pull request that changes only those is compared, instead of passing with nothing to compare. --- .duel.rb | 4 ++++ .github/workflows/perf.yml | 42 +++++++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/.duel.rb b/.duel.rb index 5c938d5..1798b3c 100644 --- a/.duel.rb +++ b/.duel.rb @@ -10,4 +10,8 @@ # The library each version's workers require (from the gemspec, unless # this says otherwise). # c.library = "multi_json" + + # Compare a change to the benchmarks, this file or the Perf workflow + # too, not only to lib/ and ext/. + c.watch += %w[duel .duel.rb .github/workflows/perf.yml] end diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml index 22ffbe0..1a03329 100644 --- a/.github/workflows/perf.yml +++ b/.github/workflows/perf.yml @@ -8,9 +8,10 @@ on: - cron: "0 4 * * *" workflow_dispatch: +# The Duel job runs the branch's code (its benchmarks too), so it only +# reads. Commenting is the Comment job's, which runs none of it. permissions: contents: read - pull-requests: write # to comment on a pull request with the report jobs: duel: @@ -20,6 +21,7 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 # the baseline comes from history, tags included + persist-credentials: false # (And whatever else your tests need set up: packages, services.) - uses: ruby/setup-ruby@v1 with: @@ -43,18 +45,44 @@ jobs: exit 0 fi bundle exec duel --markdown "$RUNNER_TEMP/duel.md" - # The report, as a comment on the pull request: one comment, updated - # on every push. (A pull request from a fork gets no comment, as its - # token can't write one: the job's summary has the report.) + # The report, for the Comment job. + - name: Keep the report + if: ${{ !cancelled() && github.event_name == 'pull_request' }} + uses: actions/upload-artifact@v7 + with: + name: duel-report + path: ${{ runner.temp }}/duel.md + if-no-files-found: ignore # none when Duel didn't get to compare + retention-days: 1 + + # The report, as a comment on the pull request: one comment, updated on + # every push. This job checks nothing out and runs none of the branch's + # code, so it alone can write. (A pull request from a fork gets no + # comment, as its token can't write one: the Duel job's summary has the + # report.) + comment: + name: Comment + needs: duel + if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + permissions: + pull-requests: write # to comment on the pull request with the report + steps: + - name: Get the report + id: report + continue-on-error: true # there's none when the Duel job failed before comparing + uses: actions/download-artifact@v8 + with: + name: duel-report + path: ${{ runner.temp }}/report - name: Comment on the pull request - if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} + if: steps.report.outcome == 'success' env: GH_TOKEN: ${{ github.token }} PR: ${{ github.event.pull_request.number }} run: | - [ -f "$RUNNER_TEMP/duel.md" ] || exit 0 marker="" - { echo "$marker"; cat "$RUNNER_TEMP/duel.md"; } > "$RUNNER_TEMP/comment.md" + { echo "$marker"; cat "$RUNNER_TEMP/report/duel.md"; } > "$RUNNER_TEMP/comment.md" ids=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \ --jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | .id") if [ -n "$ids" ]; then