run 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # ═══════════════════════════════════════════════════════════
  4. # 🔧 SETUP
  5. # ═══════════════════════════════════════════════════════════
  6. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  7. ENV_FILE="$SCRIPT_DIR/.env"
  8. RUNS_DIR="$SCRIPT_DIR/runs"
  9. LOGS_DIR="$SCRIPT_DIR/logs"
  10. # Colors
  11. RED='\033[0;31m'
  12. GREEN='\033[0;32m'
  13. BLUE='\033[0;34m'
  14. YELLOW='\033[0;33m'
  15. NC='\033[0m'
  16. # ═══════════════════════════════════════════════════════════
  17. # 🛠️ HELPERS
  18. # ═══════════════════════════════════════════════════════════
  19. log() { echo -e "${GREEN}[RUN]${NC} $*"; }
  20. warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
  21. error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
  22. # ═══════════════════════════════════════════════════════════
  23. # 🔍 DEPENDENCY VALIDATION
  24. # ═══════════════════════════════════════════════════════════
  25. check_dependencies() {
  26. local missing=()
  27. # Required tools
  28. local required_tools=("git" "find" "grep")
  29. # Optional but recommended tools
  30. local optional_tools=("gitleaks")
  31. log "Checking dependencies..."
  32. # Check required tools
  33. for tool in "${required_tools[@]}"; do
  34. if ! command -v "$tool" &>/dev/null; then
  35. missing+=("$tool")
  36. fi
  37. done
  38. # Check optional tools
  39. for tool in "${optional_tools[@]}"; do
  40. if ! command -v "$tool" &>/dev/null; then
  41. warn "Optional tool missing: $tool (recommended for security scanning)"
  42. else
  43. log "✓ Found: $tool"
  44. fi
  45. done
  46. # Report missing required tools
  47. if [[ ${#missing[@]} -gt 0 ]]; then
  48. error "Missing required tools: ${missing[*]}"
  49. error "Please install missing dependencies"
  50. exit 1
  51. fi
  52. log "✓ All required dependencies found"
  53. }
  54. # ═══════════════════════════════════════════════════════════
  55. # 🔒 SECURITY SCANNING
  56. # ═══════════════════════════════════════════════════════════
  57. run_security_scan() {
  58. log "Running security scan..."
  59. if command -v gitleaks &>/dev/null; then
  60. log "Using GitLeaks for secret detection..."
  61. if gitleaks detect --verbose --exit-code 1; then
  62. log "✅ No secrets detected"
  63. return 0
  64. else
  65. error "❌ Secrets detected! Review before pushing."
  66. return 1
  67. fi
  68. else
  69. warn "GitLeaks not installed - skipping security scan"
  70. warn "Install with: paru -S gitleaks"
  71. echo
  72. read -p "Continue without security scan? (y/N): " -r answer
  73. if [[ ! "$answer" =~ ^[Yy]$ ]]; then
  74. error "Push cancelled for security"
  75. exit 1
  76. fi
  77. return 0
  78. fi
  79. }
  80. # ═══════════════════════════════════════════════════════════
  81. # 📤 PUSH COMMAND
  82. # ═══════════════════════════════════════════════════════════
  83. handle_push() {
  84. log "Preparing to push repository..."
  85. # Check if we're in a git repository
  86. if ! git rev-parse --git-dir &>/dev/null; then
  87. error "Not in a git repository"
  88. exit 1
  89. fi
  90. # Check for uncommitted changes
  91. if ! git diff-index --quiet HEAD --; then
  92. warn "You have uncommitted changes"
  93. echo
  94. git status --short
  95. echo
  96. # Generate default commit message
  97. local default_msg="dev: automated commit - $(date '+%Y-%m-%d %H:%M:%S')"
  98. read -p "Commit all changes? (Y/n): " -r answer
  99. if [[ ! "$answer" =~ ^[Nn]$ ]]; then
  100. echo
  101. echo "Default: $default_msg"
  102. read -p "Custom commit message (or press Enter for default): " -r commit_msg
  103. # Use default if empty
  104. if [[ -z "$commit_msg" ]]; then
  105. commit_msg="$default_msg"
  106. fi
  107. git add .
  108. git commit -m "$commit_msg"
  109. log "✓ Changes committed: $commit_msg"
  110. fi
  111. fi
  112. # Run security scan
  113. if ! run_security_scan; then
  114. exit 1
  115. fi
  116. # Get current branch
  117. local current_branch
  118. current_branch=$(git branch --show-current)
  119. # Push to origin
  120. log "Pushing branch '$current_branch' to origin..."
  121. if git push origin "$current_branch"; then
  122. log "✅ Successfully pushed to origin/$current_branch"
  123. else
  124. error "❌ Push failed"
  125. exit 1
  126. fi
  127. }
  128. show_help() {
  129. cat <<EOF
  130. Usage: $0 [OPTIONS] [COMMAND|FILTERS...]
  131. COMMANDS:
  132. push Commit, scan for secrets, and push to git origin
  133. deps, check Check for required dependencies
  134. help Show this help message
  135. SCRIPT EXECUTION:
  136. [FILTERS...] Run scripts matching filters from runs/ directory
  137. OPTIONS:
  138. --dry Show what would run without executing
  139. --verbose Show detailed output during execution
  140. EXAMPLES:
  141. $0 deps # Check dependencies
  142. $0 push # Secure git push with secret scanning
  143. $0 docker # Run scripts containing 'docker'
  144. $0 --dry base # Show what base scripts would run
  145. $0 --verbose all # Run all scripts with detailed output
  146. SECURITY:
  147. The push command automatically scans for secrets using GitLeaks
  148. before pushing to prevent accidental credential exposure.
  149. EOF
  150. }
  151. get_description() {
  152. local script="$1"
  153. grep '^# NAME:' "$script" 2>/dev/null | cut -d: -f2- | sed 's/^ *//' || echo "No description"
  154. }
  155. matches_filters() {
  156. local script_name="$1"
  157. shift
  158. local filters=("$@")
  159. [[ ${#filters[@]} -eq 0 ]] && return 0
  160. for filter in "${filters[@]}"; do
  161. if [[ "$script_name" == *"$filter"* ]] || [[ "${script_name,,}" == *"${filter,,}"* ]]; then
  162. return 0
  163. fi
  164. done
  165. return 1
  166. }
  167. # ═══════════════════════════════════════════════════════════
  168. # 🚦 INITIALIZATION
  169. # ═══════════════════════════════════════════════════════════
  170. # Create runs directory if missing
  171. if [[ ! -d "$RUNS_DIR" ]]; then
  172. log "Creating runs directory at: $RUNS_DIR"
  173. mkdir -p "$RUNS_DIR"
  174. # Create example script
  175. cat >"$RUNS_DIR/example.sh" <<'EOF'
  176. #!/usr/bin/env bash
  177. # NAME: Example script
  178. echo "Hello from runs/example.sh!"
  179. echo "Edit this script or add your own to $RUNS_DIR"
  180. EOF
  181. chmod +x "$RUNS_DIR/example.sh"
  182. log "✅ Created runs/ directory with example script"
  183. exit 0
  184. fi
  185. # Create logs directory
  186. mkdir -p "$LOGS_DIR"
  187. # Load environment
  188. [[ -f "$ENV_FILE" ]] && source "$ENV_FILE"
  189. # ═══════════════════════════════════════════════════════════
  190. # 📝 PARSE ARGUMENTS
  191. # ═══════════════════════════════════════════════════════════
  192. DRY_RUN=0
  193. VERBOSE=0
  194. FILTERS=()
  195. COMMAND=""
  196. while [[ $# -gt 0 ]]; do
  197. case "$1" in
  198. --dry) DRY_RUN=1 ;;
  199. --verbose) VERBOSE=1 ;;
  200. --help | help)
  201. show_help
  202. exit 0
  203. ;;
  204. push) COMMAND="push" ;;
  205. deps | check) COMMAND="deps" ;;
  206. -*)
  207. error "Unknown option: $1"
  208. show_help
  209. exit 1
  210. ;;
  211. *) FILTERS+=("$1") ;;
  212. esac
  213. shift
  214. done
  215. # ═══════════════════════════════════════════════════════════
  216. # 🚦 HANDLE COMMANDS
  217. # ═══════════════════════════════════════════════════════════
  218. # Handle special commands first
  219. case "$COMMAND" in
  220. "push")
  221. handle_push
  222. exit 0
  223. ;;
  224. "deps")
  225. check_dependencies
  226. exit 0
  227. ;;
  228. "")
  229. # No command, continue with script execution
  230. ;;
  231. *)
  232. error "Unknown command: $COMMAND"
  233. show_help
  234. exit 1
  235. ;;
  236. esac
  237. # ═══════════════════════════════════════════════════════════
  238. # 🔍 FIND SCRIPTS TO RUN
  239. # ═══════════════════════════════════════════════════════════
  240. mapfile -t ALL_SCRIPTS < <(find "$RUNS_DIR" -type f -executable | sort)
  241. if [[ ${#ALL_SCRIPTS[@]} -eq 0 ]]; then
  242. warn "No executable scripts found in $RUNS_DIR"
  243. exit 0
  244. fi
  245. SCRIPTS_TO_RUN=()
  246. for script in "${ALL_SCRIPTS[@]}"; do
  247. script_name="$(basename "$script")"
  248. if matches_filters "$script_name" "${FILTERS[@]}"; then
  249. SCRIPTS_TO_RUN+=("$script")
  250. fi
  251. done
  252. if [[ ${#SCRIPTS_TO_RUN[@]} -eq 0 ]]; then
  253. warn "No scripts match the given filters: ${FILTERS[*]}"
  254. exit 0
  255. fi
  256. # ═══════════════════════════════════════════════════════════
  257. # ⚠️ CONFIRMATION FOR RUNNING ALL SCRIPTS
  258. # ═══════════════════════════════════════════════════════════
  259. if [[ ${#FILTERS[@]} -eq 0 && $DRY_RUN -eq 0 ]]; then
  260. echo -e "${RED}⚠️ About to run ALL scripts:${NC}"
  261. for script in "${SCRIPTS_TO_RUN[@]}"; do
  262. name="$(basename "$script")"
  263. desc="$(get_description "$script")"
  264. echo " • $name - $desc"
  265. done
  266. echo
  267. read -p "Continue? (y/N): " -r answer
  268. if [[ ! "$answer" =~ ^[Yy]$ ]]; then
  269. echo "Cancelled"
  270. exit 0
  271. fi
  272. fi
  273. # ═══════════════════════════════════════════════════════════
  274. # 🚀 EXECUTE SCRIPTS
  275. # ═══════════════════════════════════════════════════════════
  276. EXECUTED=()
  277. FAILED=()
  278. for script in "${SCRIPTS_TO_RUN[@]}"; do
  279. name="$(basename "$script")"
  280. desc="$(get_description "$script")"
  281. log_file="$LOGS_DIR/${name%.*}.log"
  282. if [[ $DRY_RUN -eq 1 ]]; then
  283. echo -e "${BLUE}[DRY]${NC} Would run: $name - $desc"
  284. continue
  285. fi
  286. echo -e "\n${BLUE}▶${NC} Running: $name"
  287. [[ "$desc" != "No description" ]] && echo " $desc"
  288. if [[ $VERBOSE -eq 1 ]]; then
  289. if "$script" 2>&1 | tee "$log_file"; then
  290. log "✅ $name completed"
  291. EXECUTED+=("$name")
  292. else
  293. error "❌ $name failed (see $log_file)"
  294. FAILED+=("$name")
  295. fi
  296. else
  297. if "$script" &>"$log_file"; then
  298. log "✅ $name completed"
  299. EXECUTED+=("$name")
  300. else
  301. error "❌ $name failed (see $log_file)"
  302. FAILED+=("$name")
  303. fi
  304. fi
  305. done
  306. # ═══════════════════════════════════════════════════════════
  307. # 📊 SUMMARY
  308. # ═══════════════════════════════════════════════════════════
  309. if [[ $DRY_RUN -eq 0 ]]; then
  310. echo -e "\n${BLUE}═══ SUMMARY ═══${NC}"
  311. echo "✅ Completed: ${#EXECUTED[@]}"
  312. [[ ${#FAILED[@]} -gt 0 ]] && echo "❌ Failed: ${#FAILED[@]}"
  313. if [[ ${#FAILED[@]} -gt 0 ]]; then
  314. echo -e "\n${RED}Failed scripts:${NC}"
  315. for failed in "${FAILED[@]}"; do
  316. echo " • $failed"
  317. done
  318. exit 1
  319. fi
  320. fi
  321. exit 0