Skip to content

Fix gem supersede on Bundler < 2.2 locks (#1688) - #1690

Merged
Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-gem-merged-section-supersede
Oct 23, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 5 commits into
mainfrom
agent/fix-gem-merged-section-supersede

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1688

Summary

On a project whose Gemfile.lock was written by Bundler 1.17–2.1, a hosted scan that found a superseding patch (or a rotated grant) for an already-redirected gem skipped it as redirect_unattributable and exited 0. Every later scan did the same, so the project stayed on the older patch. The scan now applies the new patch. It moves the Gemfile source … do block and the lock's matching Socket remote together, and the pair stays frozen-installable.

Root cause

Bundler < 2.2 writes every rubygems source into one merged GEM section. Lockfile discovery (vex::discover::gem) ties a spec in such a section to a Socket patch only when the lock's Socket remote and the Gemfile's source … do block name the same URL. The hosted gem rewriter (rewrite_gem) updated the Gemfile block in place to the new URL but never touched a pre-CHECKSUMS lock. The pair then named two different patches. The pre-write attribution gate in hosted::engine vetoed the new pin and wrote nothing.

Fix

  • formats::gem::hosted::refresh_merged_gem_lock_remote changes the merged section's remote that equals this gem's own existing Gemfile block URL to the new index URL, in place. It keeps the line ending and records a redirect_gemfile_lock_source_url edit. It acts only on the shape a converged earlier redirect leaves: spec listed once, in a merged section, that URL listed once, and a ! source pin in DEPENDENCIES. For any other shape it changes nothing, so a first redirect into a merged lock behaves as before.
  • rewrite_gem calls it when the CHECKSUMS-era convergence doesn't apply. The redirect_gem_frozen_install caveat is dropped only when the lock's BUNDLED WITH is < 2.2, which is the era the e2e proves frozen-installable. A merged lock that a later bundler kept still gets the caveat.
  • The remote-line rewrite is now one helper (refresh_remote_line), shared with the CHECKSUMS-era refresh.
  • e2e_redirect_gem_build::gem_hosted_unwind_after_unconverged_supersede_undoes_the_new_block: on bundler < 2.2 it now asserts that the superseding patch lands (Gemfile and lock both on gen 3, frozen install accepts the pair), then still runs the rollback / remove unwind. The 2.2+ arm is unchanged.

Wrappers (npm/, pypi/, gem/) only dispatch to the binary, so they need no change.

Test evidence

Issue Test Red without fix Green with fix
#1688 hosted::engine::tests::merged_section_supersede_moves_the_lock_remote_with_the_gemfile (superseding patch + rotated grant, LF + CRLF) ✅ redirect_unattributable skip ✅
#1688 e2e_redirect_gem_build::gem_hosted_unwind_after_unconverged_supersede_undoes_the_new_block on real bundler 2.1.4 ✅ panics at the gen 3 Gemfile assert (scan skipped) ✅
shape guards formats::gem::hosted::tests::{merged_section_socket_remote_follows_the_new_index_url, merged_section_refresh_refuses_other_shapes, bundled_before_2_2_reads_the_bundled_with_version} n/a (new fn) ✅

Commands run locally:

  • cargo fmt --all -- --check: ok
  • cargo clippy --workspace --all-features -- -D warnings: ok
  • cargo test -p socket-patch-core --all-features: 6683 passed, 4 failed. The 4 (copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_maps_error_and_leaves_lock_untouched, pypi_requirements::wire_failure_rolls_back_already_written_files) depend on read-only file modes, which don't block root, and this agent container runs as uid 0. None of their files are in this diff, and CI runs them as a normal user.
  • cargo test -p socket-patch-cli --all-features --lib --test e2e_gem --test in_process_gem_apply --test in_process_gem_multi_platform --test in_process_rollback_hosted --test in_process_scan --test e2e_redirect_gem_stale_install: 930 + 22 + 23 + 27 + 27 + 41 + 42 passed
  • BUNDLER_VERSION=2.1.4 SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 SOCKET_PATCH_BUNDLER_E2E_VERSION=2.1.4 cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build -- --ignored: 32 passed
  • the same on bundler 4.0.18: 32 passed

A full cargo test --workspace --all-features build doesn't fit this agent container's disk allowance, so CI runs the remaining suites. Bundler 1.17.3 isn't runnable on the local Ruby 3.3. The nightly e2e-extended 1.17.3 / 2.1.4 / 2.2.33 legs cover it, and PR CI doesn't run those legs.

Code review

I ran /code-review high. Fixed: the refresh could take another redirected gem's Socket remote (it now requires this gem's own block URL, with a regression case); the caveat is now limited to the proven bundler era; stale comments; the duplicated remote-rewrite code; and an invalid test fixture. Not changed:

Interaction with #1687

#1687 (test-only, fixes #1685) changes the same e2e test's < 2.2 branch to assert the current refusal. Whichever merges second needs a trivial merge, and this PR's branch (asserting the supersede lands) is the one to keep.

Follow-ups

  • The depscan TS twin of the gem rewriter (registry-rewrite gem.ts) should get the same merged-section refresh.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RKaRAnSHnVgv3Mxga3ZeAU


Generated by Claude Code


Note

Medium Risk
Changes gem lock rewrite and attribution paths used on every hosted scan; behavior is gated to merged pre-2.2 lock shapes with new tests, but mistakes could still break frozen installs or mis-attribute remotes.

Overview
Fixes superseding hosted gem patches on locks written by Bundler < 2.2, where one merged GEM section caused scans to skip the new pin as redirect_unattributable because only the Gemfile source … do block was updated.

When CHECKSUMS-era lock convergence does not apply, rewrite_gem now calls refresh_merged_gem_lock_remote to retarget the merged section’s Socket remote: that matches this gem’s prior block URL to the new index URL (with strict shape guards so other gems’ remotes are untouched). Remote-line edits are centralized in refresh_remote_line. The redirect_gem_frozen_install warning is suppressed only when that merged refresh succeeds and bundled_before_2_2 says the lock is from the proven frozen-installable era.

Tests add unit coverage for the refresh helper and BUNDLED WITH parsing, a hosted-engine supersede/rotated-grant scenario, and extend the e2e unwind test: Bundler ≥ 2.2 still expects Gemfile-on-new / lock-on-old; older Bundler expects Gemfile and lock both on the new patch plus a successful frozen bundle install.

Reviewed by Cursor Bugbot for commit f26a4a0. Configure here.


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Bundler 1.17-2.1 lock every rubygems source into one merged GEM
section, so the Socket remote an earlier redirect added is tied to
the gem only through the Gemfile's `source ... do` block. A
superseding patch (or a rotated grant) moved that block to the new
URL but left the lock's remote on the old one. Lockfile discovery
then saw two different patches and the scan skipped the new one as
redirect_unattributable on every run, exiting 0 with the project
stuck on the older patch.

The hosted gem rewriter now moves that merged section's Socket remote
to the new URL in place, alongside the Gemfile block, when the lock
has exactly the shape a converged earlier redirect leaves. Every
other lock shape is left as before.

Fixes #1688

Assisted-by: Claude Code:claude-opus-5-5
The unconverged-supersede capstone assumed every bundler leaves the
lock on the old patch after a superseding scan. On bundler < 2.2 the
merged GEM section now moves with the Gemfile block, so assert that
the new patch lands there (frozen install accepts the pair) and that
rollback and remove still unwind it.

Refs #1688

Assisted-by: Claude Code:claude-opus-5-5
Review follow-up. The merged-section refresh matched any Socket
remote in the section, so a first redirect of one gem could take
over another redirected gem's remote. It now moves only the remote
that equals this gem's own existing Gemfile source block URL.

The frozen-install caveat is now dropped only for locks bundler
< 2.2 wrote, the era the e2e proves frozen-installable; a later
bundler that kept a merged lock still gets the caveat. The remote
line rewrite is shared with the CHECKSUMS-era refresh.

Refs #1688

Assisted-by: Claude Code:claude-opus-5-5
Clippy flagged the negated boolean; name the condition instead.

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

1 similar comment
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f26a4a0. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[final reviewer] A heads-up for whoever resolves conflicts here. #1687 (now Ready for review, auto-merge armed) changes the same else arm of unwind_after_unconverged_supersede in e2e_redirect_gem_build.rs. It makes that arm expect the refusal (redirect_unattributable, both files unchanged), which is the behaviour this PR removes. If #1687 merges first, resolve the conflict by keeping this PR's arm (supersede lands, lock on gen 3, frozen install passes) and dropping #1687's refusal asserts and its gen2_gemfile/gen2_lock snapshot. Otherwise the nightly bundler 1.17.3 / 2.1.4 legs go red again.


Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 23, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

[burn-down] Labeled Ready for review at head f26a4a0f.


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Labeled Ready for review (burn-down agent).


Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Final review brief

What it does. On a Bundler 1.17–2.1 lock (one merged GEM section), a superseding patch or rotated grant moved the Gemfile source … do block but left the lock's Socket remote on the old URL. Discovery then refused the new pin as redirect_unattributable on every scan, and the project stayed on the old patch (#1688). The rewriter now moves that one remote line in place, with a revertible redirect_gemfile_lock_source_url edit, and drops the frozen-install caveat only for locks BUNDLED WITH < 2.2.

Risk: low–medium. The new path triggers only on a narrow, guarded shape: spec listed once, in a multi-remote section, exactly one remote equal to this gem's own prior block URL, and a !-pinned dependency. Every other shape returns false and touches nothing. It is gem-hosted-only and doesn't touch the CHECKSUMS-era path beyond extracting a shared helper.

Look here

  • formats/gem/hosted.rs:310: refresh_merged_gem_lock_remote, the shape guards. It matches only the gem's own prior block URL, so it can't steal another gem's Socket remote.
  • patch/redirect/mod.rs:7751: wiring into rewrite_gem and the frozen_installable caveat gate. prior_block_url is captured at L7461 before the in-place Gemfile refresh.
  • formats/gem/hosted.rs:386: bundled_before_2_2 (CRLF-safe, unknown → false → caveat kept).
  • hosted/engine.rs:5451: end-to-end rewrite test (supersede + rotated grant, LF + CRLF).
  • e2e_redirect_gem_build.rs:2348:`` real-bundler < 2.2 arm now asserts the gen 3 pair lands and frozen-installs.

Verified

  • Read the full diff (4 files, +424/−23).
  • Ran cargo test -p socket-patch-core --lib filtered to the 14 new/affected gem tests: all pass.
  • Reverted only patch/redirect/mod.rs to main: merged_section_supersede_moves_the_lock_remote_with_the_gemfile fails with exactly the Hosted gem: on Bundler < 2.2 locks a superseding patch is never applied (scan skips it as redirect_unattributable, exits 0) #1688 redirect_unattributable skip, so the test really covers the fix.
  • The revert path is covered by the existing edit kind. CRLF is preserved (tested).
  • CHANGELOG.md is untouched.
  • CI: 36/36 run checks green (50 skipped), ci-ok and clippy green.
  • Bugbot found no issues on this head, and there are no review threads.
  • Not run in CI: the nightly e2e-extended bundler 1.17.3/2.1.4 legs. PR CI skips them; the author ran the 2.1.4 e2e locally.

Changes I made: none.

Open questions: #1687 edits the same e2e test's < 2.2 branch to assert the old refusal (to green the nightly). This PR makes that branch assert the supersede lands, so the two conflict. If you approve both, whichever merges second needs a merge that keeps this PR's branch. Once this lands, #1687 is redundant and is probably best closed rather than merged.

Auto-merge is armed, so approving sends it straight to the merge queue.


Generated by Claude Code

Merged via the queue into main with commit 483a518 Oct 23, 2026
86 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-gem-merged-section-supersede branch October 23, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants