Repository navigation
docs(plans): pod playbooks, a team's procedure as a pod workflow (TASK-274) #1952
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 | |
| # No `concurrency:` on purpose (TASK-472). Runs started by a push, review or | |
| # review comment attach their `count` check to the PR head, and a run that a | |
| # concurrency group cancels, or a pending one it replaces, stays on the head as | |
| # a CANCELLED check that reads as red after the threads are answered. Each run | |
| # reads the threads fresh and finishes in seconds, so order is settled at post | |
| # time instead: a run skips posting when the head's status already came from a | |
| # newer run (a higher run id started later and read newer thread state). | |
| 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 | |
| # Newest run wins: the status's target_url carries the run id that posted it. | |
| posted=$(gh api "repos/$REPO/commits/$head/status" \ | |
| -q '[.statuses[] | select(.context == "coderabbit-threads" and .target_url != null) | .target_url | capture("/runs/(?<id>[0-9]+)").id | tonumber] | max // 0') | |
| if [ "$posted" -gt "$GITHUB_RUN_ID" ]; then | |
| echo "Run $posted already posted a newer result on $head; not overwriting it."; exit 0 | |
| fi | |
| 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 |