Created
September 6, 2025 14:21
-
-
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.
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
| #!/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 "$@" |
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
| # 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