From 4989d4d769a42618ff3fc1fd671e925b893eca6e Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 09:30:28 -0500 Subject: [PATCH 1/8] feat(gh-cli): add pull request involvement report Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/README.md | 22 +++++++++++ gh-cli/get-my-pull-requests.sh | 72 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100755 gh-cli/get-my-pull-requests.sh diff --git a/gh-cli/README.md b/gh-cli/README.md index 91b1323..869b714 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -990,6 +990,28 @@ Returns the most recent migration ID for a given organization. Returns the most recent migration ID for a given organization repository. +### get-my-pull-requests.sh + +Gets open pull requests grouped by your involvement: created by you, assigned to you, awaiting your review, and otherwise involving you. Optionally excludes one or more organizations. + +Usage: + +```shell +./get-my-pull-requests.sh +./get-my-pull-requests.sh joshjohanning-org +./get-my-pull-requests.sh joshjohanning-org,another-org +``` + +Output groups include: + +- Created by me - PRs you authored +- Assigned to me - PRs assigned to you +- Awaiting my review - PRs requesting your review +- Involved - PRs you authored, were assigned, mentioned, or commented on + +> [!NOTE] +> Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API. + ### get-organization-active-repositories.sh Gets a list of repositories in an organization that have had code pushed to it in the last X days. diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh new file mode 100755 index 0000000..c24594e --- /dev/null +++ b/gh-cli/get-my-pull-requests.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Get open pull requests grouped by involvement: +# - Created by me +# - Assigned to me +# - Awaiting my review (requested reviewer) +# - Involved (mentioned, assigned, authored, or commented) +# +# Usage: ./get-my-pull-requests.sh [] +# Example: ./get-my-pull-requests.sh +# Example: ./get-my-pull-requests.sh joshjohanning-org +# Example: ./get-my-pull-requests.sh joshjohanning-org,another-org +# Requires GitHub CLI authentication with access to the repositories being searched. + +set -euo pipefail + +usage() { + echo "Usage: $0 []" >&2 +} + +fail() { + echo "Error: $*" >&2 + exit 1 +} + +search_pull_requests() { + local search_query="$1" + + gh api --method GET --paginate /search/issues \ + -f q="$search_query" \ + -F per_page=100 \ + --jq '.items[] | [.number, .title, (.repository_url | sub("^.*/repos/"; ""))] | @tsv' +} + +[[ $# -le 1 ]] || { usage; exit 1; } +command -v gh >/dev/null 2>&1 || + fail "Required command not found: gh" +gh auth status >/dev/null 2>&1 || + fail "GitHub CLI is not authenticated. Run: gh auth login" + +exclude_orgs="${1:-}" +user=$(gh api user --jq '.login') + +exclusion="" +if [ -n "$exclude_orgs" ]; then + IFS=',' read -ra orgs <<< "$exclude_orgs" + for org in "${orgs[@]}"; do + [[ "$org" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]] || + fail "Invalid organization name: $org" + exclusion="$exclusion -org:$org" + done +fi + +echo "Fetching open pull requests for @$user..." >&2 +echo "" >&2 + +echo "## Created by me" +search_pull_requests "author:$user is:pr is:open$exclusion" + +echo "" >&2 +echo "## Assigned to me" +search_pull_requests "assignee:$user is:pr is:open$exclusion" + +echo "" >&2 +echo "## Awaiting my review (requested reviewer)" +search_pull_requests "review-requested:$user is:pr is:open$exclusion" + +echo "" >&2 +echo "## Involved" +search_pull_requests "involves:$user is:pr is:open$exclusion" + +echo "" >&2 +echo "Done!" >&2 From 57b791e9bdc76348068ca278f0332ac418834293 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 09:41:46 -0500 Subject: [PATCH 2/8] fix(gh-cli): address pull request report review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/README.md | 4 ++-- gh-cli/get-my-pull-requests.sh | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/gh-cli/README.md b/gh-cli/README.md index 869b714..d144f49 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -992,7 +992,7 @@ Returns the most recent migration ID for a given organization repository. ### get-my-pull-requests.sh -Gets open pull requests grouped by your involvement: created by you, assigned to you, awaiting your review, and otherwise involving you. Optionally excludes one or more organizations. +Gets open pull requests grouped by your involvement: created by you, assigned to you, awaiting your review, and other involvement. Groups are mutually exclusive. Optionally excludes one or more organizations. Usage: @@ -1007,7 +1007,7 @@ Output groups include: - Created by me - PRs you authored - Assigned to me - PRs assigned to you - Awaiting my review - PRs requesting your review -- Involved - PRs you authored, were assigned, mentioned, or commented on +- Other involvement - remaining PRs you were mentioned in or commented on > [!NOTE] > Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API. diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index c24594e..42380af 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -1,9 +1,9 @@ -#!/usr/bin/env bash +#!/bin/bash # Get open pull requests grouped by involvement: # - Created by me # - Assigned to me # - Awaiting my review (requested reviewer) -# - Involved (mentioned, assigned, authored, or commented) +# - Other involvement (mentioned or commented) # # Usage: ./get-my-pull-requests.sh [] # Example: ./get-my-pull-requests.sh @@ -25,10 +25,12 @@ fail() { search_pull_requests() { local search_query="$1" - gh api --method GET --paginate /search/issues \ - -f q="$search_query" \ - -F per_page=100 \ - --jq '.items[] | [.number, .title, (.repository_url | sub("^.*/repos/"; ""))] | @tsv' + if ! gh api --method GET --paginate /search/issues \ + -f q="$search_query" \ + -F per_page=100 \ + --jq '.items[] | [.number, .title, (.repository_url | sub("^.*/repos/"; ""))] | @tsv'; then + fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" + fi } [[ $# -le 1 ]] || { usage; exit 1; } @@ -65,8 +67,8 @@ echo "## Awaiting my review (requested reviewer)" search_pull_requests "review-requested:$user is:pr is:open$exclusion" echo "" >&2 -echo "## Involved" -search_pull_requests "involves:$user is:pr is:open$exclusion" +echo "## Other involvement" +search_pull_requests "involves:$user -author:$user -assignee:$user -review-requested:$user is:pr is:open$exclusion" echo "" >&2 echo "Done!" >&2 From bbfa3ec8d1493cc235d4e44bb2ddc494a6dbdfaf Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 09:51:46 -0500 Subject: [PATCH 3/8] fix(gh-cli): enforce pull request group precedence Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/README.md | 2 +- gh-cli/get-my-pull-requests.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gh-cli/README.md b/gh-cli/README.md index d144f49..8b30a3a 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -992,7 +992,7 @@ Returns the most recent migration ID for a given organization repository. ### get-my-pull-requests.sh -Gets open pull requests grouped by your involvement: created by you, assigned to you, awaiting your review, and other involvement. Groups are mutually exclusive. Optionally excludes one or more organizations. +Gets open pull requests grouped by your involvement: created by you, assigned to you, awaiting your review, and other involvement. Groups are mutually exclusive using the precedence shown below. Optionally excludes one or more organizations. Usage: diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index 42380af..4a5c45e 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -60,11 +60,11 @@ search_pull_requests "author:$user is:pr is:open$exclusion" echo "" >&2 echo "## Assigned to me" -search_pull_requests "assignee:$user is:pr is:open$exclusion" +search_pull_requests "assignee:$user -author:$user is:pr is:open$exclusion" echo "" >&2 echo "## Awaiting my review (requested reviewer)" -search_pull_requests "review-requested:$user is:pr is:open$exclusion" +search_pull_requests "review-requested:$user -author:$user -assignee:$user is:pr is:open$exclusion" echo "" >&2 echo "## Other involvement" From 433c90c629c146ceed0faf2614cd8bb8fa452b5c Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 10:02:15 -0500 Subject: [PATCH 4/8] fix(gh-cli): clarify argument count errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/get-my-pull-requests.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index 4a5c45e..3fc4e3f 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -33,7 +33,11 @@ search_pull_requests() { fi } -[[ $# -le 1 ]] || { usage; exit 1; } +if [[ $# -gt 1 ]]; then + echo "Error: Expected at most one argument, but received $#." >&2 + usage + exit 1 +fi command -v gh >/dev/null 2>&1 || fail "Required command not found: gh" gh auth status >/dev/null 2>&1 || From 18e0a870c1759f17d6a81bab2643b88485599d2c Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 10:15:31 -0500 Subject: [PATCH 5/8] fix(gh-cli): handle search API limits safely Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/README.md | 2 +- gh-cli/get-my-pull-requests.sh | 73 ++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/gh-cli/README.md b/gh-cli/README.md index 8b30a3a..db4d855 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -1010,7 +1010,7 @@ Output groups include: - Other involvement - remaining PRs you were mentioned in or commented on > [!NOTE] -> Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API. +> Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API and exits with an error if any group exceeds the API's 1,000-result limit. ### get-organization-active-repositories.sh diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index 3fc4e3f..a73a32f 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -23,14 +23,57 @@ fail() { } search_pull_requests() { - local search_query="$1" + local key number owner repository results search_query title total_count + search_query="$1" - if ! gh api --method GET --paginate /search/issues \ + if ! total_count=$(gh api --method GET /search/issues \ + -f q="$search_query" \ + -F per_page=1 \ + --jq '.total_count'); then + fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" + fi + + if [[ "$total_count" -gt 1000 ]]; then + fail "Search matched $total_count pull requests, but GitHub Search only exposes 1,000 results. The report stopped before completion." + fi + + if ! results=$(gh api --method GET --paginate /search/issues \ -f q="$search_query" \ -F per_page=100 \ - --jq '.items[] | [.number, .title, (.repository_url | sub("^.*/repos/"; ""))] | @tsv'; then + -f sort=created \ + -f order=desc \ + --jq '.items[] | [(.repository_url | sub("^.*/repos/"; "")), .number, .title] | @tsv'); then fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" fi + + [[ -n "$results" ]] || return + + while IFS=$'\t' read -r repository number title; do + key="$repository#$number" + if [[ "$seen_pull_requests" == *$'\n'"$key"$'\n'* ]]; then + continue + fi + seen_pull_requests+="$key"$'\n' + + owner="${repository%%/*}" + if ! is_excluded_organization "$owner"; then + printf '%s\t%s\t%s\n' "$number" "$title" "$repository" + fi + done <<< "$results" +} + +is_excluded_organization() { + local organization owner="$1" + + [[ -n "$exclude_orgs" ]] || return 1 + + for organization in "${organizations[@]}"; do + if [[ "$owner" == "$organization" ]]; then + return 0 + fi + done + + return 1 } if [[ $# -gt 1 ]]; then @@ -46,33 +89,37 @@ gh auth status >/dev/null 2>&1 || exclude_orgs="${1:-}" user=$(gh api user --jq '.login') -exclusion="" +organizations=() if [ -n "$exclude_orgs" ]; then - IFS=',' read -ra orgs <<< "$exclude_orgs" - for org in "${orgs[@]}"; do - [[ "$org" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]] || - fail "Invalid organization name: $org" - exclusion="$exclusion -org:$org" + [[ "$exclude_orgs" != ,* && "$exclude_orgs" != *, && "$exclude_orgs" != *,,* ]] || + fail "Excluded organizations must be a comma-separated list without empty values." + IFS=',' read -ra organizations <<< "$exclude_orgs" + for organization in "${organizations[@]}"; do + [[ "$organization" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,37}[A-Za-z0-9])?$ ]] || + fail "Invalid organization name: $organization" done fi +shopt -s nocasematch +seen_pull_requests=$'\n' + echo "Fetching open pull requests for @$user..." >&2 echo "" >&2 echo "## Created by me" -search_pull_requests "author:$user is:pr is:open$exclusion" +search_pull_requests "author:$user is:pr is:open" echo "" >&2 echo "## Assigned to me" -search_pull_requests "assignee:$user -author:$user is:pr is:open$exclusion" +search_pull_requests "assignee:$user -author:$user is:pr is:open" echo "" >&2 echo "## Awaiting my review (requested reviewer)" -search_pull_requests "review-requested:$user -author:$user -assignee:$user is:pr is:open$exclusion" +search_pull_requests "review-requested:$user -author:$user -assignee:$user is:pr is:open" echo "" >&2 echo "## Other involvement" -search_pull_requests "involves:$user -author:$user -assignee:$user -review-requested:$user is:pr is:open$exclusion" +search_pull_requests "involves:$user -author:$user -assignee:$user -review-requested:$user is:pr is:open" echo "" >&2 echo "Done!" >&2 From a60a9d34f271f2ae77d181d571efae7175fb4720 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 10:35:36 -0500 Subject: [PATCH 6/8] fix(gh-cli): reject incomplete search results Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/README.md | 2 +- gh-cli/get-my-pull-requests.sh | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/gh-cli/README.md b/gh-cli/README.md index db4d855..02b03aa 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -1010,7 +1010,7 @@ Output groups include: - Other involvement - remaining PRs you were mentioned in or commented on > [!NOTE] -> Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API and exits with an error if any group exceeds the API's 1,000-result limit. +> Requires GitHub CLI authentication with access to the repositories being searched. Uses the search API and exits with an error if results are incomplete or any group exceeds the API's 1,000-result limit. ### get-organization-active-repositories.sh diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index a73a32f..a696950 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -23,15 +23,20 @@ fail() { } search_pull_requests() { - local key number owner repository results search_query title total_count + local incomplete_results key number owner repository results search_metadata search_query title total_count search_query="$1" - if ! total_count=$(gh api --method GET /search/issues \ + if ! search_metadata=$(gh api --method GET /search/issues \ -f q="$search_query" \ -F per_page=1 \ - --jq '.total_count'); then + --jq '[.total_count, .incomplete_results] | @tsv'); then fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" fi + IFS=$'\t' read -r total_count incomplete_results <<< "$search_metadata" + + if [[ "$incomplete_results" == "true" ]]; then + fail "GitHub Search timed out while counting pull requests. Run the report again." + fi if [[ "$total_count" -gt 1000 ]]; then fail "Search matched $total_count pull requests, but GitHub Search only exposes 1,000 results. The report stopped before completion." @@ -42,13 +47,22 @@ search_pull_requests() { -F per_page=100 \ -f sort=created \ -f order=desc \ - --jq '.items[] | [(.repository_url | sub("^.*/repos/"; "")), .number, .title] | @tsv'); then + --jq '(["__SEARCH_STATUS__", (.incomplete_results | tostring), ""] | @tsv), + (.items[] | [(.repository_url | sub("^.*/repos/"; "")), .number, .title] | @tsv)'); then fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" fi [[ -n "$results" ]] || return + while IFS=$'\t' read -r repository incomplete_results title; do + if [[ "$repository" == "__SEARCH_STATUS__" && "$incomplete_results" == "true" ]]; then + fail "GitHub Search timed out while retrieving pull requests. Run the report again." + fi + done <<< "$results" + while IFS=$'\t' read -r repository number title; do + [[ "$repository" != "__SEARCH_STATUS__" ]] || continue + key="$repository#$number" if [[ "$seen_pull_requests" == *$'\n'"$key"$'\n'* ]]; then continue From c6ce23567e865f5b751160d53e7a831ec00d8b0c Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 11:03:10 -0500 Subject: [PATCH 7/8] fix(gh-cli): verify retrieval result counts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/get-my-pull-requests.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index a696950..be6ec66 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -23,7 +23,7 @@ fail() { } search_pull_requests() { - local incomplete_results key number owner repository results search_metadata search_query title total_count + local incomplete_results key number owner repository results retrieval_count search_metadata search_query title total_count search_query="$1" if ! search_metadata=$(gh api --method GET /search/issues \ @@ -47,16 +47,21 @@ search_pull_requests() { -F per_page=100 \ -f sort=created \ -f order=desc \ - --jq '(["__SEARCH_STATUS__", (.incomplete_results | tostring), ""] | @tsv), + --jq '(["__SEARCH_STATUS__", (.incomplete_results | tostring), (.total_count | tostring)] | @tsv), (.items[] | [(.repository_url | sub("^.*/repos/"; "")), .number, .title] | @tsv)'); then fail "Pull request search failed. Check repository access and API rate limits with: gh api rate_limit" fi [[ -n "$results" ]] || return - while IFS=$'\t' read -r repository incomplete_results title; do - if [[ "$repository" == "__SEARCH_STATUS__" && "$incomplete_results" == "true" ]]; then - fail "GitHub Search timed out while retrieving pull requests. Run the report again." + while IFS=$'\t' read -r repository incomplete_results retrieval_count; do + if [[ "$repository" == "__SEARCH_STATUS__" ]]; then + if [[ "$incomplete_results" == "true" ]]; then + fail "GitHub Search timed out while retrieving pull requests. Run the report again." + fi + if [[ "$retrieval_count" -gt 1000 ]]; then + fail "Search grew to $retrieval_count pull requests during retrieval, but GitHub Search only exposes 1,000 results. The report stopped before completion." + fi fi done <<< "$results" From de1c164901b30afcecd294d0f17dccaaf4b16ba3 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Mon, 31 Aug 2026 13:21:08 -0500 Subject: [PATCH 8/8] fix(gh-cli): validate active GitHub authentication Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gh-cli/get-my-pull-requests.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gh-cli/get-my-pull-requests.sh b/gh-cli/get-my-pull-requests.sh index be6ec66..d61750d 100755 --- a/gh-cli/get-my-pull-requests.sh +++ b/gh-cli/get-my-pull-requests.sh @@ -102,11 +102,11 @@ if [[ $# -gt 1 ]]; then fi command -v gh >/dev/null 2>&1 || fail "Required command not found: gh" -gh auth status >/dev/null 2>&1 || - fail "GitHub CLI is not authenticated. Run: gh auth login" exclude_orgs="${1:-}" -user=$(gh api user --jq '.login') +if ! user=$(gh api user --jq '.login'); then + fail "Unable to authenticate with GitHub. Run: gh auth login" +fi organizations=() if [ -n "$exclude_orgs" ]; then