Skip to content

Commit a00d973

Browse files
committed
Fix fork-based pull request label checks
1 parent f3a7c41 commit a00d973

8 files changed

Lines changed: 657 additions & 73 deletions

.github/workflows/label-test.yml

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,24 @@ on:
1111
description: "Ref to check out from the central Label Sync repository."
1212
required: true
1313
type: string
14+
review_signal_run_id:
15+
description: "Review signal workflow run whose artifact identifies the pull request."
16+
required: false
17+
type: number
18+
default: 0
19+
review_signal_head_sha:
20+
description: "Pull request head SHA recorded by the review signal workflow run."
21+
required: false
22+
type: string
23+
default: ""
1424

15-
# The repository and pull request under test are read from the caller's context rather
16-
# than passed as inputs: inside a reusable workflow, github.repository resolves to the
17-
# calling repository and github.event is the caller's triggering event. Keeping them out
18-
# of the input contract means the distributed caller workflows never need to change.
25+
# The target repository and event are read from the caller's context: inside a reusable
26+
# workflow, github.repository and github.event describe the calling workflow. The review
27+
# artifact is untrusted input and is validated against the workflow run's recorded head
28+
# SHA before the configured PAT or GitHub App token reruns the authoritative policy run.
1929

2030
permissions:
31+
actions: read
2132
contents: read
2233
issues: read
2334
pull-requests: read
@@ -27,6 +38,15 @@ jobs:
2738
runs-on: ubuntu-latest
2839

2940
steps:
41+
- name: Download review context
42+
if: ${{ github.event_name == 'workflow_run' }}
43+
uses: actions/download-artifact@v8
44+
with:
45+
name: label-test-review-context
46+
path: ${{ runner.temp }}/label-test-review-context
47+
run-id: ${{ inputs.review_signal_run_id }}
48+
github-token: ${{ github.token }}
49+
3050
- name: Check out Label Sync repository
3151
uses: actions/checkout@v7
3252
with:
@@ -64,8 +84,18 @@ jobs:
6484
run: node scripts/create-github-auth-token.mjs
6585

6686
- name: Check PR labels and approvals
87+
if: ${{ github.event_name == 'pull_request_target' }}
6788
env:
6889
TARGET_REPOSITORY: ${{ github.repository }}
6990
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
7091
GITHUB_TOKEN: ${{ github.token }}
7192
run: node scripts/check-pr-label-policy.mjs
93+
94+
- name: Rerun authoritative Label Test
95+
if: ${{ github.event_name == 'workflow_run' }}
96+
env:
97+
GITHUB_TOKEN: ${{ github.token }}
98+
TARGET_REPOSITORY: ${{ github.repository }}
99+
PULL_REQUEST_NUMBER_FILE: ${{ runner.temp }}/label-test-review-context/pr-number.txt
100+
REVIEW_SIGNAL_HEAD_SHA: ${{ inputs.review_signal_head_sha }}
101+
run: node scripts/rerun-label-policy.mjs

.github/workflows/refresh-label-test.yml

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,18 @@ on:
1111
description: "Ref to check out from the central Label Sync repository."
1212
required: true
1313
type: string
14+
review_signal_run_id:
15+
description: "Unprivileged review workflow run containing the pull request number artifact."
16+
required: true
17+
type: number
18+
review_signal_head_sha:
19+
description: "Pull request head SHA recorded by GitHub for the review workflow run."
20+
required: true
21+
type: string
1422

15-
# The repository and pull request are read from the caller's context rather than passed
16-
# as inputs. See the equivalent note in label-test.yml.
23+
# The target repository comes from the caller's trusted workflow_run context. The pull
24+
# request number crosses the privilege boundary as untrusted artifact data and is checked
25+
# against review_signal_head_sha before the workflow uses elevated credentials.
1726

1827
permissions:
1928
actions: write
@@ -24,6 +33,14 @@ jobs:
2433
runs-on: ubuntu-latest
2534

2635
steps:
36+
- name: Download review context
37+
uses: actions/download-artifact@v8
38+
with:
39+
name: label-test-review-context
40+
path: ${{ runner.temp }}/label-test-review-context
41+
run-id: ${{ inputs.review_signal_run_id }}
42+
github-token: ${{ github.token }}
43+
2744
- name: Check out Label Sync repository
2845
uses: actions/checkout@v7
2946
with:
@@ -35,9 +52,35 @@ jobs:
3552
with:
3653
node-version: "24"
3754

55+
- name: Load properties
56+
id: properties
57+
env:
58+
GITHUB_REPOSITORY: ${{ inputs.label_sync_repository }}
59+
run: node scripts/export-properties.mjs
60+
61+
- name: Resolve PAT auth token
62+
id: pat_auth
63+
if: ${{ steps.properties.outputs.auth_mode == 'pat' }}
64+
env:
65+
AUTH_MODE: pat
66+
PAT_TOKEN: ${{ secrets[steps.properties.outputs.pat_token_secret_name] }}
67+
GITHUB_TOKEN: ${{ github.token }}
68+
run: node scripts/create-github-auth-token.mjs
69+
70+
- name: Resolve GitHub App auth token
71+
id: app_auth
72+
if: ${{ steps.properties.outputs.auth_mode == 'githubApp' }}
73+
env:
74+
AUTH_MODE: githubApp
75+
GITHUB_APP_ID: ${{ secrets[steps.properties.outputs.github_app_id_secret_name] }}
76+
GITHUB_APP_PRIVATE_KEY: ${{ secrets[steps.properties.outputs.github_app_private_key_secret_name] }}
77+
GITHUB_APP_INSTALLATION_ID: ${{ secrets[steps.properties.outputs.github_app_installation_id_secret_name] }}
78+
run: node scripts/create-github-auth-token.mjs
79+
3880
- name: Rerun authoritative Label Test
3981
env:
4082
GITHUB_TOKEN: ${{ github.token }}
4183
TARGET_REPOSITORY: ${{ github.repository }}
42-
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
84+
PULL_REQUEST_NUMBER_FILE: ${{ runner.temp }}/label-test-review-context/pr-number.txt
85+
REVIEW_SIGNAL_HEAD_SHA: ${{ inputs.review_signal_head_sha }}
4386
run: node scripts/rerun-label-policy.mjs

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,20 +192,20 @@ Behavior:
192192

193193
For team approval checks, the workflow token must be able to read the configured organization team membership. The same `properties.authentication` setup used by the label sync workflows is used for the reusable Label Test workflow.
194194

195-
The policy check runs on `pull_request_target` only, so it publishes exactly one check. Review submissions, edits, and dismissals do not create a second policy check. Instead, a separate review-refresh workflow reruns the latest completed policy run for that pull request, letting a new approval replace an earlier failed result on the same check.
195+
The policy job runs on `pull_request_target` only. Review submissions, edits, and dismissals are recorded by a separate unprivileged workflow, then the existing Label Test workflow handles its completion through `workflow_run` and reruns the latest completed policy run for that pull request. This lets a new approval replace an earlier failed result on the same required check.
196196

197-
`pull_request_review` always executes from the target repository's default branch, the pull request number comes from GitHub's event payload rather than contributor-controlled data, and the refresh job never checks out pull request code. It only calls the Actions rerun API, so granting it `actions: write` is safe for fork pull requests.
197+
Fork `pull_request_review` runs cannot access repository secrets or an `actions: write` token. The review workflow therefore only uploads the pull request number as a short-lived artifact. Its `workflow_run` continuation executes from the target repository's default branch with the configured authentication, downloads the artifact outside the workspace, and verifies the referenced pull request still has the head SHA recorded by GitHub for the review run before calling the Actions rerun API. Neither stage checks out or executes pull request code.
198198

199199
A pull request shows one check until someone submits a review, and two afterwards. The refresh workflow reacts to every review regardless of labels, because GitHub cannot filter a workflow trigger by pull request label, and narrowing it with a job-level condition would publish a permanently skipped check instead. Reducing this to a single check in all cases would require a GitHub App posting a check run through the Checks API rather than distributed workflows.
200200

201201
### 05 - Distribute-Label-Workflow
202202

203203
Run `05 - Distribute-Label-Workflow` manually to install or update the Label Test workflows in selected repositories. It writes these files in each selected target repository:
204204

205-
- `.github/workflows/label-test.yml`: the single required policy check, on `pull_request_target`
206-
- `.github/workflows/label-test-review-refresh.yml`: reruns the policy check after a review
205+
- `.github/workflows/label-test.yml`: the required policy check on `pull_request_target` and its privileged `workflow_run` continuation
206+
- `.github/workflows/label-test-review-refresh.yml`: records review context without secrets for the privileged continuation
207207

208-
The refresh implementation remains in the central Label-Sync repository as the reusable `Refresh Label Test` workflow. Distribution also removes the obsolete `.github/workflows/label-test-review-signal.yml` file if a target received the earlier artifact-handshake layout. The generated workflows call back to the repository and default branch that ran the distributor, so forks distribute callers that point to the fork.
208+
The refresh implementation remains in the central Label-Sync repository as the reusable `Refresh Label Test` workflow. Distribution also removes the obsolete `.github/workflows/label-test-review-signal.yml` filename if a target received that earlier layout; the signal now lives at `.github/workflows/label-test-review-refresh.yml`. The generated workflows call back to the repository and default branch that ran the distributor, so forks distribute callers that point to the fork.
209209

210210
Inputs:
211211

scripts/distribute-label-test-workflows.mjs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,19 @@ on:
127127
- labeled
128128
- unlabeled
129129
- ready_for_review
130+
workflow_run:
131+
workflows:
132+
- Label Test Review Refresh
133+
types:
134+
- completed
130135
131136
permissions:
132137
contents: read
133138
134139
jobs:
135140
label-test:
136141
permissions:
142+
actions: read
137143
contents: read
138144
checks: write
139145
issues: write
@@ -143,6 +149,8 @@ jobs:
143149
with:
144150
label_sync_repository: ${sourceRepository}
145151
label_sync_ref: ${sourceRef}
152+
review_signal_run_id: \${{ github.event.workflow_run.id || 0 }}
153+
review_signal_head_sha: \${{ github.event.workflow_run.head_sha || '' }}
146154
secrets: inherit
147155
`;
148156
}
@@ -164,18 +172,25 @@ permissions:
164172
contents: read
165173
166174
jobs:
167-
refresh-label-test:
168-
name: Refresh Label Test
169-
permissions:
170-
actions: write
171-
contents: read
172-
checks: write
173-
pull-requests: read
174-
uses: ${sourceRepository}/.github/workflows/refresh-label-test.yml@${sourceRef}
175-
with:
176-
label_sync_repository: ${sourceRepository}
177-
label_sync_ref: ${sourceRef}
178-
secrets: inherit
175+
record-label-test-review:
176+
name: Record Label Test Review
177+
runs-on: ubuntu-latest
178+
179+
steps:
180+
- name: Record pull request number
181+
env:
182+
PULL_REQUEST_NUMBER: \${{ github.event.pull_request.number }}
183+
run: |
184+
mkdir -p "$RUNNER_TEMP/label-test-review-context"
185+
printf '%s\\n' "$PULL_REQUEST_NUMBER" > "$RUNNER_TEMP/label-test-review-context/pr-number.txt"
186+
187+
- name: Upload review context
188+
uses: actions/upload-artifact@v7
189+
with:
190+
name: label-test-review-context
191+
path: \${{ runner.temp }}/label-test-review-context/pr-number.txt
192+
if-no-files-found: error
193+
retention-days: 1
179194
`;
180195
}
181196

0 commit comments

Comments
 (0)