From f47144f56f77a462e7f14d95aeb01192874de570 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Wed, 25 Feb 2026 14:54:14 -0600 Subject: [PATCH 1/5] refactor: improve get-repository-users-permission-and-source.sh output and usability - Add input parameters for org, repo, and affiliation instead of hardcoded values - Add input validation with usage message - Format output as aligned table with user, effective permission, and sources - Filter out redundant Repository sources inherited from org admin access - Apply heuristic to display MAINTAIN/TRIAGE roles correctly in sources --- ...-repository-users-permission-and-source.sh | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/gh-cli/get-repository-users-permission-and-source.sh b/gh-cli/get-repository-users-permission-and-source.sh index b24ec24..21d0cbd 100755 --- a/gh-cli/get-repository-users-permission-and-source.sh +++ b/gh-cli/get-repository-users-permission-and-source.sh @@ -1,13 +1,27 @@ #!/bin/bash -# gh cli's token needs to be able to admin org - run this first if it can't -# gh auth refresh -h github.com -s admin:org +# Returns the permission for everyone who can access a repository and how they +# access it (direct, team, organization) +# +# gh cli's token needs to be able to admin the organization - run this first if needed: +# gh auth refresh -h github.com -s admin:org +# +# Usage: +# ./get-repository-users-permission-and-source.sh [affiliation] +# +# affiliation can be: OUTSIDE, DIRECT, ALL (default: ALL) -# affiliation can be: OUTSIDE, DIRECT, ALL +if [ -z "$2" ]; then + echo "Usage: $0 [affiliation]" + echo " affiliation: OUTSIDE, DIRECT, ALL (default: ALL)" + exit 1 +fi -# returns the permission for everyone who can access the repo and how they access it (direct, team, org) +org="$1" +repo="$2" +affiliation="${3:-ALL}" -gh api graphql --paginate -f owner='joshjohanning-org' -f repo='ghas-demo' -f affiliation='ALL' -f query=' +gh api graphql --paginate -f owner="$org" -f repo="$repo" -f affiliation="$affiliation" -f query=' query ($owner: String!, $repo: String!, $affiliation: CollaboratorAffiliation!, $endCursor: String) { repository(owner:$owner, name:$repo) { name @@ -44,4 +58,22 @@ query ($owner: String!, $repo: String!, $affiliation: CollaboratorAffiliation!, } } } -}' +}' --jq ' + .data.repository.collaborators.edges[] | + .node.login as $user | + .permission as $effective | + (.permissionSources | map(select(.source.type == "Organization") | .permission)) as $org_perms | + [.permissionSources[] | + if .source.type == "Organization" then "org-member(\(.permission))" + elif .source.type == "Team" then "team:\(.source.name)(\(.permission))" + elif (.permission as $p | $org_perms | any(. == $p)) | not then + # permissionSources only returns READ/WRITE/ADMIN - use effective for MAINTAIN/TRIAGE + if .permission == "WRITE" and $effective == "MAINTAIN" then "direct(MAINTAIN)" + elif .permission == "READ" and $effective == "TRIAGE" then "direct(TRIAGE)" + else "direct(\(.permission))" + end + else empty + end + ] | unique | join(", ") | + "\($user) | \($effective) | \(.)" +' | (echo "USER | EFFECTIVE | SOURCES" && cat) | column -t -s '|' From 44f971d1d1a0cadff449acc885f53b3fd44e8811 Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Wed, 25 Feb 2026 15:00:23 -0600 Subject: [PATCH 2/5] docs: update comments to clarify GraphQL permissionSources API limitations --- gh-cli/get-repository-users-permission-and-source.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gh-cli/get-repository-users-permission-and-source.sh b/gh-cli/get-repository-users-permission-and-source.sh index 21d0cbd..10f675b 100755 --- a/gh-cli/get-repository-users-permission-and-source.sh +++ b/gh-cli/get-repository-users-permission-and-source.sh @@ -3,6 +3,11 @@ # Returns the permission for everyone who can access a repository and how they # access it (direct, team, organization) # +# Note: The GraphQL permissionSources API only returns READ, WRITE, and ADMIN - +# it does not support MAINTAIN or TRIAGE. A heuristic is applied to direct +# sources to correct this, but team sources may still show WRITE instead of +# MAINTAIN (or READ instead of TRIAGE) due to this API limitation. +# # gh cli's token needs to be able to admin the organization - run this first if needed: # gh auth refresh -h github.com -s admin:org # From ab19d9837cc35dbd96442cd1f60b0eed7cf14e4a Mon Sep 17 00:00:00 2001 From: Josh Johanning Date: Wed, 25 Feb 2026 16:46:57 -0600 Subject: [PATCH 3/5] refactor: enhance permission mapping and output formatting in get-repository-users-permission-and-source.sh --- ...-repository-users-permission-and-source.sh | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/gh-cli/get-repository-users-permission-and-source.sh b/gh-cli/get-repository-users-permission-and-source.sh index 10f675b..eaadaee 100755 --- a/gh-cli/get-repository-users-permission-and-source.sh +++ b/gh-cli/get-repository-users-permission-and-source.sh @@ -3,10 +3,9 @@ # Returns the permission for everyone who can access a repository and how they # access it (direct, team, organization) # -# Note: The GraphQL permissionSources API only returns READ, WRITE, and ADMIN - -# it does not support MAINTAIN or TRIAGE. A heuristic is applied to direct -# sources to correct this, but team sources may still show WRITE instead of -# MAINTAIN (or READ instead of TRIAGE) due to this API limitation. +# Uses the REST API to get accurate team role names (maintain, triage) since the +# GraphQL permissionSources API only returns READ, WRITE, and ADMIN. A heuristic +# is also applied to direct sources to correct MAINTAIN/TRIAGE labels. # # gh cli's token needs to be able to admin the organization - run this first if needed: # gh auth refresh -h github.com -s admin:org @@ -26,7 +25,29 @@ org="$1" repo="$2" affiliation="${3:-ALL}" -gh api graphql --paginate -f owner="$org" -f repo="$repo" -f affiliation="$affiliation" -f query=' +# Map REST permission names (pull/push) to GraphQL-style names (READ/WRITE) +map_permission() { + case "$1" in + pull) echo "READ" ;; + triage) echo "TRIAGE" ;; + push) echo "WRITE" ;; + maintain) echo "MAINTAIN" ;; + admin) echo "ADMIN" ;; + *) echo "$1" | tr '[:lower:]' '[:upper:]' ;; + esac +} + +# Get true team permissions via REST API and build a sed command to fix labels +sed_cmd="" +while IFS=$'\t' read -r slug perm; do + mapped=$(map_permission "$perm") + sed_cmd="${sed_cmd}s/team:${slug}\([^)]*\)/team:${slug}(${mapped})/g;" +done < Date: Wed, 25 Feb 2026 16:51:14 -0600 Subject: [PATCH 4/5] docs: enhance README and script comments for get-repository-users-permission-and-source.sh --- gh-cli/README.md | 11 ++++++++++- ...-repository-users-permission-and-source.sh | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/gh-cli/README.md b/gh-cli/README.md index 63f62eb..ac71ef6 100644 --- a/gh-cli/README.md +++ b/gh-cli/README.md @@ -1345,7 +1345,16 @@ joshjohanning,admin ### get-repository-users-permission-and-source.sh -Returns the permission for everyone who can access the repo and how they access it (direct, team, org) +Returns the permission for everyone who can access a repository and how they access it (direct, team, organization). Uses the REST API to get accurate team role names including maintain, triage, and custom roles. + +Example output: + +```text +USER EFFECTIVE SOURCES +joshjohanning ADMIN org-member(ADMIN), team:admin-team(WRITE), team:approver-team(WRITE) +FluffyCarlton MAINTAIN direct(MAINTAIN), org-member(READ) +joshgoldfishturtle ADMIN org-member(READ), team:compliance-team(ADMIN) +``` ### get-repository.sh diff --git a/gh-cli/get-repository-users-permission-and-source.sh b/gh-cli/get-repository-users-permission-and-source.sh index eaadaee..0c45e09 100755 --- a/gh-cli/get-repository-users-permission-and-source.sh +++ b/gh-cli/get-repository-users-permission-and-source.sh @@ -11,19 +11,30 @@ # gh auth refresh -h github.com -s admin:org # # Usage: -# ./get-repository-users-permission-and-source.sh [affiliation] +# ./get-repository-users-permission-and-source.sh [affiliation] [hostname] # # affiliation can be: OUTSIDE, DIRECT, ALL (default: ALL) +# hostname: GitHub hostname (default: github.com), e.g. github.example.com + +# Example output: +# +# USER EFFECTIVE SOURCES +# joshjohanning ADMIN org-member(ADMIN), team:admin-team(WRITE), team:approver-team(WRITE) +# FluffyCarlton MAINTAIN direct(MAINTAIN), org-member(READ) +# joshgoldfishturtle ADMIN org-member(READ), team:compliance-team(ADMIN) + if [ -z "$2" ]; then - echo "Usage: $0 [affiliation]" + echo "Usage: $0 [affiliation] [hostname]" echo " affiliation: OUTSIDE, DIRECT, ALL (default: ALL)" + echo " hostname: GitHub hostname (default: github.com)" exit 1 fi org="$1" repo="$2" affiliation="${3:-ALL}" +hostname="${4:-github.com}" # Map REST permission names (pull/push) to GraphQL-style names (READ/WRITE) map_permission() { @@ -43,11 +54,11 @@ while IFS=$'\t' read -r slug perm; do mapped=$(map_permission "$perm") sed_cmd="${sed_cmd}s/team:${slug}\([^)]*\)/team:${slug}(${mapped})/g;" done < Date: Wed, 25 Feb 2026 16:52:53 -0600 Subject: [PATCH 5/5] fix: validate affiliation input for get-repository-users-permission-and-source.sh --- gh-cli/get-repository-users-permission-and-source.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/gh-cli/get-repository-users-permission-and-source.sh b/gh-cli/get-repository-users-permission-and-source.sh index 0c45e09..a3c4680 100755 --- a/gh-cli/get-repository-users-permission-and-source.sh +++ b/gh-cli/get-repository-users-permission-and-source.sh @@ -33,9 +33,19 @@ fi org="$1" repo="$2" -affiliation="${3:-ALL}" +affiliation_input="${3:-ALL}" +affiliation="$(echo "$affiliation_input" | tr '[:lower:]' '[:upper:]')" hostname="${4:-github.com}" +case "$affiliation" in + OUTSIDE|DIRECT|ALL) + ;; + *) + echo "Error: invalid affiliation '$affiliation_input'. Must be OUTSIDE, DIRECT, or ALL." + exit 1 + ;; +esac + # Map REST permission names (pull/push) to GraphQL-style names (READ/WRITE) map_permission() { case "$1" in