Skip to content

Commit 08f5d47

Browse files
committed
Address CodeQL alerts: extract pr-snapshot job, disable Gradle cache
1 parent f9a3c7f commit 08f5d47

2 files changed

Lines changed: 123 additions & 11 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# PR triage scripts
2+
3+
Scripts that implement the `/spotless`, `/update-branch`, `/fix`, and
4+
`/review` slash commands wired up by `.github/workflows/pr-triage-comments.yml`.
5+
6+
## Security model
7+
8+
Slash commands run on PRs that the repository owner does not necessarily
9+
control. The workflow splits into separate jobs so that no single job
10+
holds a privileged token AND executes PR-controlled code.
11+
12+
| Job | Entry point | Tokens visible | PR-controlled code allowed |
13+
| ---------------- | -------------------- | --------------------------------------------- | -------------------------- |
14+
| authorize-command| `authorize.py` | `GITHUB_TOKEN` | none (default-branch checkout only) |
15+
| pr-snapshot | inline | `GITHUB_TOKEN` | none — `gh pr checkout` + `git bundle`; PR tree never executed |
16+
| gradle-worker | `worker_gradle.py` | `GITHUB_TOKEN` | yes — runs `./gradlew` on PR tree |
17+
| copilot-worker | `worker_copilot.py` | `GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN` | only Copilot CLI editing files; never `./gradlew` or other build tools |
18+
| poster | `poster.py` | otelbot installation token | none — `git`/`gh` on the worker artifact only |
19+
20+
The PR working tree is checked out exactly once, in `pr-snapshot`, which
21+
holds no privileged secrets. It is exported as a git bundle and consumed
22+
by the worker jobs via `git fetch <bundle-file>`. This keeps `gh pr
23+
checkout` (which CodeQL flags as untrusted) out of any job that holds
24+
the Copilot token.
25+
26+
Invariants (see the comments at the top of each entry-point file):
27+
28+
* `gradle-worker` must never receive `COPILOT_GITHUB_TOKEN` or any other
29+
privileged token. Anything in the environment of `./gradlew` is reachable
30+
from a malicious PR build script.
31+
* `copilot-worker` must never invoke `./gradlew` or any other PR-controlled
32+
build tooling. PR build files would otherwise see `COPILOT_GITHUB_TOKEN`.
33+
* `poster` runs only trusted code snapshotted from the default branch
34+
(`$RUNNER_TEMP/pr-triage-trusted`). It never executes anything from the
35+
PR working tree, so `gh pr checkout` is safe even though the otelbot
36+
token is in scope.
37+
* `/fix` hands off between the two workers via a CI bundle written to
38+
`out_dir/ci-bundle/` plus a `needs-copilot.txt` marker. The
39+
copilot-worker downloads that artifact and runs Copilot on the bundle;
40+
it never re-runs Gradle.
41+
42+
## Files
43+
44+
* `authorize.py` — entry point for the `authorize-command` job.
45+
* `worker_gradle.py` — entry point for the `gradle-worker` job.
46+
* `worker_copilot.py` — entry point for the `copilot-worker` job.
47+
* `poster.py` — entry point for the `poster` job.
48+
* `triage_helpers.py` — shared helpers for parsing the issue_comment
49+
event, posting comments, and running sub-scripts. No tooling that
50+
would weaken the role split lives here.
51+
* `common.py` — shared helpers for the per-command sub-scripts
52+
(`spotless.py`, `update_branch.py`, `fix.py`, `review.py`).
53+
* `spotless.py`, `update_branch.py`, `fix.py`, `review.py` — the
54+
per-command implementations invoked by `worker_gradle.py` /
55+
`worker_copilot.py`.

.github/workflows/pr-triage-comments.yml

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,45 @@ jobs:
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
@@ -42,7 +81,7 @@ jobs:
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

Comments
 (0)