|
|
@@ -0,0 +1,377 @@
|
|
|
+#!/usr/bin/env bash
|
|
|
+
|
|
|
+set -euo pipefail
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🔧 SETUP
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
+ENV_FILE="$SCRIPT_DIR/.env"
|
|
|
+RUNS_DIR="$SCRIPT_DIR/runs"
|
|
|
+LOGS_DIR="$SCRIPT_DIR/logs"
|
|
|
+
|
|
|
+# Colors
|
|
|
+RED='\033[0;31m'
|
|
|
+GREEN='\033[0;32m'
|
|
|
+BLUE='\033[0;34m'
|
|
|
+YELLOW='\033[0;33m'
|
|
|
+NC='\033[0m'
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🛠️ HELPERS
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+log() { echo -e "${GREEN}[RUN]${NC} $*"; }
|
|
|
+warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
|
+error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🔍 DEPENDENCY VALIDATION
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+check_dependencies() {
|
|
|
+ local missing=()
|
|
|
+
|
|
|
+ # Required tools
|
|
|
+ local required_tools=("git" "find" "grep")
|
|
|
+
|
|
|
+ # Optional but recommended tools
|
|
|
+ local optional_tools=("gitleaks")
|
|
|
+
|
|
|
+ log "Checking dependencies..."
|
|
|
+
|
|
|
+ # Check required tools
|
|
|
+ for tool in "${required_tools[@]}"; do
|
|
|
+ if ! command -v "$tool" &>/dev/null; then
|
|
|
+ missing+=("$tool")
|
|
|
+ fi
|
|
|
+ done
|
|
|
+
|
|
|
+ # Check optional tools
|
|
|
+ for tool in "${optional_tools[@]}"; do
|
|
|
+ if ! command -v "$tool" &>/dev/null; then
|
|
|
+ warn "Optional tool missing: $tool (recommended for security scanning)"
|
|
|
+ else
|
|
|
+ log "✓ Found: $tool"
|
|
|
+ fi
|
|
|
+ done
|
|
|
+
|
|
|
+ # Report missing required tools
|
|
|
+ if [[ ${#missing[@]} -gt 0 ]]; then
|
|
|
+ error "Missing required tools: ${missing[*]}"
|
|
|
+ error "Please install missing dependencies"
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+
|
|
|
+ log "✓ All required dependencies found"
|
|
|
+}
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🔒 SECURITY SCANNING
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+run_security_scan() {
|
|
|
+ log "Running security scan..."
|
|
|
+
|
|
|
+ if command -v gitleaks &>/dev/null; then
|
|
|
+ log "Using GitLeaks for secret detection..."
|
|
|
+ if gitleaks detect --verbose --exit-code 1; then
|
|
|
+ log "✅ No secrets detected"
|
|
|
+ return 0
|
|
|
+ else
|
|
|
+ error "❌ Secrets detected! Review before pushing."
|
|
|
+ return 1
|
|
|
+ fi
|
|
|
+ else
|
|
|
+ warn "GitLeaks not installed - skipping security scan"
|
|
|
+ warn "Install with: paru -S gitleaks"
|
|
|
+ echo
|
|
|
+ read -p "Continue without security scan? (y/N): " -r answer
|
|
|
+ if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
|
|
+ error "Push cancelled for security"
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+ return 0
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 📤 PUSH COMMAND
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+handle_push() {
|
|
|
+ log "Preparing to push repository..."
|
|
|
+
|
|
|
+ # Check if we're in a git repository
|
|
|
+ if ! git rev-parse --git-dir &>/dev/null; then
|
|
|
+ error "Not in a git repository"
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Check for uncommitted changes
|
|
|
+ if ! git diff-index --quiet HEAD --; then
|
|
|
+ warn "You have uncommitted changes"
|
|
|
+ echo
|
|
|
+ git status --short
|
|
|
+ echo
|
|
|
+
|
|
|
+ # Generate default commit message
|
|
|
+ local default_msg="dev: automated commit - $(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
+
|
|
|
+ read -p "Commit all changes? (Y/n): " -r answer
|
|
|
+ if [[ ! "$answer" =~ ^[Nn]$ ]]; then
|
|
|
+ echo
|
|
|
+ echo "Default: $default_msg"
|
|
|
+ read -p "Custom commit message (or press Enter for default): " -r commit_msg
|
|
|
+
|
|
|
+ # Use default if empty
|
|
|
+ if [[ -z "$commit_msg" ]]; then
|
|
|
+ commit_msg="$default_msg"
|
|
|
+ fi
|
|
|
+
|
|
|
+ git add .
|
|
|
+ git commit -m "$commit_msg"
|
|
|
+ log "✓ Changes committed: $commit_msg"
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Run security scan
|
|
|
+ if ! run_security_scan; then
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+
|
|
|
+ # Get current branch
|
|
|
+ local current_branch
|
|
|
+ current_branch=$(git branch --show-current)
|
|
|
+
|
|
|
+ # Push to origin
|
|
|
+ log "Pushing branch '$current_branch' to origin..."
|
|
|
+ if git push origin "$current_branch"; then
|
|
|
+ log "✅ Successfully pushed to origin/$current_branch"
|
|
|
+ else
|
|
|
+ error "❌ Push failed"
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+show_help() {
|
|
|
+ cat <<EOF
|
|
|
+Usage: $0 [OPTIONS] [COMMAND|FILTERS...]
|
|
|
+
|
|
|
+COMMANDS:
|
|
|
+ push Commit, scan for secrets, and push to git origin
|
|
|
+ deps, check Check for required dependencies
|
|
|
+ help Show this help message
|
|
|
+
|
|
|
+SCRIPT EXECUTION:
|
|
|
+ [FILTERS...] Run scripts matching filters from runs/ directory
|
|
|
+
|
|
|
+OPTIONS:
|
|
|
+ --dry Show what would run without executing
|
|
|
+ --verbose Show detailed output during execution
|
|
|
+
|
|
|
+EXAMPLES:
|
|
|
+ $0 deps # Check dependencies
|
|
|
+ $0 push # Secure git push with secret scanning
|
|
|
+ $0 docker # Run scripts containing 'docker'
|
|
|
+ $0 --dry base # Show what base scripts would run
|
|
|
+ $0 --verbose all # Run all scripts with detailed output
|
|
|
+
|
|
|
+SECURITY:
|
|
|
+ The push command automatically scans for secrets using GitLeaks
|
|
|
+ before pushing to prevent accidental credential exposure.
|
|
|
+
|
|
|
+EOF
|
|
|
+}
|
|
|
+
|
|
|
+get_description() {
|
|
|
+ local script="$1"
|
|
|
+ grep '^# NAME:' "$script" 2>/dev/null | cut -d: -f2- | sed 's/^ *//' || echo "No description"
|
|
|
+}
|
|
|
+
|
|
|
+matches_filters() {
|
|
|
+ local script_name="$1"
|
|
|
+ shift
|
|
|
+ local filters=("$@")
|
|
|
+
|
|
|
+ [[ ${#filters[@]} -eq 0 ]] && return 0
|
|
|
+
|
|
|
+ for filter in "${filters[@]}"; do
|
|
|
+ if [[ "$script_name" == *"$filter"* ]] || [[ "${script_name,,}" == *"${filter,,}"* ]]; then
|
|
|
+ return 0
|
|
|
+ fi
|
|
|
+ done
|
|
|
+ return 1
|
|
|
+}
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🚦 INITIALIZATION
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# Create runs directory if missing
|
|
|
+if [[ ! -d "$RUNS_DIR" ]]; then
|
|
|
+ log "Creating runs directory at: $RUNS_DIR"
|
|
|
+ mkdir -p "$RUNS_DIR"
|
|
|
+
|
|
|
+ # Create example script
|
|
|
+ cat >"$RUNS_DIR/example.sh" <<'EOF'
|
|
|
+#!/usr/bin/env bash
|
|
|
+# NAME: Example script
|
|
|
+echo "Hello from runs/example.sh!"
|
|
|
+echo "Edit this script or add your own to $RUNS_DIR"
|
|
|
+EOF
|
|
|
+ chmod +x "$RUNS_DIR/example.sh"
|
|
|
+
|
|
|
+ log "✅ Created runs/ directory with example script"
|
|
|
+ exit 0
|
|
|
+fi
|
|
|
+
|
|
|
+# Create logs directory
|
|
|
+mkdir -p "$LOGS_DIR"
|
|
|
+
|
|
|
+# Load environment
|
|
|
+[[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 📝 PARSE ARGUMENTS
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+DRY_RUN=0
|
|
|
+VERBOSE=0
|
|
|
+FILTERS=()
|
|
|
+COMMAND=""
|
|
|
+
|
|
|
+while [[ $# -gt 0 ]]; do
|
|
|
+ case "$1" in
|
|
|
+ --dry) DRY_RUN=1 ;;
|
|
|
+ --verbose) VERBOSE=1 ;;
|
|
|
+ --help | help)
|
|
|
+ show_help
|
|
|
+ exit 0
|
|
|
+ ;;
|
|
|
+ push) COMMAND="push" ;;
|
|
|
+ deps | check) COMMAND="deps" ;;
|
|
|
+ -*)
|
|
|
+ error "Unknown option: $1"
|
|
|
+ show_help
|
|
|
+ exit 1
|
|
|
+ ;;
|
|
|
+ *) FILTERS+=("$1") ;;
|
|
|
+ esac
|
|
|
+ shift
|
|
|
+done
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🚦 HANDLE COMMANDS
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# Handle special commands first
|
|
|
+case "$COMMAND" in
|
|
|
+"push")
|
|
|
+ handle_push
|
|
|
+ exit 0
|
|
|
+ ;;
|
|
|
+"deps")
|
|
|
+ check_dependencies
|
|
|
+ exit 0
|
|
|
+ ;;
|
|
|
+"")
|
|
|
+ # No command, continue with script execution
|
|
|
+ ;;
|
|
|
+*)
|
|
|
+ error "Unknown command: $COMMAND"
|
|
|
+ show_help
|
|
|
+ exit 1
|
|
|
+ ;;
|
|
|
+esac
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🔍 FIND SCRIPTS TO RUN
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+mapfile -t ALL_SCRIPTS < <(find "$RUNS_DIR" -type f -executable | sort)
|
|
|
+
|
|
|
+if [[ ${#ALL_SCRIPTS[@]} -eq 0 ]]; then
|
|
|
+ warn "No executable scripts found in $RUNS_DIR"
|
|
|
+ exit 0
|
|
|
+fi
|
|
|
+
|
|
|
+SCRIPTS_TO_RUN=()
|
|
|
+for script in "${ALL_SCRIPTS[@]}"; do
|
|
|
+ script_name="$(basename "$script")"
|
|
|
+ if matches_filters "$script_name" "${FILTERS[@]}"; then
|
|
|
+ SCRIPTS_TO_RUN+=("$script")
|
|
|
+ fi
|
|
|
+done
|
|
|
+
|
|
|
+if [[ ${#SCRIPTS_TO_RUN[@]} -eq 0 ]]; then
|
|
|
+ warn "No scripts match the given filters: ${FILTERS[*]}"
|
|
|
+ exit 0
|
|
|
+fi
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# ⚠️ CONFIRMATION FOR RUNNING ALL SCRIPTS
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+if [[ ${#FILTERS[@]} -eq 0 && $DRY_RUN -eq 0 ]]; then
|
|
|
+ echo -e "${RED}⚠️ About to run ALL scripts:${NC}"
|
|
|
+ for script in "${SCRIPTS_TO_RUN[@]}"; do
|
|
|
+ name="$(basename "$script")"
|
|
|
+ desc="$(get_description "$script")"
|
|
|
+ echo " • $name - $desc"
|
|
|
+ done
|
|
|
+ echo
|
|
|
+ read -p "Continue? (y/N): " -r answer
|
|
|
+ if [[ ! "$answer" =~ ^[Yy]$ ]]; then
|
|
|
+ echo "Cancelled"
|
|
|
+ exit 0
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 🚀 EXECUTE SCRIPTS
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+EXECUTED=()
|
|
|
+FAILED=()
|
|
|
+
|
|
|
+for script in "${SCRIPTS_TO_RUN[@]}"; do
|
|
|
+ name="$(basename "$script")"
|
|
|
+ desc="$(get_description "$script")"
|
|
|
+ log_file="$LOGS_DIR/${name%.*}.log"
|
|
|
+
|
|
|
+ if [[ $DRY_RUN -eq 1 ]]; then
|
|
|
+ echo -e "${BLUE}[DRY]${NC} Would run: $name - $desc"
|
|
|
+ continue
|
|
|
+ fi
|
|
|
+
|
|
|
+ echo -e "\n${BLUE}▶${NC} Running: $name"
|
|
|
+ [[ "$desc" != "No description" ]] && echo " $desc"
|
|
|
+
|
|
|
+ if [[ $VERBOSE -eq 1 ]]; then
|
|
|
+ if "$script" 2>&1 | tee "$log_file"; then
|
|
|
+ log "✅ $name completed"
|
|
|
+ EXECUTED+=("$name")
|
|
|
+ else
|
|
|
+ error "❌ $name failed (see $log_file)"
|
|
|
+ FAILED+=("$name")
|
|
|
+ fi
|
|
|
+ else
|
|
|
+ if "$script" &>"$log_file"; then
|
|
|
+ log "✅ $name completed"
|
|
|
+ EXECUTED+=("$name")
|
|
|
+ else
|
|
|
+ error "❌ $name failed (see $log_file)"
|
|
|
+ FAILED+=("$name")
|
|
|
+ fi
|
|
|
+ fi
|
|
|
+done
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+# 📊 SUMMARY
|
|
|
+# ═══════════════════════════════════════════════════════════
|
|
|
+if [[ $DRY_RUN -eq 0 ]]; then
|
|
|
+ echo -e "\n${BLUE}═══ SUMMARY ═══${NC}"
|
|
|
+ echo "✅ Completed: ${#EXECUTED[@]}"
|
|
|
+ [[ ${#FAILED[@]} -gt 0 ]] && echo "❌ Failed: ${#FAILED[@]}"
|
|
|
+
|
|
|
+ if [[ ${#FAILED[@]} -gt 0 ]]; then
|
|
|
+ echo -e "\n${RED}Failed scripts:${NC}"
|
|
|
+ for failed in "${FAILED[@]}"; do
|
|
|
+ echo " • $failed"
|
|
|
+ done
|
|
|
+ exit 1
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+exit 0
|