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
86 changes: 86 additions & 0 deletions .github/workflows/coderabbit-threads.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CodeRabbit threads: every review thread CodeRabbit opens gets an answer
# before a gate ask (Sam, 2026-10-24, #2605). An answer is a reply inside the
# thread, or a PR comment that quotes the thread's `discussion_r<id>` link
# (seats reply that way when they cannot reply in-thread). A review body
# never counts, because a HOLD cites the unanswered link. Resolved and
# outdated threads don't count. Fork PRs are skipped entirely.
#
# Reported as the commit status `coderabbit-threads` on the PR head, not as
# this workflow's own check: a run started by a comment attaches to the
# default branch and would never show on the PR. NOT a required check. It
# never checks out PR code, so the write token on comment events only reaches
# the statuses API.
name: CodeRabbit threads

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
pull_request_review:
# `deleted` too: removing the only reply or quoting comment must re-fail
# the status rather than leave a stale success on the head.
pull_request_review_comment:
types: [created, edited, deleted]
issue_comment:
types: [created, edited, deleted]

permissions:
contents: read
pull-requests: read
statuses: write

concurrency:
group: coderabbit-threads-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true

jobs:
count:
if: github.event_name != 'issue_comment' || github.event.issue.pull_request
runs-on: ubuntu-latest
timeout-minutes: 3
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number || github.event.issue.number }}
REPO: ${{ github.repository }}
steps:
- name: Count unanswered CodeRabbit threads
run: |
set -euo pipefail
owner="${REPO%/*}"; name="${REPO#*/}"
gh api "repos/$REPO/pulls/$PR" > pr.json
# Fork PRs get no action at all (a comment event runs with a write
# token on the default branch, so don't rely on a token error).
if [ "$(jq -r .head.repo.full_name pr.json)" != "$REPO" ]; then
echo "Fork PR: not counted, no status posted."; exit 0
fi
head=$(jq -r .head.sha pr.json)
# Every thread page; replies are read 100 deep per thread, and a
# thread with more than that and no visible reply is counted as
# INCOMPLETE rather than answered or unanswered.
gh api graphql --paginate --slurp -F owner="$owner" -F name="$name" -F pr="$PR" -f query='
query($owner:String!,$name:String!,$pr:Int!,$endCursor:String){repository(owner:$owner,name:$name){pullRequest(number:$pr){
reviewThreads(first:100,after:$endCursor){pageInfo{hasNextPage endCursor} nodes{isResolved isOutdated
comments(first:100){pageInfo{hasNextPage} nodes{author{login} databaseId url}}}}}}}' \
| jq '[.[].data.repository.pullRequest.reviewThreads.nodes[]]' > threads.json
# An answer is an in-thread reply or a PR COMMENT quoting the link.
# Review bodies don't count: a HOLD citing an unanswered thread
# quotes its link, and that must leave it unanswered.
gh api --paginate "repos/$REPO/issues/$PR/comments" -q '.[].body' > said.txt
jq -r '.[]
| select(.isResolved|not) | select(.isOutdated|not)
| select(.comments.nodes[0].author.login | test("^coderabbitai"))
| select([.comments.nodes[1:][] | select(.author.login | test("^coderabbitai")|not)] | length == 0)
| "\(.comments.nodes[0].databaseId) \(.comments.pageInfo.hasNextPage) \(.comments.nodes[0].url)"' threads.json > open.txt
: > unanswered.txt; : > incomplete.txt
while read -r id more url; do
grep -q "discussion_r$id" said.txt && continue
if [ "$more" = true ]; then echo "$url" >> incomplete.txt; else echo "$url" >> unanswered.txt; fi
done < open.txt
n=$(wc -l < unanswered.txt | tr -d ' '); m=$(wc -l < incomplete.txt | tr -d ' ')
if [ "$n" != 0 ]; then state=failure; desc="$n unanswered CodeRabbit thread(s): reply in-thread or quote the link in a PR comment"
elif [ "$m" != 0 ]; then state=failure; desc="$m CodeRabbit thread(s) too long to read fully: answer them in a PR comment"
else state=success; desc="All CodeRabbit threads answered"; fi
echo "### $desc" >> "$GITHUB_STEP_SUMMARY"
sed 's/^/- /' unanswered.txt incomplete.txt >> "$GITHUB_STEP_SUMMARY"
cat unanswered.txt incomplete.txt
gh api -X POST "repos/$REPO/statuses/$head" -f state="$state" -f context=coderabbit-threads \
-f description="${desc:0:140}" -f target_url="$GITHUB_SERVER_URL/$REPO/actions/runs/$GITHUB_RUN_ID" > /dev/null
Loading