Skip to content

fix(gem): bundler >= 2.2 floor — setup refuses to wire 1.x, check red-flags unloadable wiring - #209

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
fix/gem-setup-bundler-floor
Aug 19, 2026
Merged

fix(gem): bundler >= 2.2 floor — setup refuses to wire 1.x, check red-flags unloadable wiring#209
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
fix/gem-setup-bundler-floor

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Defect

socket-patch setup on a gem project locked to bundler 1.x silently wires a plugin "socket-patch", path: ... directive that bundler 1.x cannot load — and worse, cannot even parse as a plugin. Bundler 1.x (Plugin::DSL undef_methods :path; the 1.x plugin installer supports only git/rubygems sources) resolves the directive as an ORDINARY GEM, so every later bundle install dies with exit 7 ("Could not find gem 'socket-patch' ...") before plugin registration — an error that never names socket-patch, and in --deployment mode adds a misleading "Perhaps the lockfile is corrupted?" line. Meanwhile setup --check kept reporting "configured", so the CI gate went green over broken installs.

Root cause

  • crates/socket-patch-core/src/setup/gem/update.rs:233 (add_plugin_directive) wired unconditionally — no bundler version probe existed.
  • crates/socket-patch-cli/src/commands/setup.rs:817 (append_gem_check_entries) judged only directive/file presence, so a wired-but-unloadable project checked as configured.

Campaign repros (setup-plugin-lifecycle, frozen-deployment-apply; root-cause clusters D2/C2 in the campaign REPORT.md) reproduced both on real bundler 1.17.3 under docker run --network none.

Fix

New crates/socket-patch-core/src/setup/gem/version.rs: probe the bundler that will actually run installs —

  1. the lock's BUNDLED WITH (Gemfile.lock, or gems.locked for gems.rb projects) — deterministic, PATH-independent, and the version RubyGems' switching selects;
  2. bundle --version fallback for lock-less projects (both the "Bundler version 2.7.2" and bundler-4 bare "4.0.18" spellings), stdin-nulled and bounded by a 10s timeout with kill_on_drop;
  3. fail OPEN on Unknown — a repo may be prepared on a bundler-less machine for a modern-CI project.

Below the >= 2.2 floor:

  • setup (dry-run included) refuses with an error naming the detected version, source, floor, and upgrade remedy — via the gem branch's existing error channel (exit 1, message in preview Errors / --json files[] / --silent stderr). If the project is ALREADY wired, the refusal also names the setup --remove recovery.
  • setup --check red-flags both states as errors (not needs_configuration, since setup cannot fix them); the wired state's message explains the exit-7 failure and names setup --remove.
  • setup --remove is deliberately ungated: it is the recovery path.

Docs: README.md, docs/ecosystems.md, tests/docker/README.md; new local-build matrix images Dockerfile.gem-b1 (ruby:3.1 + bundler 1.17.3) / Dockerfile.gem-b4, both with a BASE_IMAGE build-arg (these legs do not run in CI — the CI setup-matrix job drives scripts/setup-matrix.sh against the plain gem image).

Tests (red -> green)

Red-first history: commit 3b7c3d3 lands the regression tests failing against the pre-fix behavior (core: test_add_refuses_bundler_1x_locked_project, test_add_dry_run_also_refuses_bundler_1x — "wiring a bundler-1.x project must be refused as an error: [... status: Updated]"; CLI: gem_setup_refuses_bundler_1x_locked_project saw {"status": "success", "updated": 2}, gem_check_red_flags_wired_but_unloadable_bundler_1x saw "configured"). 1d21fd4 turns them green.

  • core unit: 13 version.rs tests (BUNDLED WITH parsing incl. garbage, both bundle --version spellings, floor boundaries 1.17/2.1/2.2/2.7/4.0, gems.rb->gems.locked pairing, lock-beats-machine-bundler, message contents) + 5 update.rs floor tests (refuse, dry-run refuse, 2.x proceeds, remove ungated, wired-refusal names --remove).
  • CLI host (setup_matrix_gem.rs): setup refusal end-to-end (exit 1, Gemfile byte-untouched, no plugin dir) and check red-flag + --remove recovery.
  • CLI docker: bundler_1x_setup_refuses_and_installs_keep_working against real bundler 1.17.3 (--network none) — refusal, then bundle install still exits 0 (was exit 7). Anti-vacuity verified: the same leg FAILS (SETUP-RC=0) against a stale pre-fix image.
  • Both campaign repro scripts re-run to ===PASS=== against the fix-baked image, including deployment-mode install rc=0 post-refusal and manual apply still landing the patch.

Final gate on this branch head: core setup::gem 67/67; setup_matrix_gem 17/17 (docker leg included); setup_invariants 19, setup_contract_gaps 6, cli_setup_silent 10, cli_parse_setup 29 all green; clippy clean on both crates (only pre-existing warnings in untouched test files).

Review

Lane-reviewed and approved with independently verified red->green (both red commits re-failed, head re-passed, docker anti-vacuity re-confirmed, both campaign repro scripts re-run in fresh --network none containers). 4 nits, no must-fix; two nits addressed in d43c61c (probe timeout; --remove hint on wired-1.x refusal), the stale local :latest gem-b1/b4 images retagged to the fixed build on the dev machine, the remaining nit (host-bundler test precondition) documented in-test.

🤖 Generated with Claude Code


Note

Medium Risk
Changes gem setup/check behavior for legacy Bundler 1.x projects (now hard errors instead of silent breakage), but the change is narrowly scoped to Ruby setup wiring with explicit recovery paths and broad test coverage.

Overview
Adds a bundler ≥ 2.2 gate for RubyGems setup wiring. Bundler 1.x cannot load plugin ... path: directives (they are resolved as a normal gem and every bundle install fails with exit 7), so wiring those projects was worse than refusing.

version.rs probes bundler via lock BUNDLED WITH (including gems.locked for gems.rb), then bundle --version with a 10s timeout; unknown version fails open. add_plugin_directive (including dry-run) errors below the floor and leaves the Gemfile untouched; already-wired 1.x projects get a setup --remove hint. setup --check reports error (not “configured”) for unsupported bundler, including wired-but-unloadable states, with recovery guidance. remove stays ungated.

Docs note the floor; Dockerfile.gem-b1 / Dockerfile.gem-b4 support local matrix legs; host and docker tests cover refusal, check, and recovery.

Reviewed by Cursor Bugbot for commit d43c61c. Configure here.

Bundler 1.x cannot load the `plugin ... path:` directive gem setup
writes (Plugin::DSL undef's :path; the 1.x plugin installer knows only
git/rubygems sources), so a wired project fails EVERY later
`bundle install` with exit 7 before plugin registration, with an error
that never names socket-patch — while setup and setup --check keep
reporting "configured" (campaign-confirmed on bundler 1.17.3).

Red now:
- core: add_plugin_directive wires a Gemfile.lock'd BUNDLED WITH 1.17.3
  project (real + dry-run)
- cli: setup exits 0/"success" on the same project; setup --check says
  "configured" on a wired project after a 1.x lock lands

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gs unloadable wiring

New core probe (setup/gem/version.rs): read the project's bundler from
the lock's BUNDLED WITH (Gemfile.lock / gems.locked — deterministic,
works where bundle is not on PATH, and the version RubyGems' switching
actually runs), falling back to `bundle --version` (bundler 4's bare
output parses too); fail OPEN when neither yields a version.

- add_plugin_directive (dry-run included) refuses below the floor with
  an error naming the detected version, the >= 2.2 floor, and the
  upgrade remedy; remove_plugin_directive stays ungated (recovery path)
- setup --check red-flags both unsupported states as errors: wired-but-
  unloadable (names `setup --remove`) and unwired-unwireable

Matches the gem branch's own error-channel precedent (missing Gemfile,
unwritable plugin dir): where setup cannot deliver a WORKING
configuration it errors loudly rather than narrowing silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mages, floor docs

- tests/docker/Dockerfile.gem-b1 (ruby 3.1 + bundler 1.17.3) and
  Dockerfile.gem-b4 (ruby 3.4 + bundler ~> 4.0) from the campaign's
  bundler-version matrix, with a BASE_IMAGE build-arg so a binary under
  test can be baked without touching shared :latest tags. Not built in
  CI (the CI setup-matrix job drives scripts/setup-matrix.sh against
  the plain gem image only) — local build lines documented in the
  headers and tests/docker/README.md.
- setup_matrix_gem.rs: bundler_floor_docker leg (soft-skip when docker/
  image absent; SOCKET_PATCH_GEM_B1_IMAGE override) driving real
  bundler 1.17.3: setup refuses via the `bundle --version` fallback,
  `bundle install` keeps working, the lock path refuses too, and
  --check red-flags. Verified: fails against a pre-fix image
  (SETUP-RC=0), passes against a fix-baked one.
- README.md + docs/ecosystems.md: document the bundler >= 2.2 floor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d 1.x refusal

Two nits from the lane review:
- probe_bundler's `bundle --version` fallback (lock-less projects only) now
  runs under a 10s timeout with kill_on_drop, degrading to Unknown (fail
  open) instead of hanging setup/setup --check on a wedged bundler.
- Re-running `setup` on an ALREADY-wired bundler-1.x project appends the
  `socket-patch setup --remove` recovery path to the refusal message,
  matching what `setup --check` already names for that state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-floor

# Conflicts:
#	crates/socket-patch-cli/tests/setup_matrix_gem.rs
#	crates/socket-patch-core/src/setup/gem/update.rs
@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit d9afb08 into main Aug 19, 2026
42 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/gem-setup-bundler-floor branch August 19, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants