Skip to content

Commit bbf2b66

Browse files
authored
Support workflow run to be able to auto-review PRs from forks (#116)
Signed-off-by: Lorena Rangel <lorena.rangel@docker.com>
1 parent 0f40129 commit bbf2b66

File tree

3 files changed

+176
-20
lines changed

3 files changed

+176
-20
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Lightweight trigger workflow for fork PR auto-reviews.
2+
# Runs in the fork's context (no secrets) on pull_request, saves the PR number
3+
# as an artifact, then completes so that self-review-pr.yml can fire via
4+
# workflow_run in the base repo context with full secret access.
5+
6+
name: PR Review Trigger
7+
8+
on:
9+
pull_request:
10+
types: [ready_for_review, opened]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
save-pr-metadata:
17+
# Only fork PRs — same-repo PRs are handled directly by self-review-pr.yml
18+
if: github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Save PR number
23+
env:
24+
PR_NUMBER: ${{ github.event.pull_request.number }}
25+
run: |
26+
mkdir -p pr-metadata
27+
echo "$PR_NUMBER" > pr-metadata/pr-number
28+
29+
- name: Upload PR metadata
30+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
31+
with:
32+
name: pr-review-trigger-metadata-${{ github.run_id }}
33+
path: pr-metadata/
34+
retention-days: 1

.github/workflows/review-pr.yml

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,30 +108,80 @@ jobs:
108108
# ==========================================================================
109109
# AUTOMATIC REVIEW FOR ORG MEMBERS
110110
# Triggers when a PR is marked ready for review or opened (non-draft)
111-
# Only runs for members of the configured org; same-repo branches only — fork PRs use /review command
112-
# Only runs for same-repo PRs (fork PRs don't have access to secrets with pull_request trigger)
113-
# Fork PRs use the /review command instead
111+
# Supports two trigger paths:
112+
# 1. pull_request event (same-repo branches only — fork PRs lack secret access)
113+
# 2. workflow_run event with inputs.pr-number set (fork PRs via workflow_run pattern)
114+
# Only runs for members of the configured org.
114115
# ==========================================================================
115116
auto-review:
116117
if: |
117-
github.event_name == 'pull_request' &&
118-
!github.event.pull_request.draft &&
119-
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
118+
(
119+
github.event_name == 'pull_request' &&
120+
!github.event.pull_request.draft &&
121+
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
122+
) || (
123+
github.event_name == 'workflow_run' &&
124+
inputs.pr-number != ''
125+
)
120126
runs-on: ubuntu-latest
121127
env:
122128
HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }}
123129
outputs:
124130
exit-code: ${{ steps.run-review.outputs.exit-code }}
125131

126132
steps:
133+
- name: Get PR number
134+
id: get-pr
135+
shell: bash
136+
env:
137+
INPUT_PR_NUMBER: ${{ inputs.pr-number }}
138+
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
139+
run: |
140+
echo "pr-number=${INPUT_PR_NUMBER:-$EVENT_PR_NUMBER}" >> $GITHUB_OUTPUT
141+
142+
# For workflow_run events, github.event.pull_request is unavailable — fetch PR author
143+
# and draft status via API using github.token (repo read access).
144+
# Skipped for pull_request events where the payload already has this info.
145+
- name: Fetch PR info
146+
id: pr-info
147+
if: github.event_name == 'workflow_run'
148+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
149+
env:
150+
PR_NUMBER: ${{ steps.get-pr.outputs.pr-number }}
151+
with:
152+
github-token: ${{ github.token }}
153+
script: |
154+
const { data: pr } = await github.rest.pulls.get({
155+
owner: context.repo.owner,
156+
repo: context.repo.repo,
157+
pull_number: parseInt(process.env.PR_NUMBER, 10)
158+
});
159+
core.setOutput('author', pr.user.login);
160+
core.setOutput('draft', pr.draft ? 'true' : 'false');
161+
127162
- name: Check if PR author is org member
128163
id: membership
129164
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
165+
env:
166+
PR_NUMBER: ${{ steps.get-pr.outputs.pr-number }}
167+
PR_DRAFT: ${{ steps.pr-info.outputs.draft }}
168+
PR_AUTHOR: ${{ steps.pr-info.outputs.author }}
130169
with:
131170
github-token: ${{ secrets.CAGENT_ORG_MEMBERSHIP_TOKEN }}
132171
script: |
133172
const org = '${{ inputs.auto-review-org }}';
134-
const username = context.payload.pull_request.user.login;
173+
174+
let username;
175+
if (context.eventName === 'workflow_run') {
176+
if (process.env.PR_DRAFT === 'true') {
177+
core.setOutput('is_member', 'false');
178+
console.log(`⏭️ PR #${process.env.PR_NUMBER} is a draft — skipping auto-review`);
179+
return;
180+
}
181+
username = process.env.PR_AUTHOR;
182+
} else {
183+
username = context.payload.pull_request.user.login;
184+
}
135185
136186
try {
137187
await github.rest.orgs.checkMembershipForUser({
@@ -157,13 +207,12 @@ jobs:
157207
}
158208
}
159209
160-
# With pull_request, checking out the PR head is standard behavior
161210
- name: Checkout PR head
162211
if: steps.membership.outputs.is_member == 'true'
163212
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
164213
with:
165214
fetch-depth: 0
166-
ref: refs/pull/${{ github.event.pull_request.number }}/head
215+
ref: refs/pull/${{ steps.get-pr.outputs.pr-number }}/head
167216

168217
# Generate GitHub App token for custom app identity (optional - falls back to github.token)
169218
- name: Generate GitHub App token
@@ -181,7 +230,7 @@ jobs:
181230
continue-on-error: true # Don't fail the calling workflow if the review errors
182231
uses: docker/cagent-action/review-pr@latest
183232
with:
184-
pr-number: ${{ inputs.pr-number || github.event.pull_request.number }}
233+
pr-number: ${{ steps.get-pr.outputs.pr-number }}
185234
additional-prompt: ${{ inputs.additional-prompt }}
186235
add-prompt-files: ${{ inputs.add-prompt-files }}
187236
model: ${{ inputs.model }}

.github/workflows/self-review-pr.yml

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,113 @@ on:
1212
types: [created]
1313
pull_request:
1414
types: [ready_for_review, opened]
15+
workflow_run:
16+
workflows: ["PR Review Trigger"]
17+
types: [completed]
1518

1619
permissions:
1720
contents: read
1821
pull-requests: write
1922
issues: write
2023
checks: write
24+
actions: read
2125

2226
jobs:
2327
# ==========================================================================
2428
# AUTOMATIC REVIEW FOR ORG MEMBERS
2529
# Triggers when a PR is marked ready for review or opened (non-draft)
26-
# Only runs for members of the docker org; auto-reviews same-repo PRs from org members
27-
# Only runs for same-repo PRs (fork PRs don't have access to secrets with pull_request trigger)
28-
# Fork PRs use the /review command instead
30+
# Supports two trigger paths:
31+
# 1. pull_request event (same-repo branches only — fork PRs lack secret access)
32+
# 2. workflow_run event (fork PRs via pr-review-trigger.yml → workflow_run pattern)
33+
# Only runs for members of the docker org.
2934
# ==========================================================================
3035
auto-review:
3136
if: |
32-
github.event_name == 'pull_request' &&
33-
!github.event.pull_request.draft &&
34-
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
37+
(
38+
github.event_name == 'pull_request' &&
39+
!github.event.pull_request.draft &&
40+
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
41+
) || (
42+
github.event_name == 'workflow_run' &&
43+
github.event.workflow_run.name == 'PR Review Trigger' &&
44+
github.event.workflow_run.conclusion == 'success' &&
45+
github.event.workflow_run.head_repository.full_name != github.repository
46+
)
3547
runs-on: ubuntu-latest
3648
env:
3749
HAS_APP_SECRETS: ${{ secrets.CAGENT_REVIEWER_APP_ID != '' }}
3850

3951
steps:
52+
# For workflow_run events (fork PRs), download the artifact saved by pr-review-trigger.yml
53+
# to get the PR number. For pull_request events, read it directly from the event payload.
54+
- name: Get PR number
55+
id: get-pr
56+
shell: bash
57+
env:
58+
GH_TOKEN: ${{ github.token }}
59+
EVENT_NAME: ${{ github.event_name }}
60+
EVENT_PR_NUMBER: ${{ github.event.pull_request.number }}
61+
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
62+
REPO: ${{ github.repository }}
63+
run: |
64+
if [ "$EVENT_NAME" = "pull_request" ]; then
65+
echo "pr-number=$EVENT_PR_NUMBER" >> $GITHUB_OUTPUT
66+
exit 0
67+
fi
68+
69+
mkdir -p /tmp/trigger-metadata
70+
71+
if ! gh run download "$WORKFLOW_RUN_ID" -n "pr-review-trigger-metadata-$WORKFLOW_RUN_ID" -D /tmp/trigger-metadata --repo "$REPO" 2>/tmp/download_error.log; then
72+
if grep -qi "no artifact" /tmp/download_error.log; then
73+
echo "⏭️ No trigger metadata artifact found — skipping"
74+
else
75+
echo "::warning::Failed to download trigger metadata: $(cat /tmp/download_error.log)"
76+
fi
77+
echo "pr-number=" >> $GITHUB_OUTPUT
78+
exit 0
79+
fi
80+
81+
PR_NUMBER=$(cat /tmp/trigger-metadata/pr-number | tr -d '[:space:]')
82+
if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]] || [ "$PR_NUMBER" -lt 1 ]; then
83+
echo "::error::Invalid PR number: $PR_NUMBER"
84+
exit 1
85+
fi
86+
87+
echo "pr-number=$PR_NUMBER" >> $GITHUB_OUTPUT
88+
echo "✅ Got PR number from trigger metadata: #$PR_NUMBER"
89+
4090
- name: Check if PR author is org member
4191
id: membership
4292
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
4393
with:
4494
github-token: ${{ secrets.CAGENT_ORG_MEMBERSHIP_TOKEN }}
4595
script: |
4696
const org = 'docker';
47-
const username = context.payload.pull_request.user.login;
97+
98+
// For workflow_run events, fetch PR author and draft status via API
99+
// since github.event.pull_request is not available in that context
100+
let username;
101+
if (context.eventName === 'workflow_run') {
102+
const prNumber = parseInt('${{ steps.get-pr.outputs.pr-number }}', 10);
103+
if (!prNumber) {
104+
core.setOutput('is_member', 'false');
105+
console.log('⏭️ No PR number — skipping auto-review');
106+
return;
107+
}
108+
const { data: pr } = await github.rest.pulls.get({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
pull_number: prNumber
112+
});
113+
if (pr.draft) {
114+
core.setOutput('is_member', 'false');
115+
console.log(`⏭️ PR #${prNumber} is a draft — skipping auto-review`);
116+
return;
117+
}
118+
username = pr.user.login;
119+
} else {
120+
username = context.payload.pull_request.user.login;
121+
}
48122
49123
try {
50124
await github.rest.orgs.checkMembershipForUser({
@@ -70,13 +144,12 @@ jobs:
70144
}
71145
}
72146
73-
# With pull_request, checking out the PR head is standard behavior
74147
- name: Checkout PR head
75148
if: steps.membership.outputs.is_member == 'true'
76149
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
77150
with:
78151
fetch-depth: 0
79-
ref: refs/pull/${{ github.event.pull_request.number }}/head
152+
ref: refs/pull/${{ steps.get-pr.outputs.pr-number }}/head
80153

81154
# Generate GitHub App token for custom app identity (optional - falls back to github.token)
82155
- name: Generate GitHub App token
@@ -94,7 +167,7 @@ jobs:
94167
continue-on-error: true # Don't fail the calling workflow if the review errors
95168
uses: ./review-pr
96169
with:
97-
pr-number: ${{ github.event.pull_request.number }}
170+
pr-number: ${{ steps.get-pr.outputs.pr-number }}
98171
github-token: ${{ steps.app-token.outputs.token || github.token }}
99172
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
100173
openai-api-key: ${{ secrets.OPENAI_API_KEY }}

0 commit comments

Comments
 (0)