Files
kandev/scripts/pr-resolve
T
2026-07-01 07:54:19 +01:00

183 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# pr-resolve — list and resolve PR review threads in batch.
#
# Usage:
# scripts/pr-resolve list <PR>
# List unresolved review threads, one per line, TAB-separated:
# <thread_id>\t<comment_id>\t<author>\t<path>\t<body_first_120_chars>
#
# scripts/pr-resolve show <PR> <thread_id|comment_id>
# Print the full thread/comment body plus path, line, author, commit id,
# and URL. Use before deciding how to answer a preview from list.
#
# scripts/pr-resolve reply <PR> <comment_id> <thread_id> <body>
# scripts/pr-resolve reply <PR> <comment_id> <thread_id> --body-file <path>
# Reply to the thread starter, resolve the thread, and add a +1
# reaction in one call. Use this for both "fixed" replies and
# pushback replies — the reply body conveys whether you agreed
# or not.
#
# Owner/repo are derived from `gh repo view` in the current repo.
#
# Requires: gh (authenticated) and jq.
set -euo pipefail
usage() {
sed -n '2,20p' "$0" | sed 's|^# \{0,1\}||'
exit "${1:-1}"
}
[ $# -ge 1 ] || usage
cmd=$1; shift
case "$cmd" in
-h | --help | help)
usage 0
;;
esac
if [[ "$cmd" == "reply" && ( $# -eq 4 || $# -eq 5 ) ]]; then
if [[ "$2" == PRRT_* ]]; then
echo "reply expects comment_id before thread_id; got a review thread ID in the comment_id position" >&2
exit 1
fi
if [[ "$3" != PRRT_* ]]; then
echo "reply expects thread_id after comment_id; got a non-review-thread ID in the thread_id position" >&2
exit 1
fi
if [[ $# -eq 4 && "$4" == "--body-file" ]]; then
echo "reply --body-file requires a path" >&2
exit 1
fi
if [[ $# -eq 5 ]]; then
[[ "$4" == "--body-file" ]] || usage
[[ -f "$5" ]] || { echo "body file not found: $5" >&2; exit 1; }
[[ -s "$5" ]] || { echo "body file is empty: $5" >&2; exit 1; }
fi
fi
read -r OWNER REPO < <(gh repo view --json owner,name --jq '"\(.owner.login) \(.name)"')
case "$cmd" in
list)
[ $# -eq 1 ] || usage
PR=$1
gh api graphql -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
id
isResolved
path
comments(first: 1) {
nodes { databaseId author { login } body }
}
}
}
}
}
}' -f owner="$OWNER" -f repo="$REPO" -F pr="$PR" \
| jq -r '.data.repository.pullRequest.reviewThreads.nodes
| map(select(.isResolved == false))
| .[]
| "\(.id)\t\(.comments.nodes[0].databaseId)\t\(.comments.nodes[0].author.login)\t\(.path)\t\(.comments.nodes[0].body | gsub("\n"; " ") | .[0:120])"'
;;
show)
[ $# -eq 2 ] || usage
PR=$1; TARGET=$2
case "$TARGET" in
PRRT_*)
gh api graphql -f query='
query($id: ID!) {
node(id: $id) {
... on PullRequestReviewThread {
id
isResolved
path
line
pullRequest { number }
comments(first: 100) {
nodes {
databaseId
author { login }
body
path
line
commit { oid }
url
createdAt
}
}
}
}
}' -f id="$TARGET" \
| jq -r --argjson pr "$PR" '
.data.node as $thread
| if $thread == null then error("thread not found")
elif $thread.pullRequest.number != $pr then error("thread belongs to PR #\($thread.pullRequest.number), not #\($pr)")
else
"thread_id: \($thread.id)",
"resolved: \($thread.isResolved)",
"path: \($thread.path // "-")",
"line: \($thread.line // "-")",
"comments:",
($thread.comments.nodes[] | [
" comment_id: \(.databaseId)",
" author: \(.author.login // "-")",
" path: \(.path // $thread.path // "-")",
" line: \(.line // "-")",
" commit_id: \(.commit.oid // "-")",
" html_url: \(.url // "-")",
" created_at: \(.createdAt // "-")",
" body:",
(.body | split("\n") | map(" " + .) | join("\n"))
] | join("\n"))
end'
;;
*)
gh api "repos/$OWNER/$REPO/pulls/comments/$TARGET" \
| jq -r --argjson pr "$PR" '
(.pull_request_url | capture("/pulls/(?<number>[0-9]+)$").number | tonumber) as $comment_pr
| if $comment_pr != $pr then error("comment belongs to PR #\($comment_pr), not #\($pr)")
else
"comment_id: \(.id)",
"author: \(.user.login // "-")",
"path: \(.path // "-")",
"line: \(.line // "-")",
"commit_id: \(.commit_id // "-")",
"html_url: \(.html_url // "-")",
"created_at: \(.created_at // "-")",
"body:",
(.body | split("\n") | map(" " + .) | join("\n"))
end'
;;
esac
;;
reply)
[[ $# -eq 4 || $# -eq 5 ]] || usage
PR=$1; COMMENT_ID=$2; THREAD_ID=$3
if [[ $# -eq 5 ]]; then
BODY=$(<"$5")
else
BODY=$4
fi
gh api -X POST "repos/$OWNER/$REPO/pulls/$PR/comments/$COMMENT_ID/replies" \
-f body="$BODY" --silent
gh api graphql -f query='mutation($t: ID!) { resolveReviewThread(input: {threadId: $t}) { thread { isResolved } } }' \
-f t="$THREAD_ID" --silent
gh api -X POST "repos/$OWNER/$REPO/pulls/comments/$COMMENT_ID/reactions" \
-f content="+1" --silent
echo "ok $THREAD_ID"
;;
*)
usage
;;
esac