Created
March 27, 2026 16:17
-
-
Save garriguv/ed6b43057e12676cdd7fb6d42075d71c to your computer and use it in GitHub Desktop.
PR Review Bash Script
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
| #!/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