Skip to content

Instantly share code, notes, and snippets.

@garriguv
Created March 27, 2026 16:17
Show Gist options
  • Select an option

  • Save garriguv/ed6b43057e12676cdd7fb6d42075d71c to your computer and use it in GitHub Desktop.

Select an option

Save garriguv/ed6b43057e12676cdd7fb6d42075d71c to your computer and use it in GitHub Desktop.
PR Review Bash Script
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: bin/pr-reviews <PR_NUMBER> [OPTIONS]
Extract and format PR review comments for easy reading by humans and agents.
Options:
-a, --all Show all reviews (default: only the latest review)
-r, --repo OWNER/REPO GitHub repository (default: current repo)
-h, --help Show this help message
Examples:
bin/pr-reviews 1629
bin/pr-reviews 1629 --repo getdexter/lab
EOF
exit 0
}
# --- Parse arguments ---
PR_NUMBER=""
REPO_ARGS=()
SHOW_ALL=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h | --help) usage ;;
-a | --all)
SHOW_ALL=true
shift
;;
-r | --repo)
REPO_ARGS=(--repo "$2")
shift 2
;;
*)
if [[ -z "$PR_NUMBER" ]]; then
PR_NUMBER="$1"
shift
else
echo "Error: unexpected argument '$1'" >&2
exit 1
fi
;;
esac
done
if [[ -z "$PR_NUMBER" ]]; then
echo "Error: PR number is required" >&2
echo "Run 'bin/pr-reviews --help' for usage" >&2
exit 1
fi
# --- Fetch data ---
reviews_json=$(gh api "repos/{owner}/{repo}/pulls/${PR_NUMBER}/reviews" "${REPO_ARGS[@]+"${REPO_ARGS[@]}"}" --paginate | jq -s 'add')
all_comments_json=$(gh api "repos/{owner}/{repo}/pulls/${PR_NUMBER}/comments" "${REPO_ARGS[@]+"${REPO_ARGS[@]}"}" --paginate | jq -s 'add // []')
# --- Filter to relevant reviews and comments ---
if [[ "$SHOW_ALL" == "true" ]]; then
filtered_reviews=$(echo "$reviews_json" | jq '[.[] | select(.state != "PENDING")]')
comments_json="$all_comments_json"
else
# Start with the latest review, then expand to include any reviews whose
# threads received replies in the latest review (so full context is shown).
last_review_id=$(echo "$reviews_json" | jq '[.[] | select(.state != "PENDING")] | last.id // empty')
if [[ -n "$last_review_id" ]]; then
# Find all review IDs that should be included: the latest review plus any
# reviews that own root comments replied to in the latest review.
relevant_review_ids=$(echo "$all_comments_json" | jq --argjson rid "$last_review_id" '
[.[] | select(.pull_request_review_id == $rid and .in_reply_to_id != null) | .in_reply_to_id] as $replied_root_ids |
[.[] | select([.id] | inside($replied_root_ids)) | .pull_request_review_id] as $parent_review_ids |
([$rid] + $parent_review_ids) | unique
')
filtered_reviews=$(echo "$reviews_json" | jq --argjson ids "$relevant_review_ids" '
[.[] | select(.state != "PENDING" and ([.id] | inside($ids)))]
')
comments_json=$(echo "$all_comments_json" | jq --argjson ids "$relevant_review_ids" '
[.[] | select([.pull_request_review_id] | inside($ids))]
')
else
filtered_reviews=$(echo "$reviews_json" | jq '[.[] | select(.state != "PENDING")] | [last] // []')
comments_json="$all_comments_json"
fi
fi
comment_count=$(echo "$comments_json" | jq 'length')
# --- Print reviews ---
review_count=$(echo "$filtered_reviews" | jq 'length')
if [[ "$review_count" -gt 0 ]]; then
echo "## Comment"
echo
echo "$filtered_reviews" | jq -c '.[]' | while IFS= read -r review; do
user=$(echo "$review" | jq -r '.user.login')
state=$(echo "$review" | jq -r '.state')
body=$(echo "$review" | jq -r '.body // ""')
submitted_at=$(echo "$review" | jq -r '.submitted_at // ""')
case "$state" in
APPROVED) state_label="APPROVED" ;;
CHANGES_REQUESTED) state_label="CHANGES REQUESTED" ;;
COMMENTED) state_label="COMMENTED" ;;
DISMISSED) state_label="DISMISSED" ;;
*) state_label="$state" ;;
esac
echo "@${user} — ${state_label} (${submitted_at})"
if [[ -n "$body" ]]; then
echo
echo "$body"
fi
echo
done
fi
if [[ "$comment_count" -eq 0 ]]; then
echo "No file comments."
echo
exit 0
fi
# Group by thread: roots (no in_reply_to_id) with replies attached, sorted by file + line
current_file=""
echo "$comments_json" | jq -c '
[.[] | select(.in_reply_to_id == null)] as $roots |
[.[] | select(.in_reply_to_id != null)] as $replies |
[$roots[] | . as $root | {
root: $root,
replies: [$replies[] | select(.in_reply_to_id == $root.id)] | sort_by(.created_at)
}] |
sort_by(.root.path, (.root.line // .root.original_line // 0)) |
.[]
' | while IFS= read -r thread; do
root=$(echo "$thread" | jq -c '.root')
replies=$(echo "$thread" | jq -c '.replies')
path=$(echo "$root" | jq -r '.path')
line=$(echo "$root" | jq -r '.line // .original_line // empty')
start_line=$(echo "$root" | jq -r '.start_line // .original_start_line // empty')
user=$(echo "$root" | jq -r '.user.login')
body=$(echo "$root" | jq -r '.body')
diff_hunk=$(echo "$root" | jq -r '.diff_hunk // ""')
created_at=$(echo "$root" | jq -r '.created_at // ""')
# Emit file heading when the file changes
if [[ "$path" != "$current_file" ]]; then
current_file="$path"
echo "## ${path}"
echo
fi
# Build location string
if [[ -n "$start_line" && "$start_line" != "null" && "$start_line" != "$line" ]]; then
location="${start_line}-${line}"
elif [[ -n "$line" && "$line" != "null" ]]; then
location="L${line}"
else
location=""
fi
if [[ -n "$location" ]]; then
echo "### ${location}"
fi
echo "@${user} (${created_at})"
echo
# Show the diff hunk for context (last 6 lines)
if [[ -n "$diff_hunk" ]]; then
echo '```diff'
echo "$diff_hunk"
echo '```'
echo
fi
echo "$body"
# Show replies in thread
reply_count=$(echo "$replies" | jq 'length')
if [[ "$reply_count" -gt 0 ]]; then
echo
echo "$replies" | jq -c '.[]' | while IFS= read -r reply; do
reply_user=$(echo "$reply" | jq -r '.user.login')
reply_body=$(echo "$reply" | jq -r '.body')
reply_at=$(echo "$reply" | jq -r '.created_at // ""')
echo "↳ @${reply_user} (${reply_at}):"
echo "$reply_body"
echo
done
fi
echo
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment