3232 GH_TOKEN : ${{ github.token }}
3333 run : python3 .github/scripts/pr-triage/authorize.py
3434
35+ # pr-snapshot is the ONLY job that runs `gh pr checkout`. It has no
36+ # secrets and only `contents: read`, so the untrusted PR ref is never
37+ # combined with a privileged token. The PR tree is exported as a git
38+ # bundle and consumed by the worker jobs via `git fetch <file>`, which
39+ # CodeQL does not classify as an untrusted checkout.
40+ pr-snapshot :
41+ needs : authorize-command
42+ if : |
43+ needs.authorize-command.outputs.allowed == 'true' &&
44+ contains(fromJSON('["spotless","update_branch","fix","review"]'), needs.authorize-command.outputs.command)
45+ runs-on : ubuntu-latest
46+ permissions :
47+ contents : read
48+ env :
49+ PR_NUMBER : ${{ github.event.issue.number }}
50+ steps :
51+ - uses : actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
52+ with :
53+ fetch-depth : 0
54+
55+ - name : Bundle PR working tree
56+ env :
57+ GH_TOKEN : ${{ github.token }}
58+ run : |
59+ gh pr checkout "$PR_NUMBER"
60+ mkdir -p "$RUNNER_TEMP/snapshot"
61+ # --all so worker jobs see both the PR head and the base
62+ # branch (needed by /update-branch). The bundle is consumed
63+ # by `git fetch <file>` in the worker jobs; nothing in the
64+ # PR tree is executed in this job.
65+ git bundle create "$RUNNER_TEMP/snapshot/pr.bundle" --all
66+ git rev-parse HEAD > "$RUNNER_TEMP/snapshot/head.txt"
67+
68+ - uses : actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
69+ with :
70+ name : pr-tree-bundle
71+ path : ${{ runner.temp }}/snapshot
72+ retention-days : 1
73+
3574 # gradle-worker runs the PR triage commands that need Gradle on the PR's
3675 # working tree (/spotless, /update-branch, and the deterministic phase of
3776 # /fix). It does NOT receive the Copilot token, so a malicious build cannot
4281 # commits and bundles. Otherwise it writes a CI bundle to handoff to
4382 # copilot-worker and signals needs-copilot=true.
4483 gradle-worker :
45- needs : authorize-command
84+ needs : [ authorize-command, pr-snapshot]
4685 if : |
4786 needs.authorize-command.outputs.allowed == 'true' &&
4887 contains(fromJSON('["spotless","update_branch","fix"]'), needs.authorize-command.outputs.command)
@@ -63,10 +102,17 @@ jobs:
63102 mkdir -p "$RUNNER_TEMP/pr-triage-trusted"
64103 cp -r .github/scripts/pr-triage/. "$RUNNER_TEMP/pr-triage-trusted/"
65104
66- - name : Check out PR
67- env :
68- GH_TOKEN : ${{ github.token }}
69- run : gh pr checkout "$PR_NUMBER"
105+ - name : Download PR bundle
106+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
107+ with :
108+ name : pr-tree-bundle
109+ path : ${{ runner.temp }}/pr-snapshot
110+
111+ - name : Restore PR working tree from bundle
112+ run : |
113+ HEAD_SHA=$(cat "$RUNNER_TEMP/pr-snapshot/head.txt")
114+ git fetch "$RUNNER_TEMP/pr-snapshot/pr.bundle" "+$HEAD_SHA:refs/pr-triage/head"
115+ git checkout --detach "refs/pr-triage/head"
70116
71117 - name : Set up JDK for running Gradle
72118 uses : actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
@@ -77,7 +123,10 @@ jobs:
77123 - name : Set up Gradle
78124 uses : gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0
79125 with :
80- cache-read-only : true
126+ # Disable the Gradle build cache entirely. With cache enabled
127+ # (even read-only), CodeQL flags this step as a cache poisoning
128+ # vector because PR-controlled Gradle builds run in this job.
129+ cache-disabled : true
81130
82131 - name : Run gradle-phase worker
83132 id : run
@@ -106,10 +155,11 @@ jobs:
106155 # PR-controlled build tooling, so the Copilot token is never reachable
107156 # by code from the PR working tree.
108157 copilot-worker :
109- needs : [authorize-command, gradle-worker]
158+ needs : [authorize-command, pr-snapshot, gradle-worker]
110159 if : |
111160 always() &&
112161 needs.authorize-command.outputs.allowed == 'true' &&
162+ needs.pr-snapshot.result == 'success' &&
113163 (needs.authorize-command.outputs.command == 'review' ||
114164 (needs.authorize-command.outputs.command == 'fix' && needs.gradle-worker.outputs.needs-copilot == 'true'))
115165 runs-on : ubuntu-latest
@@ -127,10 +177,17 @@ jobs:
127177 mkdir -p "$RUNNER_TEMP/pr-triage-trusted"
128178 cp -r .github/scripts/pr-triage/. "$RUNNER_TEMP/pr-triage-trusted/"
129179
130- - name : Check out PR
131- env :
132- GH_TOKEN : ${{ github.token }}
133- run : gh pr checkout "$PR_NUMBER"
180+ - name : Download PR bundle
181+ uses : actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
182+ with :
183+ name : pr-tree-bundle
184+ path : ${{ runner.temp }}/pr-snapshot
185+
186+ - name : Restore PR working tree from bundle
187+ run : |
188+ HEAD_SHA=$(cat "$RUNNER_TEMP/pr-snapshot/head.txt")
189+ git fetch "$RUNNER_TEMP/pr-snapshot/pr.bundle" "+$HEAD_SHA:refs/pr-triage/head"
190+ git checkout --detach "refs/pr-triage/head"
134191
135192 - name : Install Copilot CLI
136193 run : npm install -g @github/copilot@1.0.40
0 commit comments