Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Issue #48:** `.devrail.yml` now supports two optional keys for
customizing the toolchain container's `docker run`: `docker_network`
(a single user-defined network name, rendered as `--network <name>`)
and `docker_volumes` (a list of `host:container` mount specs, each
rendered as `-v <spec>`). These let Rails-style projects reach a
sibling service container (e.g. a Postgres at `myapp-pg`) and mount
extra data during `make test`. Both mirror the existing `env:` →
`DEVRAIL_ENV_FLAGS` pattern and are no-ops when absent.

### Fixed

- **Issue #46:** `make test` for Rails projects that depend on
`rspec-rails` (the idiomatic Rails-side gem) now correctly runs
`bundle exec rspec`. The `RUBY_EXEC_FOR` detection keyed on a bare
`rspec` lockfile entry, which `rspec-rails` projects don't have — so
the bareword `rspec` ran the container's bundled gem and activated
Ruby 3.4's default `cgi 0.4.2` before bundler/setup could switch to
the lockfile-pinned `cgi 0.5.1` (`Gem::LoadError`). Detection now keys
on `rspec-core` (the runner gem, present whenever any rspec variant
is), preserving the "only `bundle exec` when the project pins the
tool" behaviour for non-Rails projects.

## [1.11.3] - 2026-05-20

### Security
Expand Down
22 changes: 20 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,17 @@ HAS_PLUGINS_DECLARED := $(if $(filter-out missing error 0,$(DEVRAIL_PLUGIN_PROBE
# them as `-e KEY=VALUE` into DOCKER_RUN. Empty/missing section is a no-op.
DEVRAIL_ENV_FLAGS := $(shell yq -r '.env // {} | to_entries | .[] | "-e " + .key + "=" + .value' $(DEVRAIL_CONFIG) 2>/dev/null)

# Read project-specific Docker networking + volume mounts from .devrail.yml and
# inject them into DOCKER_RUN. `docker_network` (single value) attaches the
# toolchain container to a user-defined network so it can reach a sibling
# service container (e.g. a Postgres at hostname `myapp-pg` during `make test`).
# `docker_volumes` (list) mounts extra host paths or named volumes. Both mirror
# the `env:` -> DEVRAIL_ENV_FLAGS pattern; empty/missing sections are no-ops.
# Issue #48.
DEVRAIL_DOCKER_NETWORK := $(shell yq -r '.docker_network // ""' $(DEVRAIL_CONFIG) 2>/dev/null)
DEVRAIL_NETWORK_FLAG := $(if $(DEVRAIL_DOCKER_NETWORK),--network $(DEVRAIL_DOCKER_NETWORK),)
DEVRAIL_VOLUME_FLAGS := $(shell yq -r '.docker_volumes // [] | .[] | "-v " + .' $(DEVRAIL_CONFIG) 2>/dev/null)

# Ruby lint/format scope. Defaults to the conventional Rails directory set so
# rubocop and reek do not descend into vendor/bundle/ (which can hold tens of
# thousands of files of installed gem source). Override per-project via:
Expand All @@ -78,6 +89,11 @@ RUBY_PATHS ?= app lib spec config bin
# breaking projects that just declare `languages: [ruby]` and rely on the
# container's defaults. Issue #30 Gap C.
# Usage in recipes: $(call RUBY_EXEC_FOR,rubocop)rubocop $$ruby_paths
# NOTE: for rspec, detect on `rspec-core` (the actual runner gem) rather than a
# bare `rspec`. A Rails app declares only `rspec-rails`, so its lockfile has no
# bare `rspec` line — keying on that would skip `bundle exec` and run the
# container's bundled rspec, which activates Ruby 3.4's default gems before
# bundler/setup (the `cgi 0.4.2 vs 0.5.1` LoadError). Issue #46.
RUBY_EXEC_FOR = $(if $(and $(wildcard Gemfile.lock),$(shell grep -m1 -E "^[[:space:]]+$(1)[[:space:]]" Gemfile.lock 2>/dev/null)),bundle exec ,)

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -111,6 +127,8 @@ DOCKER_RUN = docker run --rm \
-e DEVRAIL_LOG_FORMAT=$(DEVRAIL_LOG_FORMAT) \
$(DEVRAIL_ENV_FLAGS) \
$(RUBY_DOCKER_ENV) \
$(DEVRAIL_NETWORK_FLAG) \
$(DEVRAIL_VOLUME_FLAGS) \
$(DEVRAIL_RESOLVED_IMAGE)

.DEFAULT_GOAL := help
Expand Down Expand Up @@ -1001,10 +1019,10 @@ _test: _plugins-load
echo '{"level":"error","msg":"db:test:prepare failed — ensure your test database is reachable (e.g. start postgres before make test)","language":"ruby"}' >&2; \
overall_exit=1; failed_languages="$${failed_languages}\"ruby:db-prepare\","; \
else \
$(call RUBY_EXEC_FOR,rspec)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
$(call RUBY_EXEC_FOR,rspec-core)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
fi; \
else \
$(call RUBY_EXEC_FOR,rspec)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
$(call RUBY_EXEC_FOR,rspec-core)rspec || { overall_exit=1; failed_languages="$${failed_languages}\"ruby\","; }; \
fi; \
else \
skipped_languages="$${skipped_languages}\"ruby\","; \
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,27 @@ languages:
- rust
```

Optional keys customize how the toolchain container runs. All are no-ops when
omitted:

```yaml
# Pass extra environment variables into the container.
env:
RAILS_ENV: test
DATABASE_HOST: myapp-pg

# Attach the container to a user-defined Docker network so it can reach a
# sibling service container (e.g. a Postgres at hostname `myapp-pg` during
# `make test`). Single network name.
docker_network: myapp-test

# Mount additional host paths or named volumes into the container. List of
# `host:container` (or `volume:container`) specs, passed straight to `docker -v`.
docker_volumes:
- ./fixtures:/workspace/fixtures
- shared-cache:/cache
```

## Architecture

- **Base image:** Debian bookworm-slim (multi-arch: amd64 + arm64)
Expand Down
109 changes: 108 additions & 1 deletion tests/smoke-rails.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#!/usr/bin/env bash
# tests/smoke-rails.sh — Rails 7+ smoke test for issues #25 and #28
# tests/smoke-rails.sh — Rails 7+ smoke test for issues #25, #28, #30, #46, #48
#
# Verifies the image is consumable by Rails 7+ projects out-of-the-box:
# 1. Gemfile with `platforms: %i[mri windows]` parses (needs Bundler 2.6+).
# 2. make _lint scopes to RUBY_PATHS — vendor/bundle/ is NOT scanned.
# 3. `bundle install` succeeds against a Gemfile containing `gem 'debug'`
# — exercises the psych->libyaml native compile path (issue #28).
# 4. Project .bundle/config wins over the container default (issue #30).
# 5. RUBY_EXEC_FOR detects rspec-rails projects via rspec-core (issue #46).
# 6. docker_network / docker_volumes render the right docker run flags (#48).
#
# Usage: bash tests/smoke-rails.sh
# Env:
Expand Down Expand Up @@ -179,4 +182,108 @@ if [ "$bundle_path_seen" != "vendor/bundle" ]; then
fi
echo "==> .bundle/config override: PASS (BUNDLE_PATH = $bundle_path_seen)"

# --- 5) RUBY_EXEC_FOR detects rspec-rails projects (issue #46) -------------
# A Rails app declares only `rspec-rails`; its Gemfile.lock has no bare `rspec`
# line. Keying detection on `rspec` skipped `bundle exec` and ran the
# container's bundled rspec, which activates Ruby 3.4's default gems before
# bundler/setup (the cgi 0.4.2 vs 0.5.1 LoadError). The Makefile now keys on
# `rspec-core` (the runner gem, always present). Probe the real macro from the
# mounted Makefile via `make --eval` — no bundle install / DB needed.
echo "==> Verifying RUBY_EXEC_FOR detects rspec-rails projects (issue #46)"
PROBE46="$FIXTURE/probe46"
mkdir -p "$PROBE46"
cat >"$PROBE46/.devrail.yml" <<'YAML'
languages: [ruby]
YAML
cat >"$PROBE46/Gemfile.lock" <<'LOCK'
GEM
remote: https://rubygems.org/
specs:
rspec-core (3.13.0)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rspec-rails (6.1.0)
rspec-core (~> 3.13)
rspec-support (3.13.0)
LOCK
rspec_exec=$(docker run --rm \
-v "$PROBE46:/workspace" \
-v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \
-w /workspace "$IMAGE" \
make --eval='_probe: ; @printf "%s" "$(call RUBY_EXEC_FOR,rspec-core)"' \
-f /workspace/Makefile _probe 2>/dev/null)
if [ "$rspec_exec" != "bundle exec " ]; then
echo "FAIL: rspec-rails project resolved exec prefix to '$rspec_exec', expected 'bundle exec '" >&2
exit 1
fi
# Negative control: a lock with no rspec must NOT force bundle exec — preserves
# the "only bundle exec when the project actually pins the tool" intent.
cat >"$PROBE46/Gemfile.lock" <<'LOCK'
GEM
remote: https://rubygems.org/
specs:
rake (13.2.1)
LOCK
no_rspec_exec=$(docker run --rm \
-v "$PROBE46:/workspace" \
-v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \
-w /workspace "$IMAGE" \
make --eval='_probe: ; @printf "%s" "$(call RUBY_EXEC_FOR,rspec-core)"' \
-f /workspace/Makefile _probe 2>/dev/null)
if [ -n "$no_rspec_exec" ]; then
echo "FAIL: lockfile without rspec forced exec prefix '$no_rspec_exec', expected empty" >&2
exit 1
fi
echo "==> rspec-rails detection: PASS"

# --- 6) docker_network + docker_volumes passthrough (issue #48) -----------
# Projects can attach the toolchain container to a sibling-service network and
# mount extra volumes via .devrail.yml. Verify the Makefile renders the flags
# (and that absent keys are a no-op).
echo "==> Verifying docker_network / docker_volumes passthrough (issue #48)"
PROBE48="$FIXTURE/probe48"
mkdir -p "$PROBE48"
cat >"$PROBE48/.devrail.yml" <<'YAML'
languages: [ruby]
docker_network: devrail-smoke-net
docker_volumes:
- /tmp/fixtures:/workspace/fixtures
- shared-cache:/cache
YAML
net_flag=$(docker run --rm \
-v "$PROBE48:/workspace" \
-v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \
-w /workspace "$IMAGE" \
make --eval='_probe: ; @printf "%s" "$(DEVRAIL_NETWORK_FLAG)"' \
-f /workspace/Makefile _probe 2>/dev/null)
if [ "$net_flag" != "--network devrail-smoke-net" ]; then
echo "FAIL: docker_network rendered '$net_flag', expected '--network devrail-smoke-net'" >&2
exit 1
fi
vol_flags=$(docker run --rm \
-v "$PROBE48:/workspace" \
-v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \
-w /workspace "$IMAGE" \
make --eval='_probe: ; @printf "%s" "$(DEVRAIL_VOLUME_FLAGS)"' \
-f /workspace/Makefile _probe 2>/dev/null)
if [ "$vol_flags" != "-v /tmp/fixtures:/workspace/fixtures -v shared-cache:/cache" ]; then
echo "FAIL: docker_volumes rendered '$vol_flags', expected '-v /tmp/fixtures:/workspace/fixtures -v shared-cache:/cache'" >&2
exit 1
fi
# Absent keys must be a no-op (no stray flags leak into DOCKER_RUN).
cat >"$PROBE48/.devrail.yml" <<'YAML'
languages: [ruby]
YAML
empty_flags=$(docker run --rm \
-v "$PROBE48:/workspace" \
-v "$REPO_ROOT/Makefile:/workspace/Makefile:ro" \
-w /workspace "$IMAGE" \
make --eval='_probe: ; @printf "[%s]" "$(DEVRAIL_NETWORK_FLAG)$(DEVRAIL_VOLUME_FLAGS)"' \
-f /workspace/Makefile _probe 2>/dev/null)
if [ "$empty_flags" != "[]" ]; then
echo "FAIL: absent docker_network/docker_volumes rendered '$empty_flags', expected '[]'" >&2
exit 1
fi
echo "==> docker_network / docker_volumes passthrough: PASS"

echo "==> All Rails smoke checks passed"
Loading