Skip to content

Commit 08cf872

Browse files
rnetserclaude
andauthored
fix: trigger unresolve workflow on thread resolve, not every push (#5261)
##### What this PR does / why we need it: The `unresolve-coderabbit-threads` workflow previously triggered on `pull_request_target: [synchronize]` (every push), paginating through ALL review threads to find resolved ones. This was wasteful — it ran on every push even when no threads were resolved. This PR changes the trigger to `pull_request_review_thread: [resolved]`, which fires only when a thread is actually resolved. The event payload provides the specific thread's `node_id`, so we query only that one thread via GraphQL `node(id:)` instead of paginating all threads. Same validation logic is preserved: skip non-CodeRabbit threads, check for substantive PR author replies (≥15 chars) or CodeRabbit verification markers, unresolve + warn if neither is found. ##### Which issue(s) this PR fixes: Fixes #5260 ##### Special notes for reviewer: `pull_request_review_thread` is an undocumented but functional GitHub webhook event that fires when a review thread is resolved/unresolved. ##### jira-ticket: NONE Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Ruth Netser <rnetser@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 72715e1 commit 08cf872

1 file changed

Lines changed: 157 additions & 93 deletions

File tree

Lines changed: 157 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
# description: Unresolve CodeRabbit review threads that were resolved by the PR author
2-
# without addressing them. Runs on every push to a PR.
1+
# description: Unresolve CodeRabbit review threads that were resolved without
2+
# being addressed.
33
#
4-
# Logic: If a CodeRabbit review thread was resolved by someone other than
5-
# coderabbitai[bot], and the resolver's last reply does NOT contain a substantive
6-
# response (e.g., "fixed", "addressed", code snippet, or explanation), the thread
7-
# is unresolved so CodeRabbit can re-evaluate it.
4+
# Two triggers:
5+
# 1. pull_request_review_thread [resolved] — fires instantly when a thread is
6+
# resolved. Only works for same-repo PRs (fork PRs have no secrets access).
7+
# 2. pull_request_target [synchronize] — fires on every push. Used as a
8+
# fallback for fork PRs (has secrets access). Paginates all threads.
9+
#
10+
# Logic: When someone resolves a CodeRabbit review thread, we check whether the
11+
# resolver left a substantive reply or CodeRabbit verified the fix. If neither,
12+
# the thread is unresolved so CodeRabbit can re-evaluate it.
813

914
name: Unresolve unaddressed CodeRabbit threads
1015

1116
on:
17+
# Instant: fires when a thread is resolved (same-repo PRs only — no secrets access for forks)
18+
pull_request_review_thread:
19+
types: [resolved]
20+
# Fallback: catches fork PRs on next push (has secrets access via pull_request_target)
1221
pull_request_target:
1322
types: [synchronize]
1423

@@ -22,121 +31,176 @@ permissions:
2231

2332
jobs:
2433
unresolve-threads:
25-
name: Unresolve prematurely resolved threads
34+
name: Check resolved CodeRabbit threads
2635
if: "!endsWith(github.event.pull_request.user.login, '[bot]')"
2736
runs-on: ubuntu-latest
2837
timeout-minutes: 5
2938

3039
steps:
31-
- name: Unresolve prematurely resolved CodeRabbit threads
40+
- name: Check and unresolve unaddressed threads
3241
env:
3342
GH_TOKEN: ${{ secrets.BOT3_TOKEN }}
43+
EVENT_NAME: ${{ github.event_name }}
44+
THREAD_ID: ${{ github.event.thread.node_id }}
45+
SENDER: ${{ github.event.sender.login }}
3446
PR_NUMBER: ${{ github.event.pull_request.number }}
3547
REPO: ${{ github.repository }}
3648
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
3749
run: |
3850
set -euo pipefail
3951
40-
# Fetch resolved review threads with pagination
41-
ALL_THREADS="[]"
42-
CURSOR=""
43-
while true; do
44-
if [ -n "$CURSOR" ]; then
45-
AFTER_ARG="-f after=$CURSOR"
52+
# --- Helper: check if a thread should be unresolved ---
53+
check_and_unresolve_thread() {
54+
local thread_data="$1"
55+
local thread_id
56+
thread_id=$(echo "$thread_data" | jq -r '.id')
57+
58+
# Check if the opening comment is from CodeRabbit — if not, skip
59+
local opener
60+
opener=$(echo "$thread_data" | jq -r '.opening_comment.nodes[0].author.login // ""')
61+
if [ "$opener" != "coderabbitai[bot]" ]; then
62+
echo "Thread $thread_id not opened by CodeRabbit (opener: $opener). Skipping."
63+
return
64+
fi
65+
66+
# Check for a substantive reply from PR author (>= 15 chars) or CodeRabbit verification
67+
local has_response
68+
has_response=$(echo "$thread_data" | jq -r --arg pr_author "$PR_AUTHOR" '
69+
any(.recent_comments.nodes[];
70+
(
71+
# PR author posted a substantive reply (>= 15 chars)
72+
(.author.login == $pr_author and ((.body // "") | length) >= 15)
73+
or
74+
# CodeRabbit verified the fix
75+
(.author.login == "coderabbitai[bot]" and ((.body // "") | test("addressed|verified|resolved|✅|concern is fully"; "i")))
76+
)
77+
)
78+
')
79+
80+
if [ "$has_response" = "true" ]; then
81+
echo "Thread $thread_id has a substantive response. Keeping resolved."
82+
return
83+
fi
84+
85+
# No substantive reply — unresolve the thread
86+
echo "Unresolving thread: $thread_id"
87+
if gh api graphql -f query="
88+
mutation {
89+
unresolveReviewThread(input: {threadId: \"$thread_id\"}) {
90+
thread {
91+
id
92+
isResolved
93+
}
94+
}
95+
}
96+
"; then
97+
# Add a comment explaining why it was unresolved
98+
gh api graphql -f query="
99+
mutation {
100+
addPullRequestReviewThreadReply(input: {pullRequestReviewThreadId: \"$thread_id\", body: \"⚠️ This thread was automatically unresolved because it was resolved without a substantive response. Please address the review comment and explain how it was resolved before resolving this thread again.\"}) {
101+
comment {
102+
id
103+
}
104+
}
105+
}
106+
" || echo " Warning: failed to add comment to $thread_id"
46107
else
47-
AFTER_ARG=""
108+
echo " Warning: failed to unresolve $thread_id"
109+
fi
110+
}
111+
112+
# --- Path 1: pull_request_review_thread (single thread) ---
113+
if [ "$EVENT_NAME" = "pull_request_review_thread" ]; then
114+
# Skip if CodeRabbit resolved its own thread
115+
if [ "$SENDER" = "coderabbitai[bot]" ]; then
116+
echo "CodeRabbit resolved its own thread. Skipping."
117+
exit 0
48118
fi
49-
PAGE=$(gh api graphql -f query='
50-
query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
51-
repository(owner: $owner, name: $repo) {
52-
pullRequest(number: $pr) {
53-
reviewThreads(first: 100, after: $after) {
119+
120+
echo "Trigger: pull_request_review_thread — checking single thread $THREAD_ID"
121+
THREAD_DATA=$(gh api graphql -f query='
122+
query($id: ID!) {
123+
node(id: $id) {
124+
... on PullRequestReviewThread {
125+
id
126+
opening_comment: comments(first: 1) {
54127
nodes {
55-
id
56-
isResolved
57-
opening_comment: comments(first: 1) {
58-
nodes {
59-
author {
60-
login
61-
}
62-
}
63-
}
64-
recent_comments: comments(last: 5) {
65-
nodes {
66-
author {
67-
login
68-
}
69-
body
70-
}
128+
author {
129+
login
71130
}
72131
}
73-
pageInfo {
74-
hasNextPage
75-
endCursor
132+
}
133+
recent_comments: comments(last: 5) {
134+
nodes {
135+
author {
136+
login
137+
}
138+
body
76139
}
77140
}
78141
}
79142
}
80143
}
81-
' -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr="$PR_NUMBER" $AFTER_ARG)
144+
' -f id="$THREAD_ID")
82145
83-
# Append nodes to accumulated list
84-
NODES=$(echo "$PAGE" | jq '.data.repository.pullRequest.reviewThreads.nodes')
85-
ALL_THREADS=$(echo "$ALL_THREADS $NODES" | jq -s 'add')
146+
check_and_unresolve_thread "$(echo "$THREAD_DATA" | jq '.data.node')"
86147
87-
# Check for next page
88-
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
89-
if [ "$HAS_NEXT" != "true" ]; then
90-
break
91-
fi
92-
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
93-
done
94-
95-
# Process each resolved thread
96-
echo "$ALL_THREADS" | jq -r --arg pr_author "$PR_AUTHOR" '
97-
.[]
98-
| select(.isResolved == true)
99-
| select(.opening_comment.nodes[0].author.login == "coderabbitai[bot]")
100-
| select(
101-
# No substantive reply from PR author OR CodeRabbit verification
102-
(any(.recent_comments.nodes[];
103-
(
104-
# PR author posted a substantive reply (>= 15 chars)
105-
(.author.login == $pr_author and ((.body // "") | length) >= 15)
106-
or
107-
# CodeRabbit verified the fix (contains confirmation markers)
108-
(.author.login == "coderabbitai[bot]" and ((.body // "") | test("addressed|verified|resolved|✅|concern is fully"; "i")))
109-
)
110-
) | not)
111-
)
112-
| .id
113-
' | while read -r thread_id; do
114-
if [ -n "$thread_id" ]; then
115-
echo "Unresolving thread: $thread_id"
116-
if gh api graphql -f query="
117-
mutation {
118-
unresolveReviewThread(input: {threadId: \"$thread_id\"}) {
119-
thread {
120-
id
121-
isResolved
122-
}
123-
}
124-
}
125-
"; then
126-
# Add a comment to the thread explaining why it was unresolved
127-
gh api graphql -f query="
128-
mutation {
129-
addPullRequestReviewThreadReply(input: {pullRequestReviewThreadId: \"$thread_id\", body: \"\u26a0\ufe0f This thread was automatically unresolved because it was resolved without a substantive response. Please address the review comment and explain how it was resolved before resolving this thread again.\"}) {
130-
comment {
131-
id
148+
# --- Path 2: pull_request_target (paginate all threads) ---
149+
else
150+
echo "Trigger: pull_request_target — scanning all resolved threads for PR #$PR_NUMBER"
151+
CURSOR=""
152+
while true; do
153+
if [ -n "$CURSOR" ]; then
154+
AFTER_ARG="-f after=$CURSOR"
155+
else
156+
AFTER_ARG=""
157+
fi
158+
PAGE=$(gh api graphql -f query='
159+
query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
160+
repository(owner: $owner, name: $repo) {
161+
pullRequest(number: $pr) {
162+
reviewThreads(first: 100, after: $after) {
163+
nodes {
164+
id
165+
isResolved
166+
opening_comment: comments(first: 1) {
167+
nodes {
168+
author {
169+
login
170+
}
171+
}
172+
}
173+
recent_comments: comments(last: 5) {
174+
nodes {
175+
author {
176+
login
177+
}
178+
body
179+
}
180+
}
181+
}
182+
pageInfo {
183+
hasNextPage
184+
endCursor
185+
}
132186
}
133187
}
134188
}
135-
" || echo " Warning: failed to add comment to $thread_id"
136-
else
137-
echo " Warning: failed to unresolve $thread_id"
189+
}
190+
' -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr="$PR_NUMBER" $AFTER_ARG)
191+
192+
# Process each resolved thread on this page
193+
echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == true)' | while read -r thread; do
194+
check_and_unresolve_thread "$thread"
195+
done
196+
197+
# Check for next page
198+
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
199+
if [ "$HAS_NEXT" != "true" ]; then
200+
break
138201
fi
139-
fi
140-
done
202+
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
203+
done
204+
fi
141205
142206
echo "Done checking CodeRabbit threads."

0 commit comments

Comments
 (0)