Skip to content

Commit 03ef4a4

Browse files
fix(makefile): rails make check honours .bundle/config + bundle exec + db:test:prepare (#30) (#32)
Three coordinated fixes so that running `make check` against a Rails 7+ project actually mirrors what its CI does. Each gap is small on its own; together they're the difference between "consumer copies the Makefile and gets a CI-equivalent local check" and "consumer hits three errors in sequence and gives up." Closes #30. Gap A — DOCKER_RUN now passes -e BUNDLE_APP_CONFIG=/workspace/.bundle when HAS_RUBY is detected. The container exports BUNDLE_APP_CONFIG=/usr/local/bundle by default (so its bundled tools resolve), which silently overrode the project's .bundle/config (e.g. `BUNDLE_PATH: vendor/bundle`). Now the project's config wins for Ruby projects. Required moving the LANGUAGES / HAS_<LANG> filter computation above DOCKER_RUN so the new env flag could key off HAS_RUBY at make-time. Gap B — _test Ruby branch detects Rails (config/application.rb + Gemfile) and runs `bundle exec rails db:test:prepare` before `rspec`. On DB-unreachable failures the loader emits a structured error event with a clear hint ("ensure your test database is reachable") rather than letting 200+ specs fail with cryptic errors. Non-Rails Ruby projects continue to call rspec directly. Postgres-as-consumer-responsibility documented in STABILITY.md. Gap C — _lint/_format/_fix/_test/_security wrap rubocop/reek/brakeman/ bundler-audit/rspec with `bundle exec` ONLY when the tool is pinned in Gemfile.lock. Implemented via a Make function: RUBY_EXEC_FOR = $(if $(and $(wildcard Gemfile.lock),$(shell grep ...)),bundle exec ,) Per-tool detection avoids breaking projects with a Gemfile but no project-pinned rubocop (which would otherwise fail "could not find rubocop in any of the sources"). Falls back to the container's bundled tool when the project does not pin it. Tested: - tests/smoke-rails.sh: extended with a 4th check that creates a project .bundle/config with `BUNDLE_PATH: vendor/bundle` and verifies the container's `bundle config get path` resolves to it. All 4 checks pass. - make _check on dev-toolchain itself: pass (no-op for non-Ruby self-check). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1ba9295 commit 03ef4a4

4 files changed

Lines changed: 107 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Rails projects can now run a CI-equivalent `make check` against their actual
13+
Gemfile-pinned tools, test database, and `.bundle/config` (#30):
14+
- **Gap A — `BUNDLE_APP_CONFIG` override.** `DOCKER_RUN` now passes
15+
`-e BUNDLE_APP_CONFIG=/workspace/.bundle` when `HAS_RUBY` is detected, so
16+
the project's `.bundle/config` (e.g. `BUNDLE_PATH: vendor/bundle`) wins
17+
over the container's `/usr/local/bundle` default. Previously, projects
18+
with a sandboxed bundle silently lost their config and bundler couldn't
19+
find their gems.
20+
- **Gap B — `_test` runs `bundle exec rails db:test:prepare` before rspec.**
21+
Detected by `config/application.rb` + `Gemfile` presence. On DB-unreachable
22+
failures the loader emits a structured `error` event with a clear hint
23+
("ensure your test database is reachable, e.g. start postgres before
24+
make test") rather than letting 200+ specs fail with cryptic errors.
25+
Non-Rails Ruby projects continue to call `rspec` directly.
26+
- **Gap C — `bundle exec` wrapping for project-pinned tools.** `_lint`,
27+
`_format`, `_fix`, `_test`, `_security` now use `bundle exec <tool>`
28+
when the tool is present in the project's `Gemfile.lock`. Falls back to
29+
the container's tool when the project does not pin it. Eliminates lint
30+
diffs caused by version drift between the container's rubocop/reek/
31+
brakeman/bundler-audit/rspec and the project's pinned versions.
32+
- `STABILITY.md` documents the new "you provide a reachable Postgres for
33+
Rails projects with DB-touching specs" expectation.
34+
35+
### Added
36+
37+
- `tests/smoke-rails.sh` — fourth assertion: with a project-local
38+
`.bundle/config` declaring `BUNDLE_PATH: vendor/bundle`, the container
39+
honours it (validates Gap A's fix end-to-end).
40+
1041
## [1.9.1] - 2026-04-29
1142

1243
### Fixed

Makefile

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ DEVRAIL_ENV_FLAGS := $(shell yq -r '.env // {} | to_entries | .[] | "-e " + .key
3333
# Non-existent paths are filtered out at runtime.
3434
RUBY_PATHS ?= app lib spec config bin
3535

36-
DOCKER_RUN := docker run --rm \
37-
-v "$$(pwd):/workspace" \
38-
-w /workspace \
39-
-e DEVRAIL_FAIL_FAST=$(DEVRAIL_FAIL_FAST) \
40-
-e DEVRAIL_LOG_FORMAT=$(DEVRAIL_LOG_FORMAT) \
41-
$(DEVRAIL_ENV_FLAGS) \
42-
$(DEVRAIL_IMAGE):$(DEVRAIL_TAG)
43-
44-
.DEFAULT_GOAL := help
36+
# Prefer `bundle exec <tool>` ONLY when the project pins that specific tool
37+
# in its Gemfile.lock. Otherwise fall back to the container's bundled tool.
38+
# This gives projects with project-pinned versions consistency with CI without
39+
# breaking projects that just declare `languages: [ruby]` and rely on the
40+
# container's defaults. Issue #30 Gap C.
41+
# Usage in recipes: $(call RUBY_EXEC_FOR,rubocop)rubocop $$ruby_paths
42+
RUBY_EXEC_FOR = $(if $(and $(wildcard Gemfile.lock),$(shell grep -m1 -E "^[[:space:]]+$(1)[[:space:]]" Gemfile.lock 2>/dev/null)),bundle exec ,)
4543

4644
# ---------------------------------------------------------------------------
4745
# .devrail.yml language detection (runs inside container where yq is available)
46+
# Computed before DOCKER_RUN so HAS_<LANG> can influence container env (e.g.
47+
# BUNDLE_APP_CONFIG override for Ruby projects — issue #30).
4848
# ---------------------------------------------------------------------------
4949
LANGUAGES := $(shell yq '.languages[]' $(DEVRAIL_CONFIG) 2>/dev/null)
5050
HAS_PYTHON := $(filter python,$(LANGUAGES))
@@ -58,6 +58,23 @@ HAS_RUST := $(filter rust,$(LANGUAGES))
5858
HAS_SWIFT := $(filter swift,$(LANGUAGES))
5959
HAS_KOTLIN := $(filter kotlin,$(LANGUAGES))
6060

61+
# When HAS_RUBY, override the container's default BUNDLE_APP_CONFIG so the
62+
# project's `.bundle/config` (e.g. `BUNDLE_PATH: vendor/bundle`) wins. Without
63+
# this, the container's own `/usr/local/bundle` config silently overrides the
64+
# project's, and bundler can't find project-installed gems (issue #30 Gap A).
65+
RUBY_DOCKER_ENV := $(if $(HAS_RUBY),-e BUNDLE_APP_CONFIG=/workspace/.bundle,)
66+
67+
DOCKER_RUN := docker run --rm \
68+
-v "$$(pwd):/workspace" \
69+
-w /workspace \
70+
-e DEVRAIL_FAIL_FAST=$(DEVRAIL_FAIL_FAST) \
71+
-e DEVRAIL_LOG_FORMAT=$(DEVRAIL_LOG_FORMAT) \
72+
$(DEVRAIL_ENV_FLAGS) \
73+
$(RUBY_DOCKER_ENV) \
74+
$(DEVRAIL_IMAGE):$(DEVRAIL_TAG)
75+
76+
.DEFAULT_GOAL := help
77+
6178
# ---------------------------------------------------------------------------
6279
# .PHONY declarations
6380
# ---------------------------------------------------------------------------
@@ -240,7 +257,7 @@ _lint: _check-config
240257
for p in $(RUBY_PATHS); do [ -d "$$p" ] && ruby_paths="$$ruby_paths $$p"; done; \
241258
ruby_paths=$${ruby_paths# }; \
242259
if [ -n "$$ruby_paths" ]; then \
243-
rubocop $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:rubocop\","; }; \
260+
$(call RUBY_EXEC_FOR,rubocop)rubocop $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:rubocop\","; }; \
244261
else \
245262
echo '{"level":"info","msg":"skipping ruby rubocop lint: none of RUBY_PATHS exist (override with RUBY_PATHS=...)","language":"ruby"}' >&2; \
246263
fi; \
@@ -251,7 +268,7 @@ _lint: _check-config
251268
exit $$overall_exit; \
252269
fi; \
253270
if [ -n "$$ruby_paths" ]; then \
254-
reek $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:reek\","; }; \
271+
$(call RUBY_EXEC_FOR,reek)reek $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:reek\","; }; \
255272
fi; \
256273
if [ "$(DEVRAIL_FAIL_FAST)" = "1" ] && [ $$overall_exit -ne 0 ]; then \
257274
end_time=$$(date +%s%3N); \
@@ -431,7 +448,7 @@ _format: _check-config
431448
for p in $(RUBY_PATHS); do [ -d "$$p" ] && ruby_paths="$$ruby_paths $$p"; done; \
432449
ruby_paths=$${ruby_paths# }; \
433450
if [ -n "$$ruby_paths" ]; then \
434-
rubocop --check --fail-level error $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
451+
$(call RUBY_EXEC_FOR,rubocop)rubocop --check --fail-level error $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
435452
else \
436453
echo '{"level":"info","msg":"skipping ruby format: none of RUBY_PATHS exist (override with RUBY_PATHS=...)","language":"ruby"}' >&2; \
437454
fi; \
@@ -581,7 +598,7 @@ _fix: _check-config
581598
for p in $(RUBY_PATHS); do [ -d "$$p" ] && ruby_paths="$$ruby_paths $$p"; done; \
582599
ruby_paths=$${ruby_paths# }; \
583600
if [ -n "$$ruby_paths" ]; then \
584-
rubocop -a $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
601+
$(call RUBY_EXEC_FOR,rubocop)rubocop -a $$ruby_paths || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
585602
else \
586603
echo '{"level":"info","msg":"skipping ruby fix: none of RUBY_PATHS exist (override with RUBY_PATHS=...)","language":"ruby"}' >&2; \
587604
fi; \
@@ -746,7 +763,18 @@ _test: _check-config
746763
if [ -n "$(HAS_RUBY)" ]; then \
747764
if [ -d "spec" ]; then \
748765
ran_languages="$${ran_languages}\"ruby\","; \
749-
rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
766+
if [ -f "config/application.rb" ] && [ -f "Gemfile" ]; then \
767+
echo '{"level":"info","msg":"detected Rails app — running db:test:prepare before rspec","language":"ruby"}' >&2; \
768+
if ! bundle exec rails db:test:prepare 2>/tmp/_devrail_rails_db_err; then \
769+
cat /tmp/_devrail_rails_db_err >&2; \
770+
echo '{"level":"error","msg":"db:test:prepare failed — ensure your test database is reachable (e.g. start postgres before make test)","language":"ruby"}' >&2; \
771+
overall_exit=1; failed_languages="$${failed_languages}\"ruby:db-prepare\","; \
772+
else \
773+
$(call RUBY_EXEC_FOR,rspec)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
774+
fi; \
775+
else \
776+
$(call RUBY_EXEC_FOR,rspec)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
777+
fi; \
750778
else \
751779
skipped_languages="$${skipped_languages}\"ruby\","; \
752780
echo '{"level":"info","msg":"skipping ruby tests: no spec/ directory found","language":"ruby"}' >&2; \
@@ -898,7 +926,7 @@ _security: _check-config
898926
if [ -n "$(HAS_RUBY)" ]; then \
899927
ran_languages="$${ran_languages}\"ruby\","; \
900928
if [ -f "config/application.rb" ]; then \
901-
brakeman -q || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:brakeman\","; }; \
929+
$(call RUBY_EXEC_FOR,brakeman)brakeman -q || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:brakeman\","; }; \
902930
else \
903931
echo '{"level":"info","msg":"skipping brakeman: not a Rails application","language":"ruby"}' >&2; \
904932
fi; \
@@ -909,7 +937,7 @@ _security: _check-config
909937
exit $$overall_exit; \
910938
fi; \
911939
if [ -f "Gemfile.lock" ]; then \
912-
bundler-audit check || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:bundler-audit\","; }; \
940+
$(call RUBY_EXEC_FOR,bundler-audit)bundler-audit check || { overall_exit=1; failed_languages="$${failed_languages}\"ruby:bundler-audit\","; }; \
913941
else \
914942
echo '{"level":"info","msg":"skipping bundler-audit: no Gemfile.lock found","language":"ruby"}' >&2; \
915943
fi; \

STABILITY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ DevRail has reached **v1.0** across all repositories. The core standards, toolch
3232
| **Pre-commit hooks** | Stable | Conventional commit hook and per-language hooks configured in template repos. |
3333
| **Documentation site** | Stable | [devrail.dev](https://devrail.dev) is live with full standards coverage. |
3434

35+
## Consumer responsibilities
36+
37+
These are services/data the dev-toolchain container does **not** provide; consumers must provide them when relevant:
38+
39+
- **Database service** (Postgres, MySQL, etc.) — required for Rails projects whose specs touch the test database. The container runs `bundle exec rails db:test:prepare` before `rspec` (when `config/application.rb` + `Gemfile` are present), which needs a reachable database. Typical local pattern: `docker-compose up -d postgres` before `make test`. Typical CI pattern: a `services:` block.
40+
- **Project bundle install** — the container ships its own gems for `rubocop`/`reek`/etc. as defaults, but for Gemfile-pinned versions it expects the project's bundle to already be installed (`bundle install`) so `bundle exec <tool>` can find them.
41+
3542
## Versioning
3643

3744
All DevRail repos follow [Semantic Versioning](https://semver.org/). The container image uses a floating major tag (`:v1`) that always points to the latest `v1.x.x` release. Pin to a specific tag (e.g., `:v1.4.0`) if you need exact reproducibility.

tests/smoke-rails.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,29 @@ fi
154154

155155
echo "==> bundle install + psych load: PASS (completed in ${bundle_elapsed}s)"
156156

157+
# --- 4) Project .bundle/config wins when DOCKER_RUN sets the override ------
158+
# Issue #30 Gap A: container's default BUNDLE_APP_CONFIG=/usr/local/bundle
159+
# silently overrides project-local .bundle/config. The Makefile's DOCKER_RUN
160+
# now passes -e BUNDLE_APP_CONFIG=/workspace/.bundle for Ruby projects so the
161+
# project's own config wins.
162+
echo "==> Verifying .bundle/config override (issue #30 Gap A)"
163+
mkdir -p "$FIXTURE/.bundle"
164+
cat >"$FIXTURE/.bundle/config" <<'BUNDLE_CFG'
165+
---
166+
BUNDLE_PATH: "vendor/bundle"
167+
BUNDLE_CFG
168+
169+
bundle_path_seen=$(docker run --rm \
170+
-v "$FIXTURE:/workspace" \
171+
-w /workspace \
172+
-e BUNDLE_APP_CONFIG=/workspace/.bundle \
173+
"$IMAGE" \
174+
bundle config get path 2>/dev/null | grep -oE '"[^"]+"' | head -1 | tr -d '"' || echo "")
175+
176+
if [ "$bundle_path_seen" != "vendor/bundle" ]; then
177+
echo "FAIL: project .bundle/config ignored — bundle path resolved to '$bundle_path_seen', expected 'vendor/bundle'" >&2
178+
exit 1
179+
fi
180+
echo "==> .bundle/config override: PASS (BUNDLE_PATH = $bundle_path_seen)"
181+
157182
echo "==> All Rails smoke checks passed"

0 commit comments

Comments
 (0)