Skip to content

Commit 4c397d2

Browse files
committed
Add local CI runner script to mirror GitHub Actions workflow
1 parent 0b97786 commit 4c397d2

File tree

3 files changed

+273
-0
lines changed

3 files changed

+273
-0
lines changed

bin/bundle-audit

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
#
5+
# This file was generated by Bundler.
6+
#
7+
# The application 'bundle-audit' is installed as part of a gem, and
8+
# this file is here to facilitate running it.
9+
#
10+
11+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12+
13+
bundle_binstub = File.expand_path("bundle", __dir__)
14+
15+
if File.file?(bundle_binstub)
16+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17+
load(bundle_binstub)
18+
else
19+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21+
end
22+
end
23+
24+
require "rubygems"
25+
require "bundler/setup"
26+
27+
load Gem.bin_path("bundler-audit", "bundle-audit")

bin/bundler-audit

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
#
5+
# This file was generated by Bundler.
6+
#
7+
# The application 'bundler-audit' is installed as part of a gem, and
8+
# this file is here to facilitate running it.
9+
#
10+
11+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12+
13+
bundle_binstub = File.expand_path("bundle", __dir__)
14+
15+
if File.file?(bundle_binstub)
16+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17+
load(bundle_binstub)
18+
else
19+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21+
end
22+
end
23+
24+
require "rubygems"
25+
require "bundler/setup"
26+
27+
load Gem.bin_path("bundler-audit", "bundler-audit")

scripts/local_ci_runner.sh

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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

Comments
 (0)