feat(address-gh-review): add thread resolution with reactions and comments
This commit is contained in:
@@ -1,19 +1,34 @@
|
|||||||
---
|
---
|
||||||
name: address-gh-review
|
name: address-gh-review
|
||||||
description: 'Fetch and address unresolved GitHub PR review comments. Use when user asks to handle PR reviews, address review feedback, mentions "/address-gh-review", or wants to see what reviewers requested. Fetches unresolved threads, presents an actionable summary, and lets the user select which items to address.'
|
description: 'Fetch and address unresolved GitHub PR review comments. Use when user asks to handle PR reviews, address review feedback, mentions "/address-gh-review", or wants to see what reviewers requested. Fetches unresolved threads, presents an actionable summary, lets the user select which items to address, and resolves threads on GitHub.'
|
||||||
---
|
---
|
||||||
|
|
||||||
# GitHub PR Review
|
# GitHub PR Review
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Fetch unresolved review threads from the current PR, consolidate them into actionable items, and address selected items — each as a separate commit.
|
Fetch unresolved review threads from the current PR, consolidate them into actionable items, address selected items (each as a separate commit), and resolve threads on GitHub with reactions or explanatory comments.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `gh` CLI authenticated and in a repo with an open PR on the current branch
|
- `gh` CLI authenticated and in a repo with an open PR on the current branch
|
||||||
- The `git-commit` skill available for committing changes
|
- The `git-commit` skill available for committing changes
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `gh_review.sh` — Fetches unresolved threads, prints human-readable summary, and appends a compact thread mapping block at the end with item numbers, thread IDs, and comment IDs for downstream resolution.
|
||||||
|
- `gh_resolve_thread.sh` — Resolves a GitHub review thread, optionally adding reactions or a comment first.
|
||||||
|
|
||||||
|
### `gh_resolve_thread.sh` Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Thumbs-up on comments, then resolve
|
||||||
|
bash gh_resolve_thread.sh thumbsup <thread-id> [comment-id1 comment-id2 ...]
|
||||||
|
|
||||||
|
# Post a reply explaining why, then resolve
|
||||||
|
bash gh_resolve_thread.sh comment "reason" <thread-id>
|
||||||
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### 1. Fetch Review Comments
|
### 1. Fetch Review Comments
|
||||||
@@ -26,6 +41,16 @@ bash gh_review.sh
|
|||||||
|
|
||||||
If the script fails (no PR, not authenticated, etc.), report the error and stop.
|
If the script fails (no PR, not authenticated, etc.), report the error and stop.
|
||||||
|
|
||||||
|
The script appends a thread mapping block at the end of its output:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Thread Mapping (item_num | thread_id | comment_ids)
|
||||||
|
1 PRRT_kwDOKhRdjM5-yKNp PRRC_kwDOKhRdjM6849mq PRRC_kwDOKhRdjM6849mr
|
||||||
|
2 PRRT_kwDOKhRdjM5-yKOJ PRRC_kwDOKhRdjM6849nZ
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture these IDs from the script output — they are needed in step 5.
|
||||||
|
|
||||||
### 2. Consolidate into Actionable Items
|
### 2. Consolidate into Actionable Items
|
||||||
|
|
||||||
Parse the output and group into actionable items. Combine threads that ask for the same change (e.g. multiple reviewers commenting on the same function about the same concern). Keep items separate when they require distinct code changes.
|
Parse the output and group into actionable items. Combine threads that ask for the same change (e.g. multiple reviewers commenting on the same function about the same concern). Keep items separate when they require distinct code changes.
|
||||||
@@ -59,6 +84,15 @@ For each selected item, in order:
|
|||||||
3. Run relevant linting/tests if applicable (e.g. `quicklint`)
|
3. Run relevant linting/tests if applicable (e.g. `quicklint`)
|
||||||
4. Commit using the `git-commit` skill — **one commit per item**
|
4. Commit using the `git-commit` skill — **one commit per item**
|
||||||
|
|
||||||
### 5. Summary
|
### 5. Resolve Threads on GitHub
|
||||||
|
|
||||||
After all selected items are addressed, print a brief summary of what was done.
|
After all code changes are committed, resolve the corresponding threads on GitHub using the thread IDs and comment IDs from the script output in step 1:
|
||||||
|
|
||||||
|
1. For **addressed items**: run `bash gh_resolve_thread.sh thumbsup <thread-id> <comment-ids...>` — adds 👍 to each comment, then resolves the thread.
|
||||||
|
2. For **skipped items**: ask the user for a brief reason, then run `bash gh_resolve_thread.sh comment "<reason>" <thread-id>` — posts an explanation, then resolves.
|
||||||
|
|
||||||
|
Only resolve threads for items the user explicitly selected (addressed or skipped). Leave untouched items unresolved.
|
||||||
|
|
||||||
|
### 6. Summary
|
||||||
|
|
||||||
|
After all selected items are addressed and threads resolved, print a brief summary of what was done (code changes + thread resolutions).
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Resolve a GitHub PR review thread, optionally adding reactions or a comment.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gh_resolve_thread.sh thumbsup <thread-id> [comment-id1 comment-id2 ...]
|
||||||
|
# gh_resolve_thread.sh comment "reason" <thread-id>
|
||||||
|
#
|
||||||
|
# - thumbsup: adds 👍 to each supplied comment ID, then resolves the thread
|
||||||
|
# - comment: posts a reply explaining why the thread is being resolved, then resolves
|
||||||
|
#
|
||||||
|
# Requires: gh CLI authenticated, running on a branch with an open PR.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
err() { echo "error: $1" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v gh >/dev/null 2>&1 || err "'gh' CLI not found"
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
err "usage: $0 {thumbsup|comment} ..."
|
||||||
|
fi
|
||||||
|
ACTION="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
thumbsup)
|
||||||
|
[[ $# -lt 1 ]] && err "usage: $0 thumbsup <thread-id> [comment-id ...]"
|
||||||
|
THREAD_ID="$1"
|
||||||
|
shift
|
||||||
|
COMMENT_IDS=("$@")
|
||||||
|
|
||||||
|
# Add thumbs-up reaction to each comment
|
||||||
|
for cid in "${COMMENT_IDS[@]+"${COMMENT_IDS[@]}"}"; do
|
||||||
|
[[ -z "$cid" ]] && continue
|
||||||
|
# shellcheck disable=SC2016 # GraphQL uses -f flags, not shell expansion
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($subjectId: ID!, $content: ReactionContent!) {
|
||||||
|
addReaction(input: { subjectId: $subjectId, content: $content }) {
|
||||||
|
reaction { content }
|
||||||
|
}
|
||||||
|
}' -f subjectId="$cid" -f content=THUMBS_UP --jq '.data.addReaction.reaction.content' >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
# Resolve the thread
|
||||||
|
# shellcheck disable=SC2016 # GraphQL uses -f flags, not shell expansion
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($threadId: ID!) {
|
||||||
|
resolveReviewThread(input: { threadId: $threadId }) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}' -f threadId="$THREAD_ID" --jq '.data.resolveReviewThread.clientMutationId' >/dev/null 2>&1 || err "failed to resolve thread $THREAD_ID"
|
||||||
|
|
||||||
|
echo "Resolved thread $THREAD_ID (thumbs-up on ${#COMMENT_IDS[@]} comment(s))"
|
||||||
|
;;
|
||||||
|
|
||||||
|
comment)
|
||||||
|
[[ $# -lt 2 ]] && err "usage: $0 comment \"reason\" <thread-id>"
|
||||||
|
REASON="$1"
|
||||||
|
THREAD_ID="$2"
|
||||||
|
|
||||||
|
# Post a reply on the thread
|
||||||
|
# shellcheck disable=SC2016 # GraphQL uses -f flags, not shell expansion
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($threadId: ID!, $body: String!) {
|
||||||
|
addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) {
|
||||||
|
comment { id }
|
||||||
|
}
|
||||||
|
}' -f threadId="$THREAD_ID" -f body="$REASON" --jq '.data.addPullRequestReviewThreadReply.comment.id' >/dev/null 2>&1 || err "failed to post reply on thread $THREAD_ID"
|
||||||
|
|
||||||
|
# Resolve the thread
|
||||||
|
# shellcheck disable=SC2016 # GraphQL uses -f flags, not shell expansion
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation($threadId: ID!) {
|
||||||
|
resolveReviewThread(input: { threadId: $threadId }) {
|
||||||
|
clientMutationId
|
||||||
|
}
|
||||||
|
}' -f threadId="$THREAD_ID" --jq '.data.resolveReviewThread.clientMutationId' >/dev/null 2>&1 || err "failed to resolve thread $THREAD_ID"
|
||||||
|
|
||||||
|
echo "Resolved thread $THREAD_ID (commented: $REASON)"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
err "unknown action: $ACTION (expected thumbsup|comment)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
# Fetch unresolved PR review threads, formatted for LLM consumption.
|
# Fetch unresolved PR review threads, formatted for LLM consumption.
|
||||||
# Omits diff hunks (the LLM can read files from the repo directly).
|
# Omits diff hunks (the LLM can read files from the repo directly).
|
||||||
# Groups comments into threads so conversation context is preserved.
|
# Groups comments into threads so conversation context is preserved.
|
||||||
|
# Appends a compact thread mapping block at the end with item numbers,
|
||||||
|
# thread IDs, and comment IDs for downstream resolution.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -24,16 +26,18 @@ ME=$(gh api user -q .login 2>/dev/null) || err "failed to fetch GitHub user - ar
|
|||||||
OWNER=$(gh repo view --json owner -q .owner.login 2>/dev/null) || err "failed to determine repository owner"
|
OWNER=$(gh repo view --json owner -q .owner.login 2>/dev/null) || err "failed to determine repository owner"
|
||||||
REPO=$(gh repo view --json name -q .name 2>/dev/null) || err "failed to determine repository name"
|
REPO=$(gh repo view --json name -q .name 2>/dev/null) || err "failed to determine repository name"
|
||||||
|
|
||||||
# Fetch unresolved review threads via GraphQL
|
# Fetch Unresolved Review Threads via GraphQL
|
||||||
OUTPUT=$(gh api graphql -f query='
|
QUERY=$(cat <<EOF
|
||||||
query {
|
query {
|
||||||
repository(owner: "'"$OWNER"'", name: "'"$REPO"'") {
|
repository(owner: "${OWNER}", name: "${REPO}") {
|
||||||
pullRequest(number: '"$PR_NUM"') {
|
pullRequest(number: ${PR_NUM}) {
|
||||||
reviewThreads(first: 100) {
|
reviewThreads(first: 100) {
|
||||||
nodes {
|
nodes {
|
||||||
|
id
|
||||||
isResolved
|
isResolved
|
||||||
comments(first: 100) {
|
comments(first: 100) {
|
||||||
nodes {
|
nodes {
|
||||||
|
id
|
||||||
author { login }
|
author { login }
|
||||||
body
|
body
|
||||||
path
|
path
|
||||||
@@ -44,28 +48,53 @@ query {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' --jq '
|
}
|
||||||
[
|
EOF
|
||||||
.data.repository.pullRequest.reviewThreads.nodes[]
|
)
|
||||||
| select(.isResolved == false)
|
|
||||||
| {
|
RAW_JSON=$(gh api graphql -f query="$QUERY" 2>/dev/null | jq -c '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)') || err "GraphQL query failed - check your permissions and token scopes"
|
||||||
file: .comments.nodes[0].path,
|
|
||||||
line: .comments.nodes[0].line,
|
# Build human-readable output + thread mapping block
|
||||||
comments: [
|
OUTPUT=""
|
||||||
.comments.nodes[]
|
MAPPING=""
|
||||||
| select(.author.login != "'"$ME"'")
|
ITEM_NUM=0
|
||||||
| {author: .author.login, body: .body}
|
|
||||||
]
|
while IFS= read -r thread; do
|
||||||
}
|
[[ -z "$thread" ]] && continue
|
||||||
| select(.comments | length > 0)
|
|
||||||
]
|
THREAD_ID=$(echo "$thread" | jq -r '.id')
|
||||||
| .[]
|
FILE=$(echo "$thread" | jq -r '.comments.nodes[0].path')
|
||||||
| "## \(.file):\(.line)\n\(.comments | map("- **\(.author)**: \(.body)") | join("\n"))\n"
|
LINE=$(echo "$thread" | jq -r '.comments.nodes[0].line')
|
||||||
' 2>/dev/null) || err "GraphQL query failed - check your permissions and token scopes"
|
|
||||||
|
# Collect comment IDs (for reactions) — exclude own comments
|
||||||
|
COMMENT_IDS=$(echo "$thread" | jq -r --arg me "$ME" '[.comments.nodes[] | select(.author.login != $me) | .id] | join(" ")')
|
||||||
|
|
||||||
|
# Build human-readable comments (exclude own comments)
|
||||||
|
COMMENTS=$(echo "$thread" | jq -r --arg me "$ME" '[.comments.nodes[] | select(.author.login != $me) | "- **\(.author.login)**: \(.body)"] | join("\n")')
|
||||||
|
|
||||||
|
if [[ -z "$COMMENTS" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ITEM_NUM=$((ITEM_NUM + 1))
|
||||||
|
|
||||||
|
# Append human-readable block
|
||||||
|
OUTPUT+="## ${FILE}:${LINE}
|
||||||
|
${COMMENTS}
|
||||||
|
|
||||||
|
"
|
||||||
|
|
||||||
|
# Append mapping line
|
||||||
|
MAPPING+="${ITEM_NUM} ${THREAD_ID} ${COMMENT_IDS}
|
||||||
|
"
|
||||||
|
done <<< "$RAW_JSON"
|
||||||
|
|
||||||
# Format output
|
# Format output
|
||||||
if [[ -z "$OUTPUT" ]]; then
|
if [[ -z "$OUTPUT" ]]; then
|
||||||
echo "No unresolved review comments found."
|
echo "No unresolved review comments found."
|
||||||
else
|
else
|
||||||
echo "$OUTPUT" | sed 's/\\n/\n/g'
|
echo "$OUTPUT"
|
||||||
|
echo "---"
|
||||||
|
echo "# Thread Mapping (item_num | thread_id | comment_ids)"
|
||||||
|
echo "$MAPPING"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user