Skip to content

Hosted yarn berry rollback/remove put back a descriptor that yarn remove deleted from one workspace member, so every yarn install --immutable fails YN0028 #1435

Closed

Description

[agent] Found by the scheduled Yarn Berry (2+) bug-hunt routine (ledger #305).

Summary

A hosted berry pin covers each descriptor of the patched package with its own resolutions selector (left-pad@npm:1.3.0, left-pad@npm:^1.3.0, …) and keys the lock entry by the tarball URL. If a user then runs yarn remove in one workspace member while another member still depends on the package, yarn keeps the URL-keyed entry (it's still live) and leaves both selectors in the root package.json. That state installs fine.

rollback and remove <pkg> then rebuild the restored entry's key from every selector that routes to the URL (restore_berry), including the one whose descriptor no workspace declares any more. Both exit 0, and every fresh yarn install --immutable fails:

➤ YN0028: │ -"left-pad@npm:1.3.0, left-pad@npm:^1.3.0":
➤ YN0028: │ +"left-pad@npm:^1.3.0":
➤ YN0028: │ The lockfile would have been modified by this install, which is explicitly forbidden.

#1203's fix (#1321) retires a selector only when the URL entry is gone entirely (berry_selector_routes_nothing), so this partial case isn't covered.

Impact

An ordinary yarn remove in a monorepo, followed by the documented undo, leaves a repo where every CI yarn install --immutable fails, after the CLI reported success. The user has to run a mutable yarn install and commit the result.

Repro (Linux, yarn 4.18.1; a local mock patch API serving a real patched tarball)

mkdir -p proj/packages/a proj/packages/b && cd proj
echo '{"name":"proj","private":true,"workspaces":["packages/*"]}' > package.json
echo '{"name":"a","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > packages/a/package.json
echo '{"name":"b","version":"1.0.0","dependencies":{"left-pad":"^1.3.0"}}' > packages/b/package.json
printf 'nodeLinker: node-modules\n' > .yarnrc.yml
yarn install
socket-patch scan --mode hosted --yes ...       # pins both selectors -> one URL-keyed entry
(cd packages/a && yarn remove left-pad)         # b still uses left-pad; fresh --immutable still passes
socket-patch rollback ...                       # exit 0 (same with `remove left-pad`)
# fresh checkout:
yarn install --immutable                        # YN0028 (diff above)

Control: the same steps without socket-patch pass yarn install --immutable.

Expected vs actual

  • Expected: CLI_CONTRACT.md says rollback/remove put the project back on its upstream entries. The restored key should list only the descriptors the workspaces still declare (or the lock's live key), so that the next yarn install --immutable passes. Stale selectors should be retired as Retire berry resolutions left by yarn remove (#1203) #1321 already does for a fully removed package (hosted_resolution_orphaned).
  • Actual: exit 0, restored key "left-pad@npm:1.3.0, left-pad@npm:^1.3.0", YN0028 on every immutable install, plain or hardened.

Matrix (main 1779317, Linux, node-modules linker)

yarn rollback remove left-pad scan --mode hosted --prune then remove
4.0.2 fail (exit 0, YN0028) not run not run
4.18.1 fail (exit 0, YN0028) fail (exit 0, YN0028) fail (redirect_prune_ignored; same YN0028)

Release 4.0.0 is not affected. It pinned through ::__archiveUrl= in the npm-keyed lock entry, which yarn remove rewrites itself. So the bug arrived with the resolutions pin.

Suspect code

crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:805-842: for a URL-keyed entry the key is descriptors = selectors routed to the URL with no check against what the workspaces (or the lock's dependency maps) still declare. retire_stale_selectors (npm.rs:1062) only handles the case where the whole URL entry is gone.

No probe runs (Linux sandbox only).

Activity

  1. mikolalysenko commented on Oct 12, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: confirmed the suspect path on main f8c7371. In restore_berry (crates/socket-patch-core/src/patch/redirect/upstream/npm.rs), a URL-keyed entry gets its restored key from every resolutions selector routed to the tarball URL. Nothing checks those selectors against the descriptors the workspaces still declare. retire_stale_selectors only covers a URL entry that is gone entirely (#1321). This is not a duplicate of #1203, which was the full-removal case. Labeled priority:p2 to match the other yarn-berry issues.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 12, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: hosted berry restore rebuilds the lock key from every routed selector, including ones no workspace declares any more). Branch: agent/fix-berry-restore-stale-selectors. Claim-ID: 2026-10-12T16:20:58Z-1750ff


    Generated by Claude Code

  3. mikolalysenko commented on Oct 12, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #1436


    Generated by Claude Code

  4. added 2 commits that reference this issue on Oct 12, 2026
    4893864
    3977e12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions