Files
thrillwiki_django_no_react/scripts/vm/github-setup.py
pacnpal f4f8ec8f9b Configure PostgreSQL with PostGIS support
- Updated database settings to use dj_database_url for environment-based configuration
- Added dj-database-url dependency
- Configured PostGIS backend for spatial data support
- Set default DATABASE_URL for production PostgreSQL connection
2025-08-19 18:51:33 -04:00

632 lines
22 KiB
Python
Executable File
Raw 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.
#!/usr/bin/env python3
"""
ThrillWiki GitHub PAT Setup Helper
Interactive script for setting up GitHub Personal Access Tokens with proper validation
and integration with the automation system.
Features:
- Guided GitHub PAT creation process
- Token validation and permission checking
- Integration with existing github-auth.py patterns
- Clear instructions for PAT scope requirements
- Secure token storage with proper file permissions
"""
import os
import sys
import json
import time
import getpass
import requests
import argparse
import subprocess
from pathlib import Path
from urllib.parse import urlencode
# Configuration
SCRIPT_DIR = Path(__file__).parent
PROJECT_DIR = SCRIPT_DIR.parent.parent
CONFIG_SCRIPT = SCRIPT_DIR / "automation-config.sh"
GITHUB_AUTH_SCRIPT = PROJECT_DIR / "scripts" / "github-auth.py"
TOKEN_FILE = PROJECT_DIR / ".github-pat"
# GitHub API Configuration
GITHUB_API_BASE = "https://api.github.com"
REQUEST_TIMEOUT = 30
# Token scope requirements for different use cases
TOKEN_SCOPES = {
"public": {
"description": "Public repositories only",
"scopes": ["public_repo"],
"note": "Suitable for public repositories and basic automation"
},
"private": {
"description": "Private repositories access",
"scopes": ["repo"],
"note": "Required for private repositories and full automation features"
},
"full": {
"description": "Full automation capabilities",
"scopes": ["repo", "workflow", "read:org"],
"note": "Recommended for complete automation setup with GitHub Actions"
}
}
class Colors:
"""ANSI color codes for terminal output"""
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
PURPLE = '\033[0;35m'
CYAN = '\033[0;36m'
BOLD = '\033[1m'
NC = '\033[0m' # No Color
def print_colored(message, color=Colors.NC):
"""Print colored message to terminal"""
print(f"{color}{message}{Colors.NC}")
def print_error(message):
"""Print error message"""
print_colored(f"❌ Error: {message}", Colors.RED)
def print_success(message):
"""Print success message"""
print_colored(f"{message}", Colors.GREEN)
def print_warning(message):
"""Print warning message"""
print_colored(f"⚠️ Warning: {message}", Colors.YELLOW)
def print_info(message):
"""Print info message"""
print_colored(f" {message}", Colors.BLUE)
def print_step(step, total, message):
"""Print step progress"""
print_colored(f"\n[{step}/{total}] {message}", Colors.CYAN)
def validate_token_format(token):
"""Validate GitHub token format"""
if not token:
return False
# GitHub token patterns
patterns = [
lambda t: t.startswith('ghp_') and len(t) >= 40, # Classic PAT
lambda t: t.startswith('github_pat_') and len(t) >= 50, # Fine-grained PAT
lambda t: t.startswith('gho_') and len(t) >= 40, # OAuth token
lambda t: t.startswith('ghu_') and len(t) >= 40, # User token
lambda t: t.startswith('ghs_') and len(t) >= 40, # Server token
]
return any(pattern(token) for pattern in patterns)
def test_github_token(token, timeout=REQUEST_TIMEOUT):
"""Test GitHub token by making API call"""
if not token:
return False, "No token provided"
try:
headers = {
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
response = requests.get(
f"{GITHUB_API_BASE}/user",
headers=headers,
timeout=timeout
)
if response.status_code == 200:
user_data = response.json()
return True, f"Valid token for user: {user_data.get('login', 'unknown')}"
elif response.status_code == 401:
return False, "Invalid or expired token"
elif response.status_code == 403:
return False, "Token lacks required permissions"
else:
return False, f"API request failed with HTTP {response.status_code}"
except requests.exceptions.RequestException as e:
return False, f"Network error: {str(e)}"
def get_token_permissions(token, timeout=REQUEST_TIMEOUT):
"""Get token permissions and scopes"""
if not token:
return None, "No token provided"
try:
headers = {
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
# Get user info and check token in response headers
response = requests.get(
f"{GITHUB_API_BASE}/user",
headers=headers,
timeout=timeout
)
if response.status_code == 200:
scopes = response.headers.get('X-OAuth-Scopes', '').split(', ')
scopes = [scope.strip() for scope in scopes if scope.strip()]
return scopes, None
else:
return None, f"Failed to get permissions: HTTP {response.status_code}"
except requests.exceptions.RequestException as e:
return None, f"Network error: {str(e)}"
def check_repository_access(token, repo_url=None, timeout=REQUEST_TIMEOUT):
"""Check if token can access the repository"""
if not token:
return False, "No token provided"
# Try to determine repository from git remote
if not repo_url:
try:
result = subprocess.run(
['git', 'remote', 'get-url', 'origin'],
cwd=PROJECT_DIR,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
repo_url = result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
if not repo_url:
return None, "Could not determine repository URL"
# Extract owner/repo from URL
if 'github.com' in repo_url:
# Handle both SSH and HTTPS URLs
if repo_url.startswith('git@github.com:'):
repo_path = repo_url.replace('git@github.com:', '').replace('.git', '')
elif 'github.com/' in repo_url:
repo_path = repo_url.split('github.com/')[-1].replace('.git', '')
else:
return None, "Could not parse repository URL"
try:
headers = {
'Authorization': f'Bearer {token}',
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
response = requests.get(
f"{GITHUB_API_BASE}/repos/{repo_path}",
headers=headers,
timeout=timeout
)
if response.status_code == 200:
repo_data = response.json()
return True, f"Access confirmed for {repo_data.get('full_name', repo_path)}"
elif response.status_code == 404:
return False, "Repository not found or no access"
elif response.status_code == 403:
return False, "Access denied - insufficient permissions"
else:
return False, f"Access check failed: HTTP {response.status_code}"
except requests.exceptions.RequestException as e:
return None, f"Network error: {str(e)}"
return None, "Not a GitHub repository"
def show_pat_instructions():
"""Show detailed PAT creation instructions"""
print_colored("\n" + "="*60, Colors.BOLD)
print_colored("GitHub Personal Access Token (PAT) Setup Guide", Colors.BOLD)
print_colored("="*60, Colors.BOLD)
print("\n🔐 Why do you need a GitHub PAT?")
print(" • Access private repositories")
print(" • Avoid GitHub API rate limits")
print(" • Enable automated repository operations")
print(" • Secure authentication without passwords")
print("\n📋 Step-by-step PAT creation:")
print(" 1. Go to: https://github.com/settings/tokens")
print(" 2. Click 'Generate new token''Generate new token (classic)'")
print(" 3. Enter a descriptive note: 'ThrillWiki Automation'")
print(" 4. Set expiration (recommended: 90 days for security)")
print(" 5. Select appropriate scopes:")
print("\n🎯 Recommended scope configurations:")
for scope_type, config in TOKEN_SCOPES.items():
print(f"\n {scope_type.upper()} REPOSITORIES:")
print(f" • Description: {config['description']}")
print(f" • Required scopes: {', '.join(config['scopes'])}")
print(f" • Note: {config['note']}")
print("\n⚡ Quick setup for most users:")
print(" • Select 'repo' scope for full repository access")
print(" • This enables all automation features")
print("\n🔒 Security best practices:")
print(" • Use descriptive token names")
print(" • Set reasonable expiration dates")
print(" • Regenerate tokens regularly")
print(" • Never share tokens in public")
print(" • Delete unused tokens immediately")
print("\n📱 After creating your token:")
print(" • Copy the token immediately (it won't be shown again)")
print(" • Return to this script and paste it when prompted")
print(" • The script will validate and securely store your token")
def interactive_token_setup():
"""Interactive token setup process"""
print_colored("\n🚀 ThrillWiki GitHub PAT Setup", Colors.BOLD)
print_colored("================================", Colors.BOLD)
# Check if token already exists
if TOKEN_FILE.exists():
try:
existing_token = TOKEN_FILE.read_text().strip()
if existing_token:
print_info("Existing GitHub token found")
# Test existing token
valid, message = test_github_token(existing_token)
if valid:
print_success(f"Current token is valid: {message}")
choice = input("\nDo you want to replace the existing token? (y/N): ").strip().lower()
if choice not in ['y', 'yes']:
print_info("Keeping existing token")
return True
else:
print_warning(f"Current token is invalid: {message}")
print_info("Setting up new token...")
except Exception as e:
print_warning(f"Could not read existing token: {e}")
# Show instructions
print("\n" + "="*50)
choice = input("Do you want to see PAT creation instructions? (Y/n): ").strip().lower()
if choice not in ['n', 'no']:
show_pat_instructions()
# Get token from user
print_step(1, 3, "Enter your GitHub Personal Access Token")
print("📋 Please paste your GitHub PAT below:")
print(" (Input will be hidden for security)")
while True:
try:
token = getpass.getpass("GitHub PAT: ").strip()
if not token:
print_error("No token entered. Please try again.")
continue
# Validate format
if not validate_token_format(token):
print_error("Invalid token format. GitHub tokens should start with 'ghp_', 'github_pat_', etc.")
retry = input("Try again? (Y/n): ").strip().lower()
if retry in ['n', 'no']:
return False
continue
break
except KeyboardInterrupt:
print("\nSetup cancelled by user")
return False
# Test token
print_step(2, 3, "Validating GitHub token")
print("🔍 Testing token with GitHub API...")
valid, message = test_github_token(token)
if not valid:
print_error(f"Token validation failed: {message}")
return False
print_success(message)
# Check permissions
print("🔐 Checking token permissions...")
scopes, error = get_token_permissions(token)
if error:
print_warning(f"Could not check permissions: {error}")
else:
print_success(f"Token scopes: {', '.join(scopes) if scopes else 'None detected'}")
# Check for recommended scopes
has_repo = 'repo' in scopes or 'public_repo' in scopes
if not has_repo:
print_warning("Token may lack repository access permissions")
# Check repository access
print("📁 Checking repository access...")
access, access_message = check_repository_access(token)
if access is True:
print_success(access_message)
elif access is False:
print_warning(access_message)
else:
print_info(access_message or "Repository access check skipped")
# Store token
print_step(3, 3, "Storing GitHub token securely")
try:
# Backup existing token if it exists
if TOKEN_FILE.exists():
backup_file = TOKEN_FILE.with_suffix('.backup')
TOKEN_FILE.rename(backup_file)
print_info(f"Existing token backed up to: {backup_file}")
# Write new token
TOKEN_FILE.write_text(token)
TOKEN_FILE.chmod(0o600) # Read/write for owner only
print_success(f"Token stored securely in: {TOKEN_FILE}")
# Try to update configuration via config script
try:
if CONFIG_SCRIPT.exists():
subprocess.run([
'bash', '-c',
f'source {CONFIG_SCRIPT} && store_github_token "{token}"'
], check=False, capture_output=True)
print_success("Token added to automation configuration")
except Exception as e:
print_warning(f"Could not update automation config: {e}")
print_success("GitHub PAT setup completed successfully!")
return True
except Exception as e:
print_error(f"Failed to store token: {e}")
return False
def validate_existing_token():
"""Validate existing GitHub token"""
print_colored("\n🔍 GitHub Token Validation", Colors.BOLD)
print_colored("===========================", Colors.BOLD)
if not TOKEN_FILE.exists():
print_error("No GitHub token file found")
print_info(f"Expected location: {TOKEN_FILE}")
return False
try:
token = TOKEN_FILE.read_text().strip()
if not token:
print_error("Token file is empty")
return False
print_info("Validating stored token...")
# Format validation
if not validate_token_format(token):
print_error("Token format is invalid")
return False
print_success("Token format is valid")
# API validation
valid, message = test_github_token(token)
if not valid:
print_error(f"Token validation failed: {message}")
return False
print_success(message)
# Check permissions
scopes, error = get_token_permissions(token)
if error:
print_warning(f"Could not check permissions: {error}")
else:
print_success(f"Token scopes: {', '.join(scopes) if scopes else 'None detected'}")
# Check repository access
access, access_message = check_repository_access(token)
if access is True:
print_success(access_message)
elif access is False:
print_warning(access_message)
else:
print_info(access_message or "Repository access check inconclusive")
print_success("Token validation completed")
return True
except Exception as e:
print_error(f"Error reading token: {e}")
return False
def remove_token():
"""Remove stored GitHub token"""
print_colored("\n🗑️ GitHub Token Removal", Colors.BOLD)
print_colored("=========================", Colors.BOLD)
if not TOKEN_FILE.exists():
print_info("No GitHub token file found")
return True
try:
# Backup before removal
backup_file = TOKEN_FILE.with_suffix('.removed')
TOKEN_FILE.rename(backup_file)
print_success(f"Token removed and backed up to: {backup_file}")
# Try to remove from config
try:
if CONFIG_SCRIPT.exists():
subprocess.run([
'bash', '-c',
f'source {CONFIG_SCRIPT} && remove_github_token'
], check=False, capture_output=True)
print_success("Token removed from automation configuration")
except Exception as e:
print_warning(f"Could not update automation config: {e}")
print_success("GitHub token removed successfully")
return True
except Exception as e:
print_error(f"Error removing token: {e}")
return False
def show_token_status():
"""Show current token status"""
print_colored("\n📊 GitHub Token Status", Colors.BOLD)
print_colored("======================", Colors.BOLD)
# Check token file
print(f"📁 Token file: {TOKEN_FILE}")
if TOKEN_FILE.exists():
print_success("Token file exists")
# Check permissions
perms = oct(TOKEN_FILE.stat().st_mode)[-3:]
if perms == '600':
print_success(f"File permissions: {perms} (secure)")
else:
print_warning(f"File permissions: {perms} (should be 600)")
# Quick validation
try:
token = TOKEN_FILE.read_text().strip()
if token:
if validate_token_format(token):
print_success("Token format is valid")
# Quick API test
valid, message = test_github_token(token, timeout=10)
if valid:
print_success(f"Token is valid: {message}")
else:
print_error(f"Token is invalid: {message}")
else:
print_error("Token format is invalid")
else:
print_error("Token file is empty")
except Exception as e:
print_error(f"Error reading token: {e}")
else:
print_warning("Token file not found")
# Check config integration
print(f"\n⚙️ Configuration: {CONFIG_SCRIPT}")
if CONFIG_SCRIPT.exists():
print_success("Configuration script available")
else:
print_warning("Configuration script not found")
# Check existing GitHub auth script
print(f"\n🔐 GitHub auth script: {GITHUB_AUTH_SCRIPT}")
if GITHUB_AUTH_SCRIPT.exists():
print_success("GitHub auth script available")
else:
print_warning("GitHub auth script not found")
def main():
"""Main CLI interface"""
parser = argparse.ArgumentParser(
description="ThrillWiki GitHub PAT Setup Helper",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s setup # Interactive token setup
%(prog)s validate # Validate existing token
%(prog)s status # Show token status
%(prog)s remove # Remove stored token
%(prog)s --help # Show this help
For detailed PAT creation instructions, run: %(prog)s setup
"""
)
parser.add_argument(
'command',
choices=['setup', 'validate', 'status', 'remove', 'help'],
help='Command to execute'
)
parser.add_argument(
'--token',
help='GitHub token to validate (for validate command)'
)
parser.add_argument(
'--force',
action='store_true',
help='Force operation without prompts'
)
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
try:
if args.command == 'setup':
success = interactive_token_setup()
sys.exit(0 if success else 1)
elif args.command == 'validate':
if args.token:
# Validate provided token
print_info("Validating provided token...")
if validate_token_format(args.token):
valid, message = test_github_token(args.token)
if valid:
print_success(message)
sys.exit(0)
else:
print_error(message)
sys.exit(1)
else:
print_error("Invalid token format")
sys.exit(1)
else:
# Validate existing token
success = validate_existing_token()
sys.exit(0 if success else 1)
elif args.command == 'status':
show_token_status()
sys.exit(0)
elif args.command == 'remove':
if not args.force:
confirm = input("Are you sure you want to remove the GitHub token? (y/N): ").strip().lower()
if confirm not in ['y', 'yes']:
print_info("Operation cancelled")
sys.exit(0)
success = remove_token()
sys.exit(0 if success else 1)
elif args.command == 'help':
parser.print_help()
sys.exit(0)
except KeyboardInterrupt:
print("\nOperation cancelled by user")
sys.exit(1)
except Exception as e:
print_error(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()