|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Local CI runner that mirrors .github/workflows/rubyonrails.yml pass conditions |
| 3 | +# Usage: ./scripts/local_ci_runner.sh [--no-services] [--no-parallel] [--timeout-es 60] |
| 4 | + |
| 5 | +set -u |
| 6 | + |
| 7 | +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" |
| 8 | +cd "$ROOT_DIR" || exit 1 |
| 9 | + |
| 10 | +NO_SERVICES=0 |
| 11 | +NO_PARALLEL=0 |
| 12 | +ES_TIMEOUT=60 |
| 13 | + |
| 14 | +while [[ $# -gt 0 ]]; do |
| 15 | + case "$1" in |
| 16 | + --no-services) NO_SERVICES=1; shift ;; |
| 17 | + --no-parallel) NO_PARALLEL=1; shift ;; |
| 18 | + --timeout-es) ES_TIMEOUT="$2"; shift 2 ;; |
| 19 | + -h|--help) |
| 20 | + cat <<EOF |
| 21 | +Usage: $0 [--no-services] [--no-parallel] [--timeout-es SECONDS] |
| 22 | +
|
| 23 | +--no-services Skip starting docker services (postgres/elasticsearch). Useful when already running. |
| 24 | +--no-parallel Run steps serially instead of spinning rubocop/security in parallel. |
| 25 | +--timeout-es N Wait up to N seconds for Elasticsearch to become healthy (default: 60). |
| 26 | +
|
| 27 | +This script attempts to reproduce the GitHub Actions job steps for the "Ruby on Rails CI" workflow |
| 28 | +in this repository and reports pass/fail per-step. |
| 29 | +EOF |
| 30 | + exit 0 |
| 31 | + ;; |
| 32 | + *) echo "Unknown option: $1"; exit 1 ;; |
| 33 | + esac |
| 34 | +done |
| 35 | + |
| 36 | +# Helpers |
| 37 | +log() { printf "[%s] %s\n" "$(date +'%H:%M:%S')" "$*"; } |
| 38 | +run_cmd() { |
| 39 | + local label="$1"; shift |
| 40 | + log "START: $label" |
| 41 | + if "$@"; then |
| 42 | + log "OK: $label" |
| 43 | + return 0 |
| 44 | + else |
| 45 | + local rc=$? |
| 46 | + log "FAIL: $label (exit $rc)" |
| 47 | + return $rc |
| 48 | + fi |
| 49 | +} |
| 50 | + |
| 51 | +# Record step results |
| 52 | +declare -A STEP_STATUS |
| 53 | + |
| 54 | +# Start services (postgres + elasticsearch) using docker compose if requested |
| 55 | +start_services() { |
| 56 | + if [[ $NO_SERVICES -eq 1 ]]; then |
| 57 | + log "Skipping service startup (--no-services)" |
| 58 | + return 0 |
| 59 | + fi |
| 60 | + |
| 61 | + if command -v docker >/dev/null 2>&1; then |
| 62 | + if docker compose version >/dev/null 2>&1; then |
| 63 | + log "Starting services via 'docker compose'" |
| 64 | + # Start services detached; avoid attaching or waiting for input |
| 65 | + DOCKER_TIMEOUT=30 |
| 66 | + docker compose up -d --remove-orphans postgres elasticsearch || return 1 |
| 67 | + elif command -v docker-compose >/dev/null 2>&1; then |
| 68 | + log "Starting services via 'docker-compose'" |
| 69 | + docker-compose up -d --remove-orphans postgres elasticsearch || return 1 |
| 70 | + else |
| 71 | + log "docker compose not available; please start Postgres and Elasticsearch manually" |
| 72 | + return 1 |
| 73 | + fi |
| 74 | + else |
| 75 | + log "docker not found; please start Postgres and Elasticsearch manually" |
| 76 | + return 1 |
| 77 | + fi |
| 78 | +} |
| 79 | + |
| 80 | +wait_for_elasticsearch() { |
| 81 | + local timeout=${ES_TIMEOUT:-60} |
| 82 | + local deadline=$((SECONDS + timeout)) |
| 83 | + log "Waiting up to ${timeout}s for Elasticsearch to be healthy on http://localhost:9200" |
| 84 | + while [[ $SECONDS -lt $deadline ]]; do |
| 85 | + if curl -s "http://localhost:9200/_cluster/health?wait_for_status=yellow&timeout=1s" >/dev/null 2>&1; then |
| 86 | + log "Elasticsearch reported healthy" |
| 87 | + return 0 |
| 88 | + fi |
| 89 | + sleep 1 |
| 90 | + done |
| 91 | + log "Timed out waiting for Elasticsearch" |
| 92 | + return 1 |
| 93 | +} |
| 94 | + |
| 95 | +prepare_db_schema() { |
| 96 | + # Matches workflow: bundle exec rake -f spec/dummy/Rakefile db:schema:load |
| 97 | + if [[ -x ./bin/dc-run ]]; then |
| 98 | + run_cmd "Prepare DB schema (via bin/dc-run)" ./bin/dc-run bundle exec rake -f spec/dummy/Rakefile db:schema:load |
| 99 | + else |
| 100 | + run_cmd "Prepare DB schema (native)" bundle exec rake -f spec/dummy/Rakefile db:schema:load |
| 101 | + fi |
| 102 | +} |
| 103 | + |
| 104 | +run_rspec() { |
| 105 | + if [[ -x ./bin/dc-run ]]; then |
| 106 | + run_cmd "Run RSpec (via bin/dc-run)" ./bin/dc-run bundle exec rspec |
| 107 | + else |
| 108 | + run_cmd "Run RSpec (native)" bundle exec rspec |
| 109 | + fi |
| 110 | +} |
| 111 | + |
| 112 | +run_rubocop() { |
| 113 | + if [[ -x ./bin/dc-run ]]; then |
| 114 | + run_cmd "Rubocop" ./bin/dc-run bundle exec rubocop --parallel |
| 115 | + else |
| 116 | + run_cmd "Rubocop" bundle exec rubocop --parallel |
| 117 | + fi |
| 118 | +} |
| 119 | + |
| 120 | +run_security_checks() { |
| 121 | + # bundler-audit + brakeman (as in workflow) |
| 122 | + if [[ -x ./bin/dc-run ]]; then |
| 123 | + # Ensure non-interactive environment |
| 124 | + export CI=1 |
| 125 | + |
| 126 | + # Don't attempt to install binstubs (can be interactive); run bundler-audit directly |
| 127 | + ./bin/dc-run bundle exec bundler-audit --update >/dev/null 2>&1 || log "bundler-audit update completed (advisories may exist)" |
| 128 | + run_cmd "Run bundler-audit" ./bin/dc-run bundle exec bundler-audit --quiet || true |
| 129 | + # Run brakeman non-interactively: force no pager and limited verbosity |
| 130 | + PAGER=cat TERM=dumb run_cmd "Run brakeman" ./bin/dc-run bundle exec brakeman --no-pager -q -w2 |
| 131 | + else |
| 132 | + export CI=1 |
| 133 | + |
| 134 | + # Don't attempt to install binstubs (can be interactive); run bundler-audit directly |
| 135 | + bundle exec bundler-audit --update >/dev/null 2>&1 || log "bundler-audit update completed (advisories may exist)" |
| 136 | + run_cmd "Run bundler-audit (native)" bundle exec bundler-audit --quiet || true |
| 137 | + PAGER=cat TERM=dumb run_cmd "Run brakeman (native)" bundle exec brakeman --no-pager -q -w2 |
| 138 | + fi |
| 139 | +} |
| 140 | + |
| 141 | +# Main orchestration |
| 142 | +log "Local CI runner starting" |
| 143 | + |
| 144 | +# Step 1: start services (non-blocking) if needed |
| 145 | +if start_services; then |
| 146 | + STEP_STATUS[start_services]=0 |
| 147 | +else |
| 148 | + STEP_STATUS[start_services]=1 |
| 149 | +fi |
| 150 | + |
| 151 | +# Step 2: run rubocop and security in parallel (these don't require DB/ES) |
| 152 | +PIDS=() |
| 153 | +if [[ $NO_PARALLEL -eq 0 ]]; then |
| 154 | + run_rubocop & |
| 155 | + PIDS+=("$!") |
| 156 | + run_security_checks & |
| 157 | + PIDS+=("$!") |
| 158 | +else |
| 159 | + run_rubocop |
| 160 | + STEP_STATUS[rubocop]=$? |
| 161 | + run_security_checks |
| 162 | + STEP_STATUS[security]=$? |
| 163 | +fi |
| 164 | + |
| 165 | +# Step 3: ensure ES healthy before DB/RSPEC steps (skip when --no-services) |
| 166 | +if [[ $NO_SERVICES -eq 1 ]]; then |
| 167 | + log "Skipping Elasticsearch health check because --no-services was passed" |
| 168 | + STEP_STATUS[elasticsearch]=0 |
| 169 | +else |
| 170 | + if wait_for_elasticsearch; then |
| 171 | + STEP_STATUS[elasticsearch]=0 |
| 172 | + else |
| 173 | + STEP_STATUS[elasticsearch]=1 |
| 174 | + fi |
| 175 | +fi |
| 176 | + |
| 177 | +# Step 4: prepare DB schema |
| 178 | +prepare_db_schema |
| 179 | +STEP_STATUS[db_prepare]=$? |
| 180 | + |
| 181 | +# Step 5: run rspec (this is the heavy step) |
| 182 | +run_rspec |
| 183 | +STEP_STATUS[rspec]=$? |
| 184 | + |
| 185 | +# Wait for background jobs if any |
| 186 | +if [[ ${#PIDS[@]} -gt 0 ]]; then |
| 187 | + for pid in "${PIDS[@]}"; do |
| 188 | + if wait "$pid"; then |
| 189 | + log "Background job (pid $pid) finished OK" |
| 190 | + else |
| 191 | + log "Background job (pid $pid) failed" |
| 192 | + fi |
| 193 | + done |
| 194 | + # Capture their exit statuses via jobs' outputs are already logged by run_cmd |
| 195 | +fi |
| 196 | + |
| 197 | +# Summarize |
| 198 | +log "Local CI run summary:" |
| 199 | +for key in start_services elasticsearch db_prepare rspec rubocop security; do |
| 200 | + if [[ -v STEP_STATUS[$key] ]]; then |
| 201 | + status=${STEP_STATUS[$key]} |
| 202 | + if [[ "$status" -eq 0 ]]; then |
| 203 | + printf " %-15s : OK\n" "$key" |
| 204 | + else |
| 205 | + printf " %-15s : FAIL (exit %d)\n" "$key" "$status" |
| 206 | + fi |
| 207 | + else |
| 208 | + printf " %-15s : SKIPPED/UNKNOWN\n" "$key" |
| 209 | + fi |
| 210 | +done |
| 211 | + |
| 212 | +# Exit non-zero if rspec or rubocop or security or db_prepare failed (mimic CI strictness) |
| 213 | +if [[ ${STEP_STATUS[rspec]:-0} -ne 0 || ${STEP_STATUS[rubocop]:-0} -ne 0 || ${STEP_STATUS[security]:-0} -ne 0 || ${STEP_STATUS[db_prepare]:-0} -ne 0 ]]; then |
| 214 | + log "One or more critical steps failed. See logs above." |
| 215 | + exit 2 |
| 216 | +fi |
| 217 | + |
| 218 | +log "All critical steps passed (rspec, rubocop, security, db_prepare)" |
| 219 | +exit 0 |
0 commit comments