Repository navigation
Fix interrupted vendor eject unpatching npm (#1782) - #1783
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A plain `vendor` over hosted pins restored each pin's registry entry straight to disk (lock rewritten, hosted .npmrc removed) before the vendored apply opened its journaled group commit. A Ctrl-C, CI cancel or kill in that window left the lock pointing at the unpatched registry tarball with no journal to replay, and a re-run said "No manifest found, nothing to vendor" (exit 0). The eject now opens the group commit before the restore and hands it to the vendored apply, so the restore lands in the same journaled commit as the vendored wiring: killed before the journal, the project stays hosted and a re-run completes the eject; killed after it, the next locked command rolls forward. Fixes #1782 Assisted-by: Claude Code:claude-opus-5-5
Follow-ups from review of the eject crash fix: - Keep the old, unjournaled restore for a Gradle pin: its hosted index and settings script are written straight to disk, so a half-captured restore killed mid-run would leave settings.gradle applying a deleted script. Same when --manifest-path roots the vendored apply somewhere other than --cwd. - When a failed commit keeps its journal for the next command to finish, don't put the hosted files back first: they would match the journal's old bytes and be replayed over anyway. - A re-run of `vendor` replays a pending commit journal before it reads the lockfiles, so it no longer tries to eject a hosted pin the replay is about to replace. - The vendored apply ignores a handed group in a dry run. Refs #1782 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Bugbot is paused — your team has an unpaid invoiceBugbot is paused because your team has an unpaid invoice. Pay it from the billing page in the Cursor dashboard and Bugbot resumes automatically. (requestId: d73f9757-de90-4385-bbb9-9b7565b939cd) |
Bugbot is paused — your team has an unpaid invoiceBugbot is paused because your team has an unpaid invoice. Pay it from the billing page in the Cursor dashboard and Bugbot resumes automatically. (requestId: 06b98b07-17e2-4c13-8f2e-47304fe2a047) |
|
bugbot run Generated by Claude Code |
1 similar comment
|
bugbot run Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
|
Ready for review at
Generated by Claude Code |
Final review briefWhat it does: A hosted-to-vendored Risk: low to medium. It changes the core eject transaction, but the journaled path only runs when the overlay captures every touched file and the project root equals Look here
Verified
Changes I made: none. Open questions (non-blocking, could be follow-ups)
Auto-merge (squash) is armed, so approving sends this PR straight to the merge queue. Generated by Claude Code |
An eject's group commit also holds every pin's upstream restore. When one package failed to vendor, the vendored apply still committed the group, journaling that package on the plain registry entry. Only the eject's later rollback undid it, so a kill before that rollback (or a commit left pending) let the next command roll the package forward, unpatched and untracked. The vendored apply now drops a handed-in eject group unless every pin is vendored in the captured ledger. Nothing reaches the lockfiles, and the downloaded artifacts are removed, so the project stays hosted. Refs #1782 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Final reviewer: I turned auto-merge off. The review brief covers Generated by Claude Code |
|
bugbot run Generated by Claude Code |
|
bugbot run 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 1ff7c0c. Configure here.
|
Ready for review at
Generated by Claude Code |
|
Burn-down agent: Ready for review again at
Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1782
Summary
An interrupted
socket-patch vendoreject (hosted → vendored) could leavean npm project unpatched with nothing to recover from. If Ctrl-C, a CI cancel
or a kill landed in a ~100 ms window, the lock was already back on the
registry tarball and the hosted
.npmrcwas gone, but no commit journalexisted yet. A re-run then printed "No manifest found, nothing to vendor"
and exited 0. With this change, an eject killed before its commit leaves the
project hosted, exactly as it was, and a re-run completes the eject. An eject
killed after the journal is written is finished by the next command.
Root cause
run_eject(crates/socket-patch-cli/src/commands/vendor.rs) ran the wetrestore_upstreamstraight to disk, and only afterwards called the vendoredbackend, which opens its group commit. The only record of the hosted state
in that window was the in-memory
EjectSnapshot. Thescan --mode vendoredtakeover was already crash-safe because it runs its restore inside the
backend's group commit.
Fix
run_ejectopens the group commit before the upstream restore and handsit to the vendored apply through
EjectCapture.group.vendor_records_reusingtakes that group in place of opening its own.The restore and the vendored wiring are captured in memory and land in a
single journaled commit. The eject uses this journaled path only when the
overlay captures every file the restore touches and the apply runs at the
same root (see "Not covered" below).
discarded before the existing
EjectSnapshotrollback runs.rollback. Putting the hosted bytes back would match the journal's recorded
old bytes, and the next command would replay over them anyway.
vendorreplays a pending commit journal (by taking the apply lock)before it reads the lockfiles. Without this, a re-run after a post-journal
kill would try to eject the hosted pin the replay is about to replace.
is vendored in the captured ledger. A failed package's upstream restore
therefore never reaches a commit (review finding: a kill or a pending
commit could otherwise roll it forward, unpatched). Regression: a
group_commit_journalfailpoint run inrolled_back_eject_reports_nothing_vendored, red before and green after.Tests
New tests in
crates/socket-patch-cli/tests/vendor_eject.rs:eject_killed_before_its_commit_stays_hosted_and_reruns(An interruptedvendoreject (Ctrl-C, SIGTERM or SIGKILL) can leave the npm lock back on the registry, unpatched, with no journal, and a re-run then says "No manifest found, nothing to vendor" (exit 0) #1782). It killsthe eject at the
vendor_group_commitfailpoint, then checks that the lockis byte-identical (still hosted), that no journal or ledger exists, and that
a re-run ejects successfully.
Red on main (
nothing reached the lock before the commitassertionfails because the lock was already restored), green with the fix.
eject_killed_after_its_journal_rolls_forward. It kills the eject at thegroup_commit_journalfailpoint; the nextvendorreplays the journal,the lock ends up vendored and the ledger has the entry.
Local runs at
fa35cf3:cargo fmt --all -- --checkandcargo clippy --workspace --all-features -- -D warnings: clean.cargo test --workspace --all-features --no-fail-fast: 15k+ pass. Everyeject, vendor, group-commit, crash-safety and mode-migration suite passes:
vendor_eject,vendor_eject_bun_lockb,vendor_eject_fresh_checkout,vendor_group_commit_e2e,vendor_crash_safety_e2e,mode_migration_{npm,bun,cargo,composer,pypi},in_process_vendor*.origin/mainin this sandbox,so they are environmental:
(uid 0). This includes unchanged
socket-patch-corelib tests.e2e_bun_lockb: one test needs the real patches API, which the sandboxcan't reach; the offline-takeover test is Nightly e2e_bun_lockb red: offline takeover test predates #1648 #1770 (fixed by CI: update bun.lockb offline takeover test (#1770) #1771); and
workspace_text_migration_heals_on_rerun.Checklist:
vendoreject (Ctrl-C, SIGTERM or SIGKILL) can leave the npm lock back on the registry, unpatched, with no journal, and a re-run then says "No manifest found, nothing to vendor" (exit 0) #1782:eject_killed_before_its_commit_stays_hosted_and_reruns(red → green), pluseject_killed_after_its_journal_rolls_forwardNot covered
hosted index and settings script under
.socket/gradle/, and the groupcommit doesn't capture those files. A half-captured restore killed early
would leave
settings.gradleapplying a deleted script, which is worsethan today. The
EjectSnapshotrollback still covers failures that aren'tcrashes. Follow-up: add the hosted Gradle files to the captured set.
--manifest-pathrooting the project somewhere other than--cwdalso keeps the old path. In that case the restore and the vendored apply
write the same files under different path spellings.
/code-review high
I ran it before marking ready. Fixed: the Gradle partial-capture regression,
the pending-journal vs. snapshot-rollback conflict, the root-spelling
mismatch, and the dry-run gate on a handed group. I also added the
post-journal test. Not changed:
switched_off("group_commit")gate: the two call sitesintentionally differ (the eject also requires a capture-safe restore).
ledger backstop can't tell an uncommitted group apart from an earlier
ledger entry.
https://claude.ai/code/session_011f9aeQTtfPaHkRAMwQAsZN
Note
Medium Risk
Changes crash-safety and commit/rollback ordering for vendor eject and journal replay on every vendor run when a journal exists, but behavior is gated and covered by targeted eject/group-commit tests.
Overview
Fixes #1782: a killed hosted→vendored eject could briefly restore the lock to the registry with no journal, so a re-run reported nothing to vendor while the project was effectively unpatched.
Eject now opens a group commit before
restore_upstream(when every restored path is capture-safe and apply runs at project root) and passes it into the vendored apply viaEjectCapture.group, so upstream restore and vendored wiring land in one journaled commit. Failed restore, an apply that never commits, or a partial eject drops that group and keeps the existing snapshot rollback; if a failed commit left a journal, rollback is skipped so the next replay is not fought. The vendored apply refuses to commit an eject group unless every pin is in the captured ledger, so a failed package’s plain-registry restore cannot be journaled.vendor(non-revert) acquires the apply lock and replays a pending commit journal before reading lockfiles, so a post-journal kill finishes on the next run instead of ejecting again from stale hosted pins.Adds regression tests for kill-before-commit (lock unchanged, re-run succeeds) and kill-after-journal (replay completes vendoring), plus a fresh-checkout case ensuring a failed multi-pin eject never leaves a journal.
Reviewed by Cursor Bugbot for commit 1ff7c0c. Configure here.
Generated by Claude Code