Skip to content

fix(upgrade): verify tool is brew-managed before routing to brew upgrade#904

Open
decode2 wants to merge 4 commits into
Gentleman-Programming:mainfrom
decode2:fix/brew-upgrade-non-brew-tools
Open

fix(upgrade): verify tool is brew-managed before routing to brew upgrade#904
decode2 wants to merge 4 commits into
Gentleman-Programming:mainfrom
decode2:fix/brew-upgrade-non-brew-tools

Conversation

@decode2

@decode2 decode2 commented Jun 16, 2026

Copy link
Copy Markdown

Summary

Fixes upgrade failures on Linux systems where Linuxbrew is installed for unrelated packages. Previously, effectiveMethod() would route ALL tools to brew upgrade when profile.PackageManager == "brew", even if the tool was installed via go install or binary download.

Root Cause

On Linux, platform detection sets PackageManager = "brew" when Linuxbrew is present on PATH — regardless of how individual tools were actually installed. The upgrade logic then blindly routed every tool to brew upgrade, which failed with "No available formula" for tools not managed by Homebrew.

Fix

  1. Added isBrewManaged(ctx, toolName) helper in strategy.go that probes brew list --versions <toolName> to verify the tool is actually installed via Homebrew.

  2. Updated effectiveMethod() to check isBrewManaged() before routing to InstallBrew. If the tool is not brew-managed, it falls through to the next priority (go-install or declared method).

  3. Added execCommandContext package var for testability (same pattern as existing execCommand).

Tests

Closes #186

Summary by CodeRabbit

  • Bug Fixes
    • Improved upgrade Homebrew routing by confirming a tool is truly Homebrew-managed before choosing the Homebrew install path.
  • Improvements
    • Upgrade install-method selection is now context-aware, using cancellation/deadline-aware command execution to improve reliability during long-running upgrades.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c7908d84-4441-4bf9-a433-676ede222e22

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea32e7 and cad09f6.

📒 Files selected for processing (1)
  • internal/update/upgrade/strategy.go

📝 Walkthrough

Walkthrough

effectiveMethod gains a context.Context parameter and now selects InstallBrew only when the new isBrewManaged(ctx, toolName) helper confirms the tool is actually brew-installed by running brew list --versions. All call sites in executor.go and strategy.go are updated to pass ctx. A new execCommandContext package var enables test injection, and tests are updated with brew-list exit-code mocking.

Changes

Brew-managed detection for upgrade strategy

Layer / File(s) Summary
Test injection and caching infrastructure
internal/update/upgrade/executor.go, internal/update/upgrade/strategy.go, internal/update/upgrade/strategy_test.go
Adds execCommandContext package var for context-aware command test injection. Introduces sync import and package-level brewManagedCache guarded by RW mutex with clearBrewManagedCache helper for test isolation. Adds fmt import and mockExitCommand helper that generates OS-specific commands exiting with a provided status code.
isBrewManaged detection helper
internal/update/upgrade/strategy.go
Implements isBrewManaged(ctx, toolName) which validates the tool name, checks an in-memory cache, and probes Homebrew via brew list --versions --formula and --cask using execCommandContext(ctx, ...), caching and returning the first successful probe result.
effectiveMethod conditional brew routing
internal/update/upgrade/executor.go
Updates effectiveMethod signature to accept context.Context and selects InstallBrew only when isBrewManaged confirms the tool is actually brew-installed; otherwise falls through to Windows installer, go-install, or the tool's declared install method. Routing priority documentation is updated accordingly.
Context threading into all effectiveMethod call sites
internal/update/upgrade/strategy.go, internal/update/upgrade/executor.go
Updates runStrategy comment and all invocations in executor.go (dev-build tools, version-unknown tools, executable-tools loop, and executeOne) to pass ctx to effectiveMethod, enabling context-aware method selection with cancellation and deadline support.
Test updates with context threading and brew-list mocking
internal/update/upgrade/strategy_test.go
Updates Windows-focused TestEffectiveMethod_* cases to pass context.Background() to effectiveMethod. Refactors main TestEffectiveMethod with brewListExit per-case field, mocks execCommandContext to intercept brew list --formula/--cask --versions and return deterministic exit-code results, and updates the auto-detect case to assert brew precedence when isBrewManaged returns true. Expands isBrewManaged tests to validate probe counts and caching with controlled formula/cask exit-code combinations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: verifying tools are brew-managed before routing to brew upgrade, which directly addresses the core issue.
Linked Issues check ✅ Passed The code changes fully implement the proposed fix: adding isBrewManaged() helper to verify actual Homebrew installation, updating effectiveMethod() to call it before routing to InstallBrew, and falling back to alternative methods if not brew-managed.
Out of Scope Changes check ✅ Passed All changes are within scope: executor.go adds context-aware execution and calls effectiveMethod(), strategy.go implements isBrewManaged() and updates routing logic, and strategy_test.go adds comprehensive test coverage for the new behavior.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/update/upgrade/strategy.go`:
- Around line 305-318: The isBrewManaged function uses brew list without
explicit filtering flags, which has unreliable exit code semantics across
Homebrew versions. Since the function's contract states it should detect tools
installed "either as a formula or a cask," modify the function to make two
separate execCommandContext calls to brew list, one with the --formula flag and
one with the --cask flag, and return true if either call succeeds (returns nil).
This explicit probing approach ensures correct detection across different
Homebrew versions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a180db3c-f0b9-47c9-bf78-815fba8e3bfa

📥 Commits

Reviewing files that changed from the base of the PR and between 8b2e2cf and 601dd6f.

📒 Files selected for processing (5)
  • internal/cli/run.go
  • internal/cli/run_component_paths_test.go
  • internal/update/upgrade/executor.go
  • internal/update/upgrade/strategy.go
  • internal/update/upgrade/strategy_test.go

Comment thread internal/update/upgrade/strategy.go
decode2 added 2 commits June 16, 2026 02:39
On Linux systems with Linuxbrew installed for unrelated packages,
profile.PackageManager is set to 'brew' even when tools were installed
via go install or binary download. This caused 'brew upgrade <tool>'
to fail with 'No available formula' errors.

Added isBrewManaged() helper that probes 'brew list --versions <tool>'
to verify the tool is actually managed by Homebrew. effectiveMethod()
now checks this before routing to InstallBrew, falling through to
go-install or the declared method when the tool is not brew-managed.

Closes Gentleman-Programming#186
brew list --versions without explicit type flags has inconsistent exit-code
semantics across Homebrew versions. Probe --formula and --cask separately
to ensure correct detection regardless of Homebrew version.
@decode2 decode2 force-pushed the fix/brew-upgrade-non-brew-tools branch from 601dd6f to 5c412c4 Compare June 16, 2026 05:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/update/upgrade/executor.go`:
- Around line 500-501: The effectiveMethod function is being called multiple
times for the same tool (at the code around lines 500-501, 513-514, 521-525, and
within runStrategy at lines 571-577) instead of once per tool, causing repeated
external shell calls via isBrewManaged and creating TOCTOU risks. Compute
effectiveMethod(ctx, r.Tool, profile) once at the start of the tool processing
flow, store the result in a variable, and pass this precomputed method value
through all execution paths instead of recomputing it. Update all the affected
sites to use the stored method value: replace the effectiveMethod calls at lines
500-501, 513-514, 521-525 with references to the precomputed variable, and
modify runStrategy to accept the precomputed method as a parameter instead of
computing it internally at lines 571-577.

In `@internal/update/upgrade/strategy_test.go`:
- Around line 359-365: The test table in the struct definition is missing a test
case that exercises the mixed formula/cask outcome behavior. Currently all test
cases use the same brewListExit value for both probes, so the code path where
one probe fails (like --formula returning non-zero exit) while the other
succeeds (--cask returning zero) is never verified. Add at least one test case
to the tests slice where brewListExit differs from another field or add multiple
entries with different exit codes to cover the scenario where --formula fails
but --cask succeeds, ensuring the fallback/dual-probe logic is fully exercised
in the test coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4704b492-64b6-4a49-892b-81c614bf7826

📥 Commits

Reviewing files that changed from the base of the PR and between 601dd6f and 5c412c4.

📒 Files selected for processing (3)
  • internal/update/upgrade/executor.go
  • internal/update/upgrade/strategy.go
  • internal/update/upgrade/strategy_test.go

Comment on lines +500 to 501
Method: effectiveMethod(ctx, r.Tool, profile),
Status: UpgradeSkipped,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve the install method once per tool and pass it through execution.

Line 521 and Line 576 now recompute effectiveMethod, and runStrategy computes it again downstream. Because brew detection now shells out (isBrewManaged), this creates repeated external probes per tool and a TOCTOU risk where the reported Method can diverge from the method actually executed if probes fail intermittently.

Suggested fix
diff --git a/internal/update/upgrade/executor.go b/internal/update/upgrade/executor.go
@@
-        method := effectiveMethod(ctx, r.Tool, profile)
+        method := effectiveMethod(ctx, r.Tool, profile)
         msg := fmt.Sprintf("Upgrading %s via %s (%s → %s)", r.Tool.Name, method, r.InstalledVersion, r.LatestVersion)
         sp := NewSpinner(pw, msg)
-        toolResult := executeOne(ctx, r, profile, dryRun)
+        toolResult := executeOne(ctx, r, profile, dryRun, method)
@@
-func executeOne(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, dryRun bool) ToolUpgradeResult {
+func executeOne(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, dryRun bool, method update.InstallMethod) ToolUpgradeResult {
     base := ToolUpgradeResult{
         ToolName:   r.Tool.Name,
         OldVersion: r.InstalledVersion,
         NewVersion: r.LatestVersion,
-        Method:     effectiveMethod(ctx, r.Tool, profile),
+        Method:     method,
     }
@@
-    exitReq, err := runStrategy(ctx, r, profile)
+    exitReq, err := runStrategy(ctx, r, profile, method)
diff --git a/internal/update/upgrade/strategy.go b/internal/update/upgrade/strategy.go
@@
-func runStrategy(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile) (bool, error) {
+func runStrategy(ctx context.Context, r update.UpdateResult, profile system.PlatformProfile, method update.InstallMethod) (bool, error) {
@@
-    method := effectiveMethod(ctx, r.Tool, profile)
-
     switch method {

Also applies to: 513-514, 521-525, 571-577

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/update/upgrade/executor.go` around lines 500 - 501, The
effectiveMethod function is being called multiple times for the same tool (at
the code around lines 500-501, 513-514, 521-525, and within runStrategy at lines
571-577) instead of once per tool, causing repeated external shell calls via
isBrewManaged and creating TOCTOU risks. Compute effectiveMethod(ctx, r.Tool,
profile) once at the start of the tool processing flow, store the result in a
variable, and pass this precomputed method value through all execution paths
instead of recomputing it. Update all the affected sites to use the stored
method value: replace the effectiveMethod calls at lines 500-501, 513-514,
521-525 with references to the precomputed variable, and modify runStrategy to
accept the precomputed method as a parameter instead of computing it internally
at lines 571-577.

Comment thread internal/update/upgrade/strategy_test.go
@Alan-TheGentleman Alan-TheGentleman added the type:bug Bug fix label Jun 17, 2026

@Alan-TheGentleman Alan-TheGentleman 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.

This is the lower-risk direction because Homebrew ownership should come from Homebrew, not from path heuristics. Please cache the resolved method once per tool so we do not run repeated brew list probes during the same upgrade path. Also add one mixed probe test where --formula fails and --cask succeeds. After that, this is the PR I would keep for #186.

effectiveMethod is called multiple times per tool during planning,
execution, and verification. Each call previously spawned 2 brew
processes (one for --formula, one for --cask). Cache the result per
tool name to avoid redundant brew list calls during a single upgrade
session.

Also adds tests for mixed probe scenarios (formula fails + cask
succeeds) and cache behavior verification.

Addresses review feedback from @Alan-TheGentleman

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/update/upgrade/strategy.go (1)

77-83: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Refresh the strategy-routing comment to match current behavior.

Line 77 still says brew profile always routes to brew, but routing is now conditional on isBrewManaged(ctx, toolName). Keeping this comment stale can mislead future changes.

Suggested doc-only fix
-//   - brew profile → brewUpgrade (regardless of tool's declared method)
+//   - brew profile + brew-managed tool → brewUpgrade
+//   - brew profile + non-brew-managed tool → fall through to go-install/declared method
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/update/upgrade/strategy.go` around lines 77 - 83, The upgrade
strategy routing comment describes the brew profile routing as unconditional
with "regardless of tool's declared method", but the actual implementation now
conditionally routes based on the isBrewManaged function check. Update the
comment block describing the upgrade strategy routing to accurately reflect that
brew profile routing is conditional on isBrewManaged(ctx, toolName) rather than
being an unconditional path, ensuring the documentation matches the current
implementation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/update/upgrade/strategy.go`:
- Around line 77-83: The upgrade strategy routing comment describes the brew
profile routing as unconditional with "regardless of tool's declared method",
but the actual implementation now conditionally routes based on the
isBrewManaged function check. Update the comment block describing the upgrade
strategy routing to accurately reflect that brew profile routing is conditional
on isBrewManaged(ctx, toolName) rather than being an unconditional path,
ensuring the documentation matches the current implementation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9d5ddb85-81bc-4c28-ad15-83ba7e09b260

📥 Commits

Reviewing files that changed from the base of the PR and between 5c412c4 and 5ea32e7.

📒 Files selected for processing (2)
  • internal/update/upgrade/strategy.go
  • internal/update/upgrade/strategy_test.go

… brew routing

Update the runStrategy documentation comment to accurately reflect that
brew routing is now conditional on isBrewManaged(ctx, toolName), not
unconditional for brew profiles.

Addresses CodeRabbit review feedback.
@decode2

decode2 commented Jun 19, 2026

Copy link
Copy Markdown
Author

Changes requested have been applied:

✅ Cache �ffectiveMethod result per tool to avoid repeated �rew list probes
✅ Added mixed probe test (formula fails + cask succeeds)

The cache is implemented at the isBrewManaged() level with a package-level map protected by sync.RWMutex. This ensures we don't spawn multiple �rew list processes for the same tool during a single upgrade run.

Ready for re-review.

@decode2

decode2 commented Jun 19, 2026

Copy link
Copy Markdown
Author

@Alan-TheGentleman Changes requested have been applied and are ready for re-review.

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

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(upgrade): brew upgrade fails for non-brew tools on Linux with Linuxbrew

2 participants