Skip to content

Commit e4e112e

Browse files
zoomote[bot]roomoteedelauna
authored
[Fix] Review-state labels missing on new PRs because GitHub rejects the label workflow (#2088)
* fix(ci): move fork review follow-up out of label-pr-review-state so GitHub accepts the workflow GitHub rejects a workflow that lists its own name in workflow_run.workflows, so no schedule, pull_request_target, issue_comment, or workflow_run runs of 'Label PR review state' started since PR #2049 merged. - Remove 'Label PR review state' from its own workflow_run.workflows list. - Remove the workflow_run.name loop-guard clause from the resolve and reconcile job if conditions and from the resolve script. - Move the privileged fork pull_request_review follow-up (issue #2046) into a new workflow, label-pr-review-state-fork-review-followup.yml, which listens for completed 'Label PR review state' runs, keeps the one-hop loop guard on workflow_run.event == 'pull_request_review', resolves fork PRs by exact head owner, branch, and SHA, and dispatches the reconciliation workflow's existing workflow_dispatch entrypoint. - Add workflow-self-listen.test.ts, which fails when any workflow lists its own name in workflow_run.workflows (actionlint does not catch this), and wire it plus the new follow-up workflow tests into the test:workflow suite. Closes #2086 * fix(ci): handle dispatch failures and widen follow-up tests --------- Co-authored-by: Roomote <roomote@roomote.dev> Co-authored-by: Elliott de Launay <edelauna@gmail.com>
1 parent 8dd5533 commit e4e112e

7 files changed

Lines changed: 371 additions & 147 deletions
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
name: Label PR review state fork review follow-up
2+
3+
on:
4+
# Fork pull_request_review runs of "Label PR review state" get a read-only
5+
# token and skip their label writes (issue #2046). When such a run
6+
# completes, this privileged follow-up resolves the fork PRs and dispatches
7+
# a workflow_dispatch run of "Label PR review state", which reconciles them
8+
# and writes the computed labels. The follow-up lives in its own workflow
9+
# because GitHub rejects a workflow that lists itself in
10+
# workflow_run.workflows (issue #2086). It takes no artifact input and
11+
# never checks out code, so no untrusted PR code or artifacts are touched.
12+
workflow_run:
13+
workflows: ["Label PR review state"]
14+
types: [completed]
15+
16+
permissions:
17+
# This privileged workflow only reads PR metadata and dispatches the
18+
# reconciliation workflow. All unspecified permissions, including contents,
19+
# are none; no fork code or configuration is checked out or executed.
20+
actions: write
21+
pull-requests: read
22+
23+
# Each completed run gets its own group: one run can resolve several PRs, and
24+
# a shared key would let an unrelated completion supersede queued dispatches.
25+
concurrency:
26+
group: label-pr-review-state-fork-review-followup-${{ github.event.workflow_run.id }}
27+
cancel-in-progress: false
28+
29+
jobs:
30+
dispatch:
31+
name: Zoo Code / dispatch fork review reconciliation
32+
# Loop guard (issue #2046): follow up only on runs triggered by
33+
# pull_request_review. The runs this workflow dispatches have event
34+
# 'workflow_dispatch', so their completion is filtered out here and
35+
# the chain stops after one hop.
36+
if: github.event.workflow_run.event == 'pull_request_review'
37+
runs-on: ubuntu-latest
38+
steps:
39+
- name: Resolve fork PRs and dispatch reconciliation
40+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
41+
with:
42+
retries: 3
43+
script: |
44+
const { owner, repo } = context.repo;
45+
const run = context.payload.workflow_run;
46+
47+
// Mirrors the job-level `if`: only runs triggered by
48+
// pull_request_review need a follow-up, so the chain
49+
// stops after one hop.
50+
if (run?.event !== 'pull_request_review') {
51+
core.info(`Ignoring workflow_run with event '${run?.event}'`);
52+
return;
53+
}
54+
55+
// Same-repository review runs already ran with a privileged
56+
// token and wrote their labels; only fork runs need the
57+
// follow-up.
58+
if (run.head_repository && run.head_repository.fork === false) {
59+
core.info('Ignoring same-repository pull_request_review run');
60+
return;
61+
}
62+
63+
// Resolve which PRs the completed run targeted. Fork runs
64+
// backfill pull_requests empty, so fall back to the exact head
65+
// owner, branch, and SHA lookup used by the resolve job of
66+
// label-pr-review-state.yml. Keep both copies in sync.
67+
let prNumbers = [];
68+
if (run.pull_requests?.length > 0) {
69+
prNumbers = run.pull_requests.map(pr => pr.number);
70+
} else {
71+
const headOwner = run.head_repository?.owner?.login;
72+
if (headOwner && run.head_branch && run.head_sha) {
73+
const candidates = await github.paginate(github.rest.pulls.list, {
74+
owner,
75+
repo,
76+
state: 'open',
77+
head: `${headOwner}:${run.head_branch}`,
78+
per_page: 100,
79+
});
80+
const baseRepository = `${owner}/${repo}`.toLowerCase();
81+
prNumbers = candidates
82+
.filter(pr =>
83+
pr.head?.sha === run.head_sha &&
84+
pr.base?.repo?.full_name?.toLowerCase() === baseRepository
85+
)
86+
.map(pr => pr.number);
87+
core.info(
88+
`Resolved workflow_run ${run.id ?? '(unknown)'} to ` +
89+
`${prNumbers.length} PR(s) by exact head owner, branch, and SHA`
90+
);
91+
}
92+
}
93+
94+
// Each dispatch starts a privileged workflow_dispatch run of the
95+
// reconciliation workflow for one PR. Its completion re-triggers
96+
// this workflow, but the loop guard above filters it out.
97+
// A failed dispatch must not skip the PRs after it: collect the
98+
// failures and fail the step once, naming each PR.
99+
const failures = [];
100+
for (const prNumber of prNumbers) {
101+
try {
102+
await github.rest.actions.createWorkflowDispatch({
103+
owner,
104+
repo,
105+
workflow_id: 'label-pr-review-state.yml',
106+
ref: 'main',
107+
inputs: { pull_request_number: String(prNumber) },
108+
});
109+
core.info(`Dispatched label-pr-review-state.yml for PR #${prNumber}`);
110+
} catch (error) {
111+
failures.push(`#${prNumber}: ${error.message}`);
112+
}
113+
}
114+
if (failures.length > 0) {
115+
core.setFailed(`Failed to dispatch ${failures.length} PR(s): ${failures.join('; ')}`);
116+
}

‎.github/workflows/label-pr-review-state.yml‎

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,11 @@ on:
3838
issue_comment:
3939
types: [created, edited]
4040
# Fork pull_request_review runs get a read-only token and skip their label
41-
# writes. This workflow lists itself so such a run's completion re-triggers
42-
# it with a privileged token that writes the computed labels (issue #2046).
43-
# The resolve job accepts a workflow_run of this workflow only when the
44-
# completed run was itself a pull_request_review run, so the chain stops
45-
# after one hop. The privileged follow-up takes no artifact input and never
46-
# checks out code, so no untrusted PR code or artifacts are touched.
41+
# writes (issue #2046). The privileged follow-up for those runs lives in
42+
# label-pr-review-state-fork-review-followup.yml: GitHub rejects a workflow
43+
# that lists itself in workflow_run.workflows (issue #2086).
4744
workflow_run:
48-
workflows:
49-
[
50-
"Code QA Roo Code",
51-
"E2E Tests (Mocked)",
52-
"Webview Visual Regression",
53-
"CodeQL Advanced",
54-
"Label PR review state",
55-
]
45+
workflows: ["Code QA Roo Code", "E2E Tests (Mocked)", "Webview Visual Regression", "CodeQL Advanced"]
5646
types: [completed]
5747

5848
permissions:
@@ -104,17 +94,9 @@ concurrency:
10494
jobs:
10595
resolve:
10696
name: Zoo Code / resolve PRs to reconcile
107-
# The second clause is the loop guard for the self workflow_run
108-
# follow-up (issue #2046): accept a workflow_run of this workflow only
109-
# when the completed run was triggered by pull_request_review. A run
110-
# triggered by workflow_run has event 'workflow_run', so the next level
111-
# is filtered out and the chain stops after one hop.
11297
if: >-
113-
(github.event_name != 'issue_comment' ||
114-
(github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]')) &&
115-
(github.event_name != 'workflow_run' ||
116-
github.event.workflow_run.name != 'Label PR review state' ||
117-
github.event.workflow_run.event == 'pull_request_review')
98+
github.event_name != 'issue_comment' ||
99+
(github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]')
118100
runs-on: ubuntu-latest
119101
outputs:
120102
pr_numbers: ${{ steps.resolve.outputs.pr_numbers }}
@@ -152,27 +134,12 @@ jobs:
152134
return;
153135
}
154136
155-
// Loop guard for the self workflow_run follow-up (issue #2046),
156-
// mirroring the job-level `if`: accept a workflow_run of this
157-
// workflow only when the completed run was triggered by
158-
// pull_request_review. A run triggered by workflow_run has event
159-
// 'workflow_run', so the chain stops after one hop.
160-
if (context.eventName === 'workflow_run' &&
161-
context.payload.workflow_run?.name === 'Label PR review state' &&
162-
context.payload.workflow_run?.event !== 'pull_request_review') {
163-
core.info(
164-
'Ignoring workflow_run of this workflow not triggered by a ' +
165-
'pull_request_review run'
166-
);
167-
core.setOutput('pr_numbers', '[]');
168-
return;
169-
}
170-
171137
// Resolve which PRs this event targets. Single-PR events carry the
172138
// number in the payload; sweep triggers (hourly schedule, pushes to
173139
// main) list every open PR; workflow_run events use the run's
174140
// associated PRs, with a fork-safe backfill by exact head owner,
175-
// branch, and SHA when GitHub left pull_requests empty.
141+
// branch, and SHA when GitHub left pull_requests empty. Keep the
142+
// backfill in sync with label-pr-review-state-fork-review-followup.yml.
176143
let prNumbers = [];
177144
if (context.payload.pull_request?.number) {
178145
prNumbers = [context.payload.pull_request.number];
@@ -238,9 +205,6 @@ jobs:
238205
if: >-
239206
(github.event_name != 'issue_comment' ||
240207
(github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]')) &&
241-
(github.event_name != 'workflow_run' ||
242-
github.event.workflow_run.name != 'Label PR review state' ||
243-
github.event.workflow_run.event == 'pull_request_review') &&
244208
needs.resolve.outputs.pr_numbers != '[]'
245209
strategy:
246210
fail-fast: false

‎src/eslint.config.mjs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@ export default [
5858
// Tests inside __tests__/ directories must not traverse 3+ levels above
5959
// the test file. That escapes the src/ package root and makes Turbo
6060
// invisible to cache changes in those files.
61-
// pr-review-state-workflow.test.ts and stale-pr-clock-workflow.test.ts are the
62-
// intentional exceptions: they read root-level config and run in a dedicated
63-
// non-Turbo CI step.
61+
// The workflow test files below are the intentional exceptions: they read
62+
// root-level config and run in a dedicated non-Turbo CI step.
6463
files: ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.spec.ts"],
6564
ignores: [
6665
"**/services/__tests__/pr-review-state-workflow.test.ts",
6766
"**/services/__tests__/stale-pr-clock-workflow.test.ts",
67+
"**/services/__tests__/fork-review-followup-workflow.test.ts",
68+
"**/services/__tests__/workflow-self-listen.test.ts",
6869
],
6970
rules: {
7071
"no-restricted-syntax": [

0 commit comments

Comments
 (0)