From 699e0a39774e82f412d4c919c85b2a61a667792d Mon Sep 17 00:00:00 2001 From: Matthew Mellor Date: Fri, 29 May 2026 12:56:28 -0500 Subject: [PATCH] feat(makefile): docker_network/docker_volumes + rspec-rails detection Bundles two downstream Rails-sync fixes surfaced from tipsyhive. #48 (Added): new optional .devrail.yml keys `docker_network` (single network name -> --network) and `docker_volumes` (list -> repeated -v), injected into DOCKER_RUN. Lets projects reach a sibling service container (e.g. Postgres at `myapp-pg`) and mount extra data during `make test`. Mirrors the existing `env:` -> DEVRAIL_ENV_FLAGS pattern; no-ops when absent. #46 (Fixed): RUBY_EXEC_FOR now keys rspec detection on `rspec-core` (the runner gem) instead of a bare `rspec` lock entry. Rails apps declare only `rspec-rails`, so the old pattern skipped `bundle exec` and ran the container's bundled rspec, activating Ruby 3.4's default cgi 0.4.2 before bundler/setup (Gem::LoadError vs lock-pinned 0.5.1). Preserves the "only bundle exec when the project pins the tool" behaviour for non-Rails projects. Regression coverage added to tests/smoke-rails.sh (sections 5 and 6) via `make --eval` probes against the mounted Makefile. README and CHANGELOG updated. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 24 ++++++++++ Makefile | 22 ++++++++- README.md | 21 +++++++++ tests/smoke-rails.sh | 109 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 173 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a7c457..e3b9eae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `) + and `docker_volumes` (a list of `host:container` mount specs, each + rendered as `-v `). 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 diff --git a/Makefile b/Makefile index 93ebe0f..b62b87b 100644 --- a/Makefile +++ b/Makefile @@ -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: @@ -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 ,) # --------------------------------------------------------------------------- @@ -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 @@ -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\","; \ diff --git a/README.md b/README.md index e287eff..b7cbf48 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/tests/smoke-rails.sh b/tests/smoke-rails.sh index 954b70f..3203dd1 100755 --- a/tests/smoke-rails.sh +++ b/tests/smoke-rails.sh @@ -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: @@ -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"