#!/bin/bash # Universal GitHub User Removal Script # Removes a specified user from all repositories matching a prefix pattern # under a given username or organization set -euo pipefail # Color definitions readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly CYAN='\033[0;36m' readonly BOLD='\033[1m' readonly NC='\033[0m' # No Color # Global variables GITHUB_TOKEN="" ROOT_USER="" USER_TO_REMOVE="" REPO_PREFIX="" DRY_RUN=false VERBOSE=false # Statistics TOTAL_REPOS_FOUND=0 MATCHING_REPOS=0 COLLABORATOR_REPOS=0 SUCCESSFUL_REMOVALS=0 FAILED_REMOVALS=0 ALREADY_REMOVED=0 # Utility functions log_info() { echo -e "${BLUE}ℹ️ $1${NC}" } log_success() { echo -e "${GREEN}✅ $1${NC}" } log_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } log_error() { echo -e "${RED}❌ $1${NC}" } log_verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${CYAN}🔍 $1${NC}" fi } log_header() { echo -e "\n${BOLD}${CYAN}$1${NC}" echo -e "${CYAN}$(printf '=%.0s' {1..60})${NC}" } show_usage() { cat << EOF ${BOLD}GitHub User Removal Script${NC} ${YELLOW}Usage:${NC} $0 -t TOKEN -r ROOT_USER -u USER_TO_REMOVE -p REPO_PREFIX [OPTIONS] ${YELLOW}Required Parameters:${NC} -t, --token TOKEN GitHub Personal Access Token -r, --root ROOT_USER Root GitHub username or organization -u, --user USER_TO_REMOVE Username to remove from repositories -p, --prefix REPO_PREFIX Repository name prefix (use '*' for all repos) ${YELLOW}Optional Parameters:${NC} -d, --dry-run Show what would be done without making changes -v, --verbose Enable verbose logging -h, --help Show this help message ${YELLOW}Examples:${NC} # Remove user from all repositories starting with 'bytelyst-' $0 -t "\$GITHUB_TOKEN" -r "saravanakumardb" -u "i-ayushh18" -p "bytelyst-" # Remove user from all repositories (dry run) $0 -t "\$GITHUB_TOKEN" -r "myorg" -u "olduser" -p "*" --dry-run # Remove user with verbose output $0 -t "\$GITHUB_TOKEN" -r "myorg" -u "olduser" -p "project-" -v ${YELLOW}Repository Prefix Patterns:${NC} "bytelyst-" - Matches repos starting with 'bytelyst-' "*api*" - Matches repos containing 'api' "*" - Matches all repositories "web-app" - Matches repos starting with 'web-app' EOF } # Parse command line arguments parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in -t|--token) GITHUB_TOKEN="$2" shift 2 ;; -r|--root) ROOT_USER="$2" shift 2 ;; -u|--user) USER_TO_REMOVE="$2" shift 2 ;; -p|--prefix) REPO_PREFIX="$2" shift 2 ;; -d|--dry-run) DRY_RUN=true shift ;; -v|--verbose) VERBOSE=true shift ;; -h|--help) show_usage exit 0 ;; *) log_error "Unknown option: $1" show_usage exit 1 ;; esac done } # Validate required parameters validate_parameters() { local missing_params=() [[ -z "$GITHUB_TOKEN" ]] && missing_params+=("token (-t)") [[ -z "$ROOT_USER" ]] && missing_params+=("root user (-r)") [[ -z "$USER_TO_REMOVE" ]] && missing_params+=("user to remove (-u)") [[ -z "$REPO_PREFIX" ]] && missing_params+=("repository prefix (-p)") if [[ ${#missing_params[@]} -gt 0 ]]; then log_error "Missing required parameters: ${missing_params[*]}" echo "" show_usage exit 1 fi } # Validate GitHub token validate_token() { log_info "Validating GitHub token..." local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" 2>/dev/null) local login login=$(echo "$response" | jq -r '.login // empty' 2>/dev/null) if [[ -z "$login" ]]; then log_error "Invalid or expired GitHub token" log_error "Please ensure your token has 'repo' and 'admin:org' permissions" exit 1 fi log_success "Token validated (authenticated as: $login)" } # Fetch all repositories for a user or organization fetch_all_repos() { local owner="$1" local all_repos=() local page=1 log_verbose "Fetching repositories for: $owner" while true; do local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/users/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null) # Check if response is valid JSON if ! echo "$response" | jq empty 2>/dev/null; then log_warning "Invalid response from GitHub API for page $page" break fi # Check for errors local error_message error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null) if [[ -n "$error_message" ]]; then if [[ "$error_message" == "Not Found" ]]; then # Try as organization log_verbose "User not found, trying as organization..." response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/orgs/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null) if ! echo "$response" | jq empty 2>/dev/null; then log_error "Could not fetch repositories for '$owner' (not found as user or organization)" exit 1 fi else log_error "GitHub API error: $error_message" exit 1 fi fi local repos repos=$(echo "$response" | jq -r '.[].full_name // empty' 2>/dev/null) if [[ -z "$repos" ]]; then break fi while IFS= read -r repo; do if [[ -n "$repo" && "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") log_verbose "Found repository: $repo" fi done <<< "$repos" ((page++)) done TOTAL_REPOS_FOUND=${#all_repos[@]} if [[ ${#all_repos[@]} -gt 0 ]]; then printf '%s\n' "${all_repos[@]}" fi } # Check if repository name matches prefix pattern matches_prefix() { local repo_full_name="$1" local pattern="$2" local repo_name repo_name=$(basename "$repo_full_name") # Convert shell wildcard pattern to bash pattern case "$pattern" in "*") return 0 # Match all ;; *"*"*) # Pattern contains wildcards if [[ "$repo_name" == $pattern ]]; then return 0 fi ;; *) # Simple prefix match if [[ "$repo_name" == "$pattern"* ]]; then return 0 fi ;; esac return 1 } # Check if user is a collaborator on a repository is_collaborator() { local repo="$1" local user="$2" local response response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null) local http_code="${response: -3}" case "$http_code" in 204) return 0 # User is a collaborator ;; 404) return 1 # User is not a collaborator ;; 403) log_verbose "Access forbidden for $repo (insufficient permissions)" return 1 ;; *) log_verbose "Failed to check collaboration status for $user on $repo (HTTP: $http_code)" return 1 ;; esac } # Remove user from repository remove_user_from_repo() { local repo="$1" local user="$2" if [[ "$DRY_RUN" == "true" ]]; then log_info "[DRY RUN] Would remove $user from $repo" return 0 fi log_verbose "Removing $user from $repo..." local response response=$(curl -s -w "%{http_code}" -X DELETE \ -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null) local http_code="${response: -3}" case "$http_code" in 204) log_success "Successfully removed $user from $repo" ((SUCCESSFUL_REMOVALS++)) return 0 ;; 404) log_info "User $user was not a collaborator on $repo (already removed)" ((ALREADY_REMOVED++)) return 0 ;; *) log_error "Failed to remove $user from $repo (HTTP: $http_code)" ((FAILED_REMOVALS++)) return 1 ;; esac } # Show final summary show_summary() { log_header "Operation Summary" echo -e "${BOLD}Repositories scanned:${NC} $TOTAL_REPOS_FOUND" echo -e "${BOLD}Repositories matching prefix:${NC} $MATCHING_REPOS" echo -e "${BOLD}Repositories where user was collaborator:${NC} $COLLABORATOR_REPOS" echo "" echo -e "${GREEN}Successful removals:${NC} $SUCCESSFUL_REMOVALS" echo -e "${YELLOW}Already removed:${NC} $ALREADY_REMOVED" echo -e "${RED}Failed removals:${NC} $FAILED_REMOVALS" if [[ $((SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS)) -gt 0 ]]; then local success_rate=$((((SUCCESSFUL_REMOVALS + ALREADY_REMOVED) * 100) / (SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS))) echo -e "${CYAN}Success rate:${NC} ${success_rate}%" fi if [[ "$DRY_RUN" == "true" ]]; then echo "" log_warning "This was a dry run - no actual changes were made" log_info "Run without --dry-run to perform the actual removals" fi } # Main execution function main() { log_header "GitHub User Removal Script" # Parse and validate arguments parse_arguments "$@" validate_parameters # Show operation details log_info "Root user/organization: $ROOT_USER" log_info "User to remove: $USER_TO_REMOVE" log_info "Repository prefix: $REPO_PREFIX" if [[ "$DRY_RUN" == "true" ]]; then log_warning "DRY RUN MODE - No changes will be made" fi # Validate token validate_token # Fetch all repositories log_header "Repository Discovery" local repos_output repos_output=$(fetch_all_repos "$ROOT_USER") if [[ -z "$repos_output" ]]; then log_warning "No repositories found for '$ROOT_USER'" exit 0 fi log_success "Found $TOTAL_REPOS_FOUND repositories" # Filter repositories by prefix log_header "Filtering Repositories" local matching_repos=() while IFS= read -r repo; do if [[ -n "$repo" ]] && matches_prefix "$repo" "$REPO_PREFIX"; then matching_repos+=("$repo") log_verbose "Matched: $repo" fi done <<< "$repos_output" MATCHING_REPOS=${#matching_repos[@]} if [[ $MATCHING_REPOS -eq 0 ]]; then log_warning "No repositories match the prefix pattern '$REPO_PREFIX'" log_info "Available repositories:" while IFS= read -r repo; do log_info " • $(basename "$repo")" done <<< "$repos_output" exit 0 fi log_success "Found $MATCHING_REPOS repositories matching prefix '$REPO_PREFIX'" # Check collaborator status and remove log_header "Processing Repositories" local current=0 for repo in "${matching_repos[@]}"; do ((current++)) printf "\r${CYAN}Progress: [%3d%%] %d/%d repositories processed${NC}" \ $((current * 100 / MATCHING_REPOS)) "$current" "$MATCHING_REPOS" if is_collaborator "$repo" "$USER_TO_REMOVE"; then ((COLLABORATOR_REPOS++)) echo # New line after progress remove_user_from_repo "$repo" "$USER_TO_REMOVE" fi done echo # New line after progress # Show final summary show_summary if [[ $FAILED_REMOVALS -gt 0 ]]; then exit 1 else log_success "Operation completed successfully!" exit 0 fi } # Script execution if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi