Repository navigation
Fix gem rollback leaving superseding Gemfile block (#1626) - #1664
Conversation
Assisted-by: Claude Code:claude-opus-5-5
On a Gemfile.lock without a CHECKSUMS section, a hosted re-scan that picks up a newer patch for the same gem moves only the Gemfile's source block to the new patch; the lock keeps the old one until the next bundle install. A rollback or remove in that window restored the lock but left the new block, reported success, and the next bundle install put the patched gem back. The restore now also undoes a Socket source block for the same gem name and version under another patch uuid when the lock pins the gem, so the Gemfile and lock come back consistent. Ambiguous shapes (several such blocks, or the uuid named elsewhere) are still refused. Fixes #1626 Assisted-by: Claude Code:claude-opus-5-5
Adds the end-to-end repro from the issue: hosted scan, converge, superseding re-scan that leaves the Gemfile on the new patch and the lock on the old one, then rollback and remove. Both must leave no Socket wiring in either file, and a frozen bundle install must accept the restored pair. Documents the case in the gem unwind contract. Refs #1626 Assisted-by: Claude Code:claude-opus-5-5
When the Gemfile names the newer patch but the lock still pins the older one, restoring only the newer patch's pin undid its block and left the lock on the older patch, so the pair stayed inconsistent. That pin is now refused with a message naming the older patch, while the older patch's pin restores both files. Pins the lock really wires (not ones whose uuid just appears in it) go first, so either order of the two pins gives the same result. Refs #1626 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
On a bundler < 2.2 lock, one merged GEM section lists the specs of every remote together, so another gem's Socket remote there says nothing about which patch serves this gem. The earlier-patch check read it as one and refused a valid restore of this gem. It now looks only at sections with a single remote. Refs #1626 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Ready for review at
Slack announcement not sent: this run has no Slack send tool. Generated by Claude Code |
rustfmt skips this test function (its long string literals defeat it), so the extra indentation the #1626 change introduced on `&ctx,` was never corrected by `cargo fmt --check`. Align it with the other arguments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
bugbot run Final reviewer: pushed Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 da3c353. Configure here.
|
Ready for review at
Generated by Claude Code |
Final review briefWhat it does. A superseding hosted re-scan on a lock with no CHECKSUMS moves only the Gemfile's Risk: low. The change is limited to the gem restore path and only fires in a narrow state: the lock wires A, and the Gemfile has only a rewriter-shaped Socket block for B with the same name and version. Ambiguous cases, such as several candidate blocks, are refused rather than guessed. Blocks a user wrote themselves are never matched. Look here
Verified
Changes I made: none. Open questions: none blocking. Nits:
Auto-merge is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
|
[agent] Dequeued with
No test or build step failed. The same head ( Generated by Claude Code |
|
[final reviewer] Re-added to the merge queue (first re-add). The 17:24 UTC eviction came from a Generated by Claude Code |
|
[agent] Evicted a second time (19:31 UTC) for the same reason as before, which isn't this diff. In merge-group CI run 39838789738 (d10ae08), This head also passed full queue CI twice in between, as the PR was rebuilt behind other entries: runs 39826164711 and 39837255979 both succeeded. A job that dies at runner setup points to Actions runner provisioning, not the code. Nothing to fix here. The PR is still approved and green, and it needs re-queueing again. If it keeps happening, the runner setup cancels are worth raising separately. Generated by Claude Code |
|
[final reviewer] Re-added to the merge queue at 19:49 UTC after the 19:31 eviction ( Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1626
Summary
On a Gemfile.lock with no
CHECKSUMSsection, a hosted re-scan that picks up a superseding patch (same gem and version, new uuid) moves only the Gemfile'ssource "<patch registry>" do … endblock to the new uuid. The lock keeps pinning the old patch until the nextbundle install. Ifrollbackorremoveran in that window, the lock was restored but the new block stayed. The command exited 0, frozen installs then failed, and the next unfrozenbundle installinstalled the patched gem again.After this change, rollback and remove restore both files. If only the newer patch's pin is being restored, it is refused with a clear message instead of leaving the pair half-restored.
Root cause
patch/redirect/upstream/gem.rs:restore_one→find_blockonly matched a Gemfile block whose patch-registry uuid equals the uuid of the pin being restored. The lock's pin (old uuid) never matched the block the superseding scan wrote (new uuid).Fix
superseded_block: when the lock wires the pin but the manifest has no block for its uuid, undo the one rewriter-shaped Socket block for the same gem name and version under another hosted uuid. The shapes that stay refused are several such blocks, or that uuid named outside the block.wiring, not a substring match) are processed first, so pin order doesn't change the outcome.Test evidence
gem::tests::restore_undoes_a_superseding_patch_block(old pin alone; both pins in both orders; new pin alone is refused; two superseding blocks are refused)left: None)gem::tests::earlier_lock_pin_needs_a_section_of_its_owngem_hosted_unwind_after_unconverged_supersede_undoes_the_new_blockine2e_redirect_gem_build: the issue's repro, then a frozenbundle installthat must accept the restored pairrollback: the Gemfile is still wired to a Socket patch)To get the red runs, I disabled the new
superseded_blockbranch locally and ran the same tests.Local runs (Linux, Ruby 3.3.6, Bundler 4.0.18):
cargo fmt --all -- --check: okcargo clippy --workspace --all-features -- -D warnings: okSOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored: 31 + 8 passed, 0 failedcargo test --workspace --all-features --no-fail-fast: 14,671 passed, 10 failed. All 10 are environment-only in this sandbox, which runs as root and has no access to the real patch API: the read-only-dir and unremovable-file simulations (copy_tree,vlt_heal,pypi_poetry,pypi_requirements,in_process_redirectwrite-failure tests,repaircleanup tests), ande2e_bun_lockb, which tries to reachpatches-api.socket.devand gets a proxy tunnel error. None of them touch gem code. CI runs them as non-root.npm/,pypi/andgem/only dispatch to the binary./code-review high: findings not fixed
manifest_not_found. Its wiring has to go together with the lock pin it replaced.bundle install. That is the documentedredirect_gem_no_checksums_sectionbehaviour, and changing it is a separate design decision. This PR makes the unwind correct in that window.https://claude.ai/code/session_01HwsQHc975WcR4onkKf9gvf
Note
Medium Risk
Changes hosted gem rollback/remove behavior in a narrow but user-visible window (unconverged supersede); logic is well-tested but touches lock/manifest pairing and refusal paths.
Overview
Fixes #1626: after a superseding hosted re-scan on a CHECKSUMS-less lock, the Gemfile can point at a new patch UUID while Gemfile.lock still pins the old one.
rollbackandremoveused to restore only the lock and leave the newsource "<patch registry>" doblock, so the pair looked “restored” but frozenbundle installfailed and the next unfrozen install re-patched.Gem upstream restore now treats that mismatch explicitly. When the lock wires the old pin but the manifest block names another hosted UUID for the same gem/version,
superseded_blockremoves that superseding block as part of restoring the lock pin; pins the lock actually wires are processed first, and the superseding UUID is marked handled so it isn’t edited twice. Restoring only the newer pin is refused with guidance when the lock still pins an earlier Socket patch (earlier_lock_pin, ignoring merged Bundler < 2.2 sections). CLI_CONTRACT.md documents the case; unit and Bundler e2e tests cover rollback/remove and frozen install after unwind.Reviewed by Cursor Bugbot for commit da3c353. Configure here.
Generated by Claude Code