Skip to content

Commit 8a82c78

Browse files
masnwilliamsclaude
andauthored
Refresh brew tap before upgrading in kernel upgrade (#208)
## Summary `kernel upgrade` detects a newer version from GitHub releases, then shells out to `brew upgrade kernel/tap/kernel`. But `brew upgrade` doesn't refresh the tap on its own, and Homebrew throttles its auto-update — so right after a release the local tap is stale, brew still sees the old formula, and the upgrade silently no-ops with "already installed" (exit 0). The user sees `kernel upgrade` announce a new version and then nothing changes. Repro: release a new version, then on a machine whose tap hasn't refreshed run `kernel upgrade` → "New version available: X → Y" followed by "Warning: kernel/tap/kernel X already installed", no upgrade. ## Fix - `kernel upgrade` now runs `brew update` before `brew upgrade kernel/tap/kernel` (brew method only; npm/pnpm/bun already pull `@latest`). - `executeUpgrade`/`upgradeCommandArgs` refactored to run an ordered list of commands so the brew path can do two steps; stderr from all steps is still captured for the existing old-tap error diagnosis. - The passive suggested-upgrade hint now shows `brew update && brew upgrade kernel/tap/kernel` for the same reason. ## Testing `go build ./...`, `go vet`, `gofmt`, and `go test ./pkg/update/...` all pass (suggestion-string test updated). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > CLI-only change to upgrade command sequencing and user-facing hints; no auth, data, or core runtime behavior beyond fixing stale-tap brew upgrades. > > **Overview** > Fixes **Homebrew upgrades silently doing nothing** right after a release when the local tap is stale: `kernel upgrade` now runs **`brew update` before `brew upgrade kernel/tap/kernel`** for the brew install path only. > > Upgrade execution is refactored so each install method returns an **ordered list of shell commands** (`[][]string`). `executeUpgrade` runs them sequentially and still aggregates **stderr** across steps for the existing old-tap error handling. Dry-run / displayed commands use **`brew update && brew upgrade kernel/tap/kernel`**. > > Passive upgrade hints in **`SuggestUpgradeCommand`** (brew and unknown-method fallback) use the same two-step brew string. Tests cover brew command ordering and the updated suggestion text. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1332054. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ebe7339 commit 8a82c78

4 files changed

Lines changed: 94 additions & 27 deletions

File tree

‎cmd/upgrade.go‎

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -98,50 +98,62 @@ func isOldTapError(stderr string) bool {
9898
return strings.Contains(stderr, "brew tap kernel/tap")
9999
}
100100

101-
// upgradeCommandArgs returns the command and arguments for a given installation method.
102-
// Returns nil if the method is unknown.
103-
func upgradeCommandArgs(method update.InstallMethod) []string {
101+
// upgradeCommandArgs returns the ordered commands to run for a given installation
102+
// method. Returns nil if the method is unknown.
103+
func upgradeCommandArgs(method update.InstallMethod) [][]string {
104104
switch method {
105105
case update.InstallMethodBrew:
106-
return []string{"brew", "upgrade", "kernel/tap/kernel"}
106+
// brew upgrade doesn't refresh the tap first, so a freshly released
107+
// formula is invisible and the upgrade silently no-ops ("already
108+
// installed"). Update the tap before upgrading.
109+
return [][]string{
110+
{"brew", "update"},
111+
{"brew", "upgrade", "kernel/tap/kernel"},
112+
}
107113
case update.InstallMethodPNPM:
108-
return []string{"pnpm", "add", "-g", "@onkernel/cli@latest"}
114+
return [][]string{{"pnpm", "add", "-g", "@onkernel/cli@latest"}}
109115
case update.InstallMethodNPM:
110-
return []string{"npm", "i", "-g", "@onkernel/cli@latest"}
116+
return [][]string{{"npm", "i", "-g", "@onkernel/cli@latest"}}
111117
case update.InstallMethodBun:
112-
return []string{"bun", "add", "-g", "@onkernel/cli@latest"}
118+
return [][]string{{"bun", "add", "-g", "@onkernel/cli@latest"}}
113119
default:
114120
return nil
115121
}
116122
}
117123

118124
// getUpgradeCommand returns the command string for display (e.g., dry-run output).
119125
func getUpgradeCommand(method update.InstallMethod) string {
120-
args := upgradeCommandArgs(method)
121-
if args == nil {
126+
cmds := upgradeCommandArgs(method)
127+
if cmds == nil {
122128
return ""
123129
}
124-
return strings.Join(args, " ")
130+
joined := make([]string, len(cmds))
131+
for i, args := range cmds {
132+
joined[i] = strings.Join(args, " ")
133+
}
134+
return strings.Join(joined, " && ")
125135
}
126136

127-
// executeUpgrade runs the appropriate upgrade command based on the installation method.
128-
// Returns the captured stderr (for error diagnosis) and any error.
137+
// executeUpgrade runs the appropriate upgrade commands based on the installation
138+
// method. Returns the captured stderr (for error diagnosis) and any error.
129139
func executeUpgrade(method update.InstallMethod) (stderr string, err error) {
130-
args := upgradeCommandArgs(method)
131-
if args == nil {
140+
cmds := upgradeCommandArgs(method)
141+
if cmds == nil {
132142
return "", fmt.Errorf("unknown installation method")
133143
}
134144

135-
cmd := exec.Command(args[0], args[1:]...)
136-
cmd.Stdout = os.Stdout
137-
cmd.Stdin = os.Stdin
138-
139-
// Capture stderr while also displaying it to the user
140145
var stderrBuf bytes.Buffer
141-
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
142-
143-
err = cmd.Run()
144-
return stderrBuf.String(), err
146+
for _, args := range cmds {
147+
cmd := exec.Command(args[0], args[1:]...)
148+
cmd.Stdout = os.Stdout
149+
cmd.Stdin = os.Stdin
150+
// Capture stderr while also displaying it to the user
151+
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
152+
if err = cmd.Run(); err != nil {
153+
return stderrBuf.String(), err
154+
}
155+
}
156+
return stderrBuf.String(), nil
145157
}
146158

147159
// printManualUpgradeInstructions prints instructions for manually upgrading kernel

‎cmd/upgrade_test.go‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/kernel/cli/pkg/update"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestUpgradeCommandArgs(t *testing.T) {
11+
tests := []struct {
12+
method update.InstallMethod
13+
expected [][]string
14+
}{
15+
{
16+
// brew upgrade no-ops on a stale tap, so update must run first.
17+
update.InstallMethodBrew,
18+
[][]string{
19+
{"brew", "update"},
20+
{"brew", "upgrade", "kernel/tap/kernel"},
21+
},
22+
},
23+
{update.InstallMethodNPM, [][]string{{"npm", "i", "-g", "@onkernel/cli@latest"}}},
24+
{update.InstallMethodPNPM, [][]string{{"pnpm", "add", "-g", "@onkernel/cli@latest"}}},
25+
{update.InstallMethodBun, [][]string{{"bun", "add", "-g", "@onkernel/cli@latest"}}},
26+
{update.InstallMethodUnknown, nil},
27+
}
28+
for _, tt := range tests {
29+
t.Run(string(tt.method), func(t *testing.T) {
30+
assert.Equal(t, tt.expected, upgradeCommandArgs(tt.method))
31+
})
32+
}
33+
}
34+
35+
func TestUpgradeCommandArgsBrewUpdatesBeforeUpgrade(t *testing.T) {
36+
cmds := upgradeCommandArgs(update.InstallMethodBrew)
37+
updateIdx, upgradeIdx := -1, -1
38+
for i, args := range cmds {
39+
if len(args) >= 2 && args[0] == "brew" && args[1] == "update" {
40+
updateIdx = i
41+
}
42+
if len(args) >= 2 && args[0] == "brew" && args[1] == "upgrade" {
43+
upgradeIdx = i
44+
}
45+
}
46+
assert.NotEqual(t, -1, updateIdx, "expected a brew update step")
47+
assert.NotEqual(t, -1, upgradeIdx, "expected a brew upgrade step")
48+
assert.Less(t, updateIdx, upgradeIdx, "brew update must run before brew upgrade")
49+
}
50+
51+
func TestGetUpgradeCommandBrew(t *testing.T) {
52+
assert.Equal(t, "brew update && brew upgrade kernel/tap/kernel", getUpgradeCommand(update.InstallMethodBrew))
53+
assert.Equal(t, "", getUpgradeCommand(update.InstallMethodUnknown))
54+
}

‎pkg/update/check.go‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,15 +434,16 @@ func SuggestUpgradeCommand() string {
434434
func suggestUpgradeCommandForMethod(method InstallMethod) string {
435435
switch method {
436436
case InstallMethodBrew:
437-
return "brew upgrade kernel/tap/kernel"
437+
// brew upgrade alone can no-op on a stale tap, so refresh it first.
438+
return "brew update && brew upgrade kernel/tap/kernel"
438439
case InstallMethodPNPM:
439440
return "pnpm add -g @onkernel/cli@latest"
440441
case InstallMethodNPM:
441442
return "npm i -g @onkernel/cli@latest"
442443
case InstallMethodBun:
443444
return "bun add -g @onkernel/cli@latest"
444445
default:
445-
return "brew upgrade kernel/tap/kernel"
446+
return "brew update && brew upgrade kernel/tap/kernel"
446447
}
447448
}
448449

‎pkg/update/check_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ func TestSuggestUpgradeCommandForMethod(t *testing.T) {
1111
method InstallMethod
1212
expected string
1313
}{
14-
{InstallMethodBrew, "brew upgrade kernel/tap/kernel"},
14+
{InstallMethodBrew, "brew update && brew upgrade kernel/tap/kernel"},
1515
{InstallMethodNPM, "npm i -g @onkernel/cli@latest"},
1616
{InstallMethodPNPM, "pnpm add -g @onkernel/cli@latest"},
1717
{InstallMethodBun, "bun add -g @onkernel/cli@latest"},
18-
{InstallMethodUnknown, "brew upgrade kernel/tap/kernel"},
18+
{InstallMethodUnknown, "brew update && brew upgrade kernel/tap/kernel"},
1919
}
2020
for _, tt := range tests {
2121
t.Run(string(tt.method), func(t *testing.T) {

0 commit comments

Comments
 (0)