Skip to content

fix(cli): refuse upgrade methods that cannot replace the running binary - #52149

Closed
Raed2180416 wants to merge 1 commit into
anomalyco:devfrom
Raed2180416:fix-upgrade-foreign-install
Closed

Raed2180416 wants to merge 1 commit into
anomalyco:devfrom
Raed2180416:fix-upgrade-foreign-install

Conversation

@Raed2180416

@Raed2180416 Raed2180416 commented Sep 29, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #52152

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

method() chose a package manager by probing only that manager's global package list. That list can match even when the running binary is a different copy. The upgrade then ran install --global against the other copy, so the command reported success while the executing binary stayed put.

The invariant being violated: a package manager can only replace the OpenCode copy inside its own global root. So each candidate manager now also resolves its global root, and is only eligible if that root contains the running executable. If none qualify, detection returns unknown and the existing actionable error is shown instead of a silent no-op.

  • Root resolution runs alongside the existing probe, concurrently, so detection costs no extra wall-clock time.
  • Each root command is bounded at 5s; a manager that doesn't implement it yields an empty root, which preserves today's behaviour rather than disabling upgrades.
  • installedPackage became installed ({ package, executable }) to carry the resolved path. The name is still derived by the same manifest check, so detection for correct single-install setups is unchanged.

How did you verify your code works?

replacesExecutable is exported as a pure helper with unit tests in updater.test.ts, covering: binary inside the root (accept), outside it (reject — the reported case), a sibling directory sharing the root string prefix (reject), the root itself (reject), empty/unavailable root (preserves behaviour), and untrimmed/relative roots.

I also ran the helper against the real paths from the reproduction, to confirm the buggy layout is refused and the healthy layout still accepted (guarding against a false negative that would break normal upgrades):

bugged layout  -> npm selected? no  (refused)
healthy layout -> npm selected? yes (normal upgrade)

Then end-to-end on a working install, after repointing the binary into the global prefix: upgrade 2.0.18 moved the running binary 2.0.19 → 2.0.18, and a bare upgrade moved it 2.0.18 → 2.0.19. Both now actually change the running version.

Caveat, stated plainly: my machine ran out of disk partway through, so I could not run the full packages/cli suite or the pre-push check hook, since both need a workspace install. The helper tests were run in isolation and pass, and both changed files parse cleanly, but I am relying on CI for the authoritative verification.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Scope of "tested locally", so it is not overread: I ran the new replacesExecutable unit tests in isolation (6 pass), parse-checked both changed files, exercised the helper against the real paths from the reproduction, and confirmed end-to-end on a working install that upgrade 2.0.18 moved the running binary 2.0.19 -> 2.0.18 and a bare upgrade moved it back to 2.0.19. I did not run the full packages/cli suite, because my machine ran out of disk and the workspace install could not complete. CI is the authoritative check for the full suite.

`method()` selected a package manager purely by probing that manager's
global package list. When a second copy of the CLI is registered
elsewhere - a custom npm prefix, or a manually linked directory - the
probe still matched, so `opencode upgrade` ran `install --global` against
an unrelated copy, printed "Upgrade complete", and left the running
binary untouched.

Confirmed on a real install, where the running binary was 2.0.19 while
`npm install --global @opencode/[email protected]` reported success and changed
nothing about the binary that was actually executing.

Now each candidate manager must also own the running executable: its
global root has to contain the binary, otherwise it is skipped. When no
manager qualifies, detection reports "unknown" and the upgrade is
refused with an actionable message instead of silently doing nothing.
An unreadable root preserves the previous behaviour rather than
disabling upgrades outright.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cli: upgrade reports success but the running binary is not replaced when another copy is registered with the package manager

2 participants