Skip to content

Instantly share code, notes, and snippets.

@aarora79
Created September 6, 2025 14:21
Show Gist options
  • Select an option

  • Save aarora79/ddcef2acd5ecd4734b167675e06a425b to your computer and use it in GitHub Desktop.

Select an option

Save aarora79/ddcef2acd5ecd4734b167675e06a425b to your computer and use it in GitHub Desktop.
Script to bulk manage GitHub repo collaborator permissions - revokes and re-grants access with maintain level for classroom/organization repositories.
#!/bin/bash
# Script to update GitHub repository access for multiple users
# First revokes existing access, then adds back with maintain permission
#
# USAGE EXAMPLES:
# ==============
#
# Process a single user (mw1296) with default repo pattern:
# ./update_repo_access.sh -u mw1296
#
# Process multiple users from a file with default repo pattern:
# ./update_repo_access.sh -f users.txt
#
# Process a single user with custom repo pattern:
# ./update_repo_access.sh -u mw1296 -r 'gu-dsan6000/spring-2025-{gh_username}'
#
# Process multiple users from file with custom repo pattern:
# ./update_repo_access.sh -f users.txt -r 'gu-dsan6000/fall-2025-a02-{gh_username}'
#
# The {gh_username} placeholder will be replaced with the actual GitHub username
# Don't exit on error - we want to process all users
set +e
# Function for logging with timestamp
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Function to check if user has access to a repository
check_access() {
local repo=$1
local username=$2
log "Checking current access for $username on $repo..."
# Check if user is a collaborator
if gh api "repos/$repo/collaborators/$username" --silent 2>/dev/null; then
# Get current permission level
permission=$(gh api "repos/$repo/collaborators/$username/permission" 2>/dev/null | jq -r '.permission // "unknown"')
log " Current access: $permission"
return 0
else
log " No current access found"
return 1
fi
}
# Function to revoke access
revoke_access() {
local repo=$1
local username=$2
log "Revoking access for $username on $repo..."
if gh api "repos/$repo/collaborators/$username" --method DELETE 2>/dev/null; then
log " Access revoked successfully"
return 0
else
log " Failed to revoke access (may not have had access)"
return 1
fi
}
# Function to grant maintain access
grant_maintain_access() {
local repo=$1
local username=$2
log "Granting maintain access for $username on $repo..."
# Grant the permission (this might return an invitation or success)
# Note: GitHub API uses 'maintain' but invitation objects may show 'write'
response=$(gh api "repos/$repo/collaborators/$username" \
--method PUT \
--field permission=maintain 2>&1)
if [ $? -eq 0 ]; then
# Wait a moment for GitHub to process
sleep 1
# Check if this created an invitation (common for GitHub Classroom repos)
if echo "$response" | grep -q '"invitee"'; then
# This is an invitation response
invitation_permission=$(echo "$response" | jq -r '.permissions // "unknown"')
log " Invitation created with requested 'maintain' permission"
log " Note: Invitation may show 'write' but will grant 'maintain' when accepted"
log " User needs to accept the invitation to activate permissions"
return 0
else
# Direct collaborator add (user already has GitHub account linked)
actual_permission=$(gh api "repos/$repo/collaborators/$username/permission" 2>/dev/null | jq -r '.permission // "unknown"')
if [ "$actual_permission" = "maintain" ]; then
log " Maintain access granted successfully (verified: $actual_permission)"
return 0
elif [ "$actual_permission" = "unknown" ] || [ "$actual_permission" = "none" ]; then
# User might need to accept invitation first
log " Invitation sent for maintain access (user needs to accept)"
return 0
else
log " WARNING: Permission shows as '$actual_permission' (expected 'maintain')"
log " Note: This may be correct if user hasn't accepted invitation yet"
return 0
fi
fi
else
log " Failed to grant maintain access"
return 1
fi
}
# Function to process a single user
process_user() {
local username=$1
local repo_pattern=$2
# Construct repository path
local repo="${repo_pattern//\{gh_username\}/$username}"
log "Processing user: $username"
log "Repository: $repo"
# Check if repository exists
if ! gh api "repos/$repo" --silent 2>/dev/null; then
log " WARNING: Repository $repo does not exist or is not accessible - skipping"
return 2 # Return special code for skipped
fi
# Check current access
check_access "$repo" "$username"
# Revoke existing access (if any)
revoke_access "$repo" "$username"
# Wait a moment to ensure GitHub processes the revocation
sleep 1
# Grant maintain access
if grant_maintain_access "$repo" "$username"; then
log " SUCCESS: $username now has maintain access to $repo"
return 0
else
log " ERROR: Failed to grant maintain access to $username"
return 1
fi
}
# Main script
main() {
# Check if gh CLI is installed
if ! command -v gh &> /dev/null; then
log "ERROR: GitHub CLI (gh) is not installed"
log "Install it from: https://cli.github.com/"
exit 1
fi
# Check if authenticated
if ! gh auth status &>/dev/null; then
log "ERROR: Not authenticated with GitHub CLI"
log "Run: gh auth login"
exit 1
fi
# Check command line arguments
if [ $# -lt 1 ]; then
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -u USERNAME Process a single user"
echo " -f FILE Process users from a file (one username per line)"
echo " -r REPO_PATTERN Repository pattern with {gh_username} placeholder"
echo " (default: gu-dsan6000/fall-2025-a02-{gh_username})"
echo ""
echo "Examples:"
echo ""
echo " # Process a single user (mw1296) with default repo pattern:"
echo " $0 -u mw1296"
echo ""
echo " # Process multiple users from a file with default repo pattern:"
echo " $0 -f users.txt"
echo ""
echo " # Process a single user with custom repo pattern:"
echo " $0 -u mw1296 -r 'gu-dsan6000/spring-2025-{gh_username}'"
echo ""
echo " # Process multiple users from file with custom repo pattern:"
echo " $0 -f users.txt -r 'gu-dsan6000/fall-2025-a02-{gh_username}'"
echo ""
echo " # Common patterns:"
echo " -r 'gu-dsan6000/fall-2025-a02-{gh_username}' # Fall 2025 Assignment 02"
echo " -r 'gu-dsan6000/spring-2025-{gh_username}' # Spring 2025"
echo " -r 'orgname/repo-{gh_username}' # Generic pattern"
exit 1
fi
# Default repository pattern
REPO_PATTERN="gu-dsan6000/fall-2025-a02-{gh_username}"
USERS_FILE=""
SINGLE_USER=""
# Parse command line options
while getopts "u:f:r:" opt; do
case $opt in
u)
SINGLE_USER="$OPTARG"
;;
f)
USERS_FILE="$OPTARG"
;;
r)
REPO_PATTERN="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
log "Starting GitHub repository access update"
log "Repository pattern: $REPO_PATTERN"
log "Permission level: maintain"
# Track results
SUCCESS_COUNT=0
FAILED_COUNT=0
SKIPPED_COUNT=0
FAILED_USERS=()
SKIPPED_REPOS=()
# Process single user or file
if [ -n "$SINGLE_USER" ]; then
# Process single user
repo="${REPO_PATTERN//\{gh_username\}/$SINGLE_USER}"
process_user "$SINGLE_USER" "$REPO_PATTERN"
result=$?
if [ $result -eq 0 ]; then
((SUCCESS_COUNT++))
elif [ $result -eq 2 ]; then
((SKIPPED_COUNT++))
SKIPPED_REPOS+=("https://github.com/$repo")
else
((FAILED_COUNT++))
FAILED_USERS+=("$SINGLE_USER - https://github.com/$repo")
fi
elif [ -n "$USERS_FILE" ]; then
# Process users from file
if [ ! -f "$USERS_FILE" ]; then
log "ERROR: File $USERS_FILE not found"
exit 1
fi
log "Processing users from file: $USERS_FILE"
while IFS= read -r username || [ -n "$username" ]; do
# Skip empty lines and comments
if [ -z "$username" ] || [[ "$username" == \#* ]]; then
continue
fi
# Trim whitespace
username=$(echo "$username" | xargs)
# Generate repo name for this user
repo="${REPO_PATTERN//\{gh_username\}/$username}"
# Call process_user and capture result
process_user "$username" "$REPO_PATTERN"
result=$?
if [ $result -eq 0 ]; then
((SUCCESS_COUNT++))
elif [ $result -eq 2 ]; then
((SKIPPED_COUNT++))
SKIPPED_REPOS+=("https://github.com/$repo")
else
((FAILED_COUNT++))
FAILED_USERS+=("$username - https://github.com/$repo")
fi
# Add a small delay between users to avoid rate limiting
sleep 1
log "---"
done < "$USERS_FILE"
fi
# Print summary
log "========================================="
log "SUMMARY"
log "========================================="
log "Successful updates: $SUCCESS_COUNT"
log "Skipped (repo not found): $SKIPPED_COUNT"
log "Failed updates: $FAILED_COUNT"
if [ ${#SKIPPED_REPOS[@]} -gt 0 ]; then
log ""
log "Repositories that do not exist:"
for repo_url in "${SKIPPED_REPOS[@]}"; do
log " - $repo_url"
done
fi
if [ ${#FAILED_USERS[@]} -gt 0 ]; then
log ""
log "Failed users (errors during processing):"
for user_info in "${FAILED_USERS[@]}"; do
log " - $user_info"
done
fi
log "========================================="
# Don't exit with error - let all users be processed
# Exit code 0 if we processed everything, even with some failures
if [ $SUCCESS_COUNT -eq 0 ] && [ $SKIPPED_COUNT -eq 0 ] && [ $FAILED_COUNT -gt 0 ]; then
# Only exit with error if ALL attempts failed
exit 1
fi
}
# Run main function
main "$@"
# Users file for update_repo_access.sh
# Add one GitHub username per line
# Lines starting with # are comments and will be ignored
username1
# Add more usernames below
# username2
# username3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment