Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 61 additions & 104 deletions .github/workflows/unresolve-coderabbit-threads.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
# description: Unresolve CodeRabbit review threads that were resolved without
# being addressed.
# being addressed. Runs on every push to a PR via pull_request_target.
#
# Two triggers:
Comment thread
rnetser marked this conversation as resolved.
# 1. pull_request_review_thread [resolved] — fires instantly when a thread is
# resolved. Only works for same-repo PRs (fork PRs have no secrets access).
# 2. pull_request_target [synchronize] — fires on every push. Used as a
# fallback for fork PRs (has secrets access). Paginates all threads.
#
# Logic: When someone resolves a CodeRabbit review thread, we check whether the
# resolver left a substantive reply or CodeRabbit verified the fix. If neither,
# the thread is unresolved so CodeRabbit can re-evaluate it.
# Logic: When a PR is pushed, scan all resolved CodeRabbit review threads.
# If a thread was resolved without a substantive PR-author reply or CodeRabbit
# verification, unresolve it so CodeRabbit can re-evaluate.

name: Unresolve unaddressed CodeRabbit threads

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

Expand All @@ -32,6 +22,7 @@ permissions:
jobs:
unresolve-threads:
name: Check resolved CodeRabbit threads
# Webhook payload uses REST-format logins where bots have [bot] suffix
if: "!endsWith(github.event.pull_request.user.login, '[bot]')"
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -40,39 +31,42 @@ jobs:
- name: Check and unresolve unaddressed threads
env:
GH_TOKEN: ${{ secrets.BOT3_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
THREAD_ID: ${{ github.event.thread.node_id }}
SENDER: ${{ github.event.sender.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail

# Bot identity constants — GraphQL returns login without [bot] suffix
# and uses __typename to distinguish bots from human users.
BOT_LOGIN="coderabbitai"
BOT_TYPE="Bot"

# --- Helper: check if a thread should be unresolved ---
check_and_unresolve_thread() {
local thread_data="$1"
local thread_id
thread_id=$(echo "$thread_data" | jq -r '.id')

# Check if the opening comment is from CodeRabbit — if not, skip
local opener
opener=$(echo "$thread_data" | jq -r '.opening_comment.nodes[0].author.login // ""')
if [ "$opener" != "coderabbitai[bot]" ]; then
echo "Thread $thread_id not opened by CodeRabbit (opener: $opener). Skipping."
local opener_login opener_type
opener_login=$(echo "$thread_data" | jq -r '.opening_comment.nodes[0].author.login // ""')
opener_type=$(echo "$thread_data" | jq -r '.opening_comment.nodes[0].author.__typename // ""')
if [ "$opener_login" != "$BOT_LOGIN" ] || [ "$opener_type" != "$BOT_TYPE" ]; then
echo "Thread $thread_id not opened by CodeRabbit (opener: $opener_login, type: $opener_type). Skipping."
return
fi

# Check for a substantive reply from PR author (>= 15 chars) or CodeRabbit verification
local has_response
has_response=$(echo "$thread_data" | jq -r --arg pr_author "$PR_AUTHOR" '
has_response=$(echo "$thread_data" | jq -r --arg pr_author "$PR_AUTHOR" --arg bot_login "$BOT_LOGIN" --arg bot_type "$BOT_TYPE" '
any(.recent_comments.nodes[];
(
# PR author posted a substantive reply (>= 15 chars)
(.author.login == $pr_author and ((.body // "") | length) >= 15)
or
# CodeRabbit verified the fix
(.author.login == "coderabbitai[bot]" and ((.body // "") | test("addressed|verified|resolved|✅|concern is fully"; "i")))
(.author.login == $bot_login and .author.__typename == $bot_type and ((.body // "") | test("\\b(addressed|verified|resolved)\\b|✅|concern is fully"; "i")))
)
)
')
Expand Down Expand Up @@ -109,98 +103,61 @@ jobs:
fi
}

# --- Path 1: pull_request_review_thread (single thread) ---
if [ "$EVENT_NAME" = "pull_request_review_thread" ]; then
# Skip if CodeRabbit resolved its own thread
if [ "$SENDER" = "coderabbitai[bot]" ]; then
echo "CodeRabbit resolved its own thread. Skipping."
exit 0
echo "Scanning all resolved threads for PR #$PR_NUMBER"
CURSOR=""
while true; do
if [ -n "$CURSOR" ]; then
AFTER_ARG="-f after=$CURSOR"
else
AFTER_ARG=""
fi

echo "Trigger: pull_request_review_thread — checking single thread $THREAD_ID"
THREAD_DATA=$(gh api graphql -f query='
query($id: ID!) {
node(id: $id) {
... on PullRequestReviewThread {
id
opening_comment: comments(first: 1) {
PAGE=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100, after: $after) {
nodes {
author {
login
}
}
}
recent_comments: comments(last: 5) {
nodes {
author {
login
}
body
}
}
}
}
}
' -f id="$THREAD_ID")

check_and_unresolve_thread "$(echo "$THREAD_DATA" | jq '.data.node')"

# --- Path 2: pull_request_target (paginate all threads) ---
else
echo "Trigger: pull_request_target — scanning all resolved threads for PR #$PR_NUMBER"
CURSOR=""
while true; do
if [ -n "$CURSOR" ]; then
AFTER_ARG="-f after=$CURSOR"
else
AFTER_ARG=""
fi
PAGE=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100, after: $after) {
nodes {
id
isResolved
opening_comment: comments(first: 1) {
nodes {
author {
login
}
id
isResolved
opening_comment: comments(first: 1) {
nodes {
author {
login
__typename
}
}
recent_comments: comments(last: 5) {
nodes {
author {
login
}
body
}
recent_comments: comments(last: 5) {
nodes {
author {
login
__typename
}
body
}
}
pageInfo {
hasNextPage
endCursor
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
' -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr="$PR_NUMBER" $AFTER_ARG)

# Process each resolved thread on this page
echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == true)' | while read -r thread; do
check_and_unresolve_thread "$thread"
done

# Check for next page
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
if [ "$HAS_NEXT" != "true" ]; then
break
fi
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
}
' -f owner="${REPO%%/*}" -f repo="${REPO##*/}" -F pr="$PR_NUMBER" $AFTER_ARG)

# Process each resolved thread on this page
echo "$PAGE" | jq -c '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == true)' | while read -r thread; do
check_and_unresolve_thread "$thread"
done
fi

# Check for next page
HAS_NEXT=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
if [ "$HAS_NEXT" != "true" ]; then
break
fi
CURSOR=$(echo "$PAGE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
done

echo "Done checking CodeRabbit threads."