Repository navigation
Fix rollback over unledgered vendor wiring (#991) - #1739
Mikola Lysenko (mikolalysenko) merged 7 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Regression tests for #991. After a git merge keeps one side of .socket/vendor/state.json, the lockfile still wires the other side's package into .socket/vendor/ with no ledger entry. An unscoped rollback should refuse before writing anything instead of reporting success, deleting the ledger and leaving that package patched. - in_process_rollback_vendored: the still-wired orphan case now expects a vendor_ledger_missing refusal (dry run, wet run and --preserve-state), with the lock, ledger and manifest untouched. - e2e_vendor_yarn_classic_build: the merge flow from the issue with real yarn classic, ending with the merged-entries remedy unwinding both packages byte-exact. - vendor_orphans: the vendor_orphan_still_wired advisory must not send users to `repair`, which no longer rebuilds ledger entries. These fail on main. Assisted-by: Claude Code:claude-opus-5-5
When a git merge keeps one side of .socket/vendor/state.json, the lockfile can still wire the other side's package into .socket/vendor/ with no ledger entry. A bare rollback reverted the ledgered packages, exited 0 and deleted the ledger, leaving that package patched and every later run asking for a ledger no commit had (#991). An unscoped rollback now refuses up front with vendor_ledger_missing, naming each unowned dir and how to recover (merge both sides' ledger entries, or restore the lockfile), and writes nothing. This matches the existing refusal when the ledger is missing entirely. A scoped rollback does not own that wiring and proceeds as before. The vendor_orphan_still_wired advisory no longer tells users to run `repair`, which stopped rebuilding ledger entries in v5. Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the #991 refusal: - vendor --check and rollback now read lockfile references the ledger does not own through one helper, so a fix to that comparison reaches both commands. - The rollback refusal is recorded as a failed rollback in telemetry, like the other errors raised after the lock is taken. - The remedy reads correctly when several entries are missing, and emit_rollback_error keeps its own doc comment. - A test covers a scoped rollback that keeps its target's still-wired orphan, checking the advisory's remedy text. Assisted-by: Claude Code:claude-opus-5-5
A ledger entry whose ecosystem has no backend in this build (or was hand-edited) still owns its uuid dir. The new rollback refusal matched on ecosystem and uuid together, so it refused such a project as "unledgered wiring" instead of letting that entry's own revert fail with vendor_revert_failed. Rollback now treats any entry carrying the uuid as the owner. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Ported from #1738 so this PR's PDM e2e leg can go green. unearth 0.19 moved to httpx2, and PDM 2.x's unbounded unearth now resolves it, so `pdm lock` crashes with "PDMPyPIClient is not an instance or subtype of type (Client)". This is a no-op once #1738 lands on main. Assisted-by: Claude Code:claude-opus-5-5
|
[agent] CI on 2120d22: the only failure was Generated by Claude Code |
|
BugBot review Generated by Claude Code |
The vendor_orphan_still_wired advisory follows a revert that has already rewritten the lockfile, so telling users to run `git checkout -- <lockfile>` would bring back the wiring that revert just removed. The advisory now names only the ledger restore. The up-front rollback refusal, which writes nothing, still offers returning the lockfiles to their pre-vendor versions. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review 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 dcd5127. Configure here.
|
Burn-down agent: labeled Ready for review at head
Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at head
Generated by Claude Code |
Final review briefWhat it does: An unscoped Risk: low–medium. The change is small and fails closed, but it's a new hard stop on the unscoped path: one unledgered reference now blocks the agent and hosted legs too, the same way the existing "ledger missing" refusal does. Look here:
Verified:
Changes I made: none. Open questions (non-blocking):
Auto-merge is armed: approving sends it straight to the merge queue. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #991
What changes for users
Two branches each vendor a different package. Git merges
yarn.lockcleanly, but.socket/vendor/state.jsonconflicts and the user keeps one side. The lockfile still wires the other side's package into.socket/vendor/npm/<uuid>/, and no ledger entry owns that dir.rollbackreverted the ledgered package and exited 0 with only avendor_orphan_still_wiredadvisory. It then deletedstate.json, so the other package stayed patched and committed. Every laterrollback/scan --mode vendoredfailed with "restore state.json from version control". The advisory told users to runrepair, which no longer rebuilds ledger entries in v5.rollback(wet,--dry-runand--preserve-state) refuses before writing anything. It exits 1 withvendor_ledger_missingand names each unowned dir. The remedy: restore the missing entries, and after a merge that kept one side, merge both sides'entries; or restore the lockfile. This is the same refusal rollback already gives when the ledger is missing entirely. Once the entries are merged, rollback unwinds both packages byte-exact (checked with real yarn 1.22.22).vendor_orphan_still_wiredadvisory (vendor --revert,scan --prune, scopedrollback,remove) now names the ledger restore instead ofrepair. It does not suggest checking out the lockfile, because it follows a revert that already rewrote it (a Bugbot finding).The
vendor --checkhalf of #991 was already fixed on main (re-verified in the issue on 10-14).vendor --checkand rollback now share one helper,unledgered_vendor_references, for "which lockfile references does the ledger not own".Root cause
rollback's ledger guard (rollback.rs, "Ledger-less but still wired?") ran only whenstate.jsonwas absent. With a partial ledger, the unowned wiring reached the #1610 orphan sweep. That sweep treats a wired orphan as an advisory, so the run reverted every ledgered entry, saved an empty ledger (which deletes the file) and exited 0.Changes
commands/rollback.rs: after scope resolution and before the prompt or any write, an unscoped run with project state checks the lockfile references against the loaded ledger. Any in---ecosystemsreference whose uuid no entry carries gets avendor_ledger_missingrefusal, recorded in telemetry like the other post-lock errors. An entry whose ecosystem has no backend still owns its uuid, so its own revert reportsvendor_revert_failed(the existingvendored_unknown_ecosystem_*tests).vendored_backend/repair.rs: a new sharedunledgered_vendor_references.vendor --check(vendor.rs) now uses it.vendor_orphans.rs: a sharedUNLEDGERED_WIRING_REMEDY, and the advisory text no longer points atrepair.CLI_CONTRACT.md: rollback State discovery, the vendored-leg note and thevendor_ledger_missingrow.Tests (red on main, green here)
e2e_vendor_yarn_classic_build::yarn_classic_rollback_refuses_a_lock_reference_the_ledger_lost: merge flow from the issue, dry and wet refusal, nothing written, then the merged-entries remedy unwinds both byte-exactrollback (dry=true) must refuse … left: 0 right: 1in_process_rollback_vendored::unscoped_rollback_refuses_a_still_wired_orphan(replaces…_keeps_a_still_wired_orphan, which asserted the exit-0 behavior): dry, wet and--preserve-stateall refuse; lock, ledger and manifest are byte-identical afterwards; a scoped rollback still reverts its targetrollback ["--dry-run"] refuses … left: 0 right: 1vendor_orphans::tests::orphan_still_wired_names_a_working_remedyandin_process_rollback_vendored::scoped_rollback_keeps_a_still_wired_orphan_of_its_targetsocket-patch repair)Commands run locally (Linux, root container):
cargo fmt --all -- --check: ok.cargo clippy --workspace --all-features -- -D warnings: ok.cargo test --workspace --all-features --no-fail-fast: everything this PR touches passes. That includes therollbacksuite (122),in_process_rollback_vendored(23),in_process_vendor,repair,vendor_jvm_cli,e2e_vendor_composer_build,e2e_sbt_vendorande2e_vendor_yarn_classic_build(41, withSOCKET_PATCH_YARN_E2E_REQUIRED=1). Two kinds of failure are environmental and fail identically withorigin/main'ssrc/. (1) Tests that inject write failures with chmod don't fail as root:repair_cleanup_failure…,repair_exits_zero…lock_file_unremovable, 3in_process_redirectwrite-failure tests and 4socket-patch-coreunit tests (core is untouched). (2) Twoe2e_bun_lockbtests need patches-api.socket.dev, which the sandbox proxy blocks.node --test npm/socket-patch/bin/socket-patch.test.mjs: 24 pass. The wrappers only dispatch to the binary, so no wrapper changes are needed.CI note
Commit 274386a ports #1738's
unearth<0.19pin (test harness only) because unearth 0.19 broke the PDM e2e leg on main. It is a no-op once #1738 merges./code-review highoutcomesFixed: the misplaced
emit_rollback_errordoc comment, the missing telemetry on the refusal, the duplicated reference-vs-ledger comparison (now shared withvendor --check), singular/plural remedy grammar, a redundantdedup, and the lost rollback coverage ofvendor_orphan_still_wired(new scoped test).Not fixed, with reasons (follow-ups, not claimed by this PR):
--ecosystems-narrowed rollback can still revert the last ledger entry and deletestate.jsonwhile another package's wiring is unowned. A scoped run doesn't promise to unwind that package, and the ledger it deletes held nothing for it. The next unscopedrollback, orvendor --check(the CI gate), names the wiring. Refusing a targeted rollback over an unrelated package would block a legitimate request.vendor --revertkeeps the still-wired orphan with an advisory and exits 0.e2e_vendor_composer_buildasserts that on purpose ("sweeping is not a failure"), so changing it is a separate decision for that command.maven2/gradle/coursier) are not uuid-keyed, soscan_vendor_referencescan't see them. A merge that loses ajvmentry is not covered here.vendor --checkhandles JVM through its ownLEDGER_OWNED_PATHSpath.https://claude.ai/code/session_011f9aePVxAgoVYdQqvzxbPm
Generated by Claude Code