|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +RED='\033[0;31m' |
| 5 | +GREEN='\033[0;32m' |
| 6 | +YELLOW='\033[1;33m' |
| 7 | +CYAN='\033[0;36m' |
| 8 | +NC='\033[0m' |
| 9 | + |
| 10 | +FAILED=() |
| 11 | +PASSED=() |
| 12 | +SKIPPED=() |
| 13 | + |
| 14 | +run_step() { |
| 15 | + local name="$1" |
| 16 | + shift |
| 17 | + echo -e "\n${CYAN}── $name ──${NC}" |
| 18 | + echo -e "${YELLOW}$ $*${NC}" |
| 19 | + if "$@"; then |
| 20 | + PASSED+=("$name") |
| 21 | + echo -e "${GREEN}✓ $name${NC}" |
| 22 | + else |
| 23 | + FAILED+=("$name") |
| 24 | + echo -e "${RED}✗ $name${NC}" |
| 25 | + fi |
| 26 | +} |
| 27 | + |
| 28 | +skip_step() { |
| 29 | + local name="$1" |
| 30 | + local reason="$2" |
| 31 | + SKIPPED+=("$name ($reason)") |
| 32 | + echo -e "\n${YELLOW}── $name [SKIPPED: $reason] ──${NC}" |
| 33 | +} |
| 34 | + |
| 35 | +usage() { |
| 36 | + echo "Usage: $0 [rust|lint|python|all|fix]" |
| 37 | + echo "" |
| 38 | + echo "Mirrors the GitHub Actions CI workflows locally." |
| 39 | + echo "" |
| 40 | + echo " rust Rust CI: cargo check, test, fmt --check, clippy" |
| 41 | + echo " lint Lint PR: ruff check, isort --check-only" |
| 42 | + echo " python Python CI: nox test suite (current Python version)" |
| 43 | + echo " all Everything (default)" |
| 44 | + echo " fix Auto-fix: cargo fmt, ruff --fix, isort" |
| 45 | + exit 0 |
| 46 | +} |
| 47 | + |
| 48 | +# ── rust-CI.yml ─────────────────────────────────────────────────────────────── |
| 49 | +run_rust() { |
| 50 | + echo -e "\n${CYAN}═══ Rust CI (.github/workflows/rust-CI.yml) ═══${NC}" |
| 51 | + run_step "cargo check" cargo check |
| 52 | + run_step "cargo test" cargo test |
| 53 | + run_step "cargo fmt" cargo fmt --check |
| 54 | + run_step "cargo clippy" cargo clippy |
| 55 | +} |
| 56 | + |
| 57 | +# ── lint-pr.yml ─────────────────────────────────────────────────────────────── |
| 58 | +run_lint() { |
| 59 | + echo -e "\n${CYAN}═══ Lint PR (.github/workflows/lint-pr.yml) ═══${NC}" |
| 60 | + |
| 61 | + if command -v ruff &>/dev/null; then |
| 62 | + run_step "ruff check" ruff check . |
| 63 | + else |
| 64 | + skip_step "ruff check" "ruff not installed (pip install ruff)" |
| 65 | + fi |
| 66 | + |
| 67 | + if command -v isort &>/dev/null; then |
| 68 | + run_step "isort check" isort --check-only --diff . |
| 69 | + else |
| 70 | + skip_step "isort check" "isort not installed (pip install isort)" |
| 71 | + fi |
| 72 | +} |
| 73 | + |
| 74 | +# ── python-CI.yml ───────────────────────────────────────────────────────────── |
| 75 | +run_python() { |
| 76 | + echo -e "\n${CYAN}═══ Python CI (.github/workflows/python-CI.yml) ═══${NC}" |
| 77 | + local pyver |
| 78 | + pyver=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')") |
| 79 | + |
| 80 | + if command -v nox &>/dev/null; then |
| 81 | + run_step "nox (python $pyver)" nox --non-interactive --error-on-missing-interpreter -p "$pyver" |
| 82 | + else |
| 83 | + skip_step "nox tests" "nox not installed (pip install nox)" |
| 84 | + fi |
| 85 | +} |
| 86 | + |
| 87 | +# ── fix mode ────────────────────────────────────────────────────────────────── |
| 88 | +run_fix() { |
| 89 | + echo -e "\n${CYAN}═══ Auto-fix ═══${NC}" |
| 90 | + run_step "cargo fmt" cargo fmt |
| 91 | + command -v ruff &>/dev/null && run_step "ruff fix" ruff check --fix . || skip_step "ruff fix" "not installed" |
| 92 | + command -v isort &>/dev/null && run_step "isort fix" isort . || skip_step "isort fix" "not installed" |
| 93 | +} |
| 94 | + |
| 95 | +# ── main ────────────────────────────────────────────────────────────────────── |
| 96 | +MODE="${1:-all}" |
| 97 | + |
| 98 | +case "$MODE" in |
| 99 | + rust) run_rust ;; |
| 100 | + lint) run_lint ;; |
| 101 | + python) run_python ;; |
| 102 | + fix) run_fix ;; |
| 103 | + all) run_rust; run_lint; run_python ;; |
| 104 | + -h|--help|help) usage ;; |
| 105 | + *) echo "Unknown mode: $MODE"; usage ;; |
| 106 | +esac |
| 107 | + |
| 108 | +# ── summary ─────────────────────────────────────────────────────────────────── |
| 109 | +echo -e "\n${CYAN}═══ Summary ═══${NC}" |
| 110 | +for s in "${PASSED[@]+"${PASSED[@]}"}"; do echo -e " ${GREEN}✓${NC} $s"; done |
| 111 | +for s in "${SKIPPED[@]+"${SKIPPED[@]}"}"; do echo -e " ${YELLOW}⊘${NC} $s"; done |
| 112 | +for s in "${FAILED[@]+"${FAILED[@]}"}"; do echo -e " ${RED}✗${NC} $s"; done |
| 113 | + |
| 114 | +if [ ${#FAILED[@]} -gt 0 ]; then |
| 115 | + echo -e "\n${RED}CI would fail: ${#FAILED[@]} check(s) failed.${NC}" |
| 116 | + exit 1 |
| 117 | +else |
| 118 | + echo -e "\n${GREEN}All checks passed. Safe to push.${NC}" |
| 119 | + exit 0 |
| 120 | +fi |
0 commit comments