bytelyst-devops-tools/remove_user_from_repos.sh

445 lines
12 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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