-
Notifications
You must be signed in to change notification settings - Fork 38
feat(BRE2-810): sudo gating, brev upgrade #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package upgrade | ||
|
|
||
| import "os/exec" | ||
|
|
||
| // InstallMethod represents how brev was installed on the system. | ||
| type InstallMethod int | ||
|
|
||
| const ( | ||
| // InstallMethodDirect means brev was installed via direct binary download. | ||
| InstallMethodDirect InstallMethod = iota | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment about |
||
| // InstallMethodBrew means brev was installed via Homebrew. | ||
| InstallMethodBrew | ||
| ) | ||
|
|
||
| // Detector determines how brev was installed. | ||
| type Detector interface { | ||
| Detect() InstallMethod | ||
| } | ||
|
|
||
| // SystemDetector checks the actual system for install method. | ||
| type SystemDetector struct{} | ||
|
|
||
| // Detect checks whether brev was installed via Homebrew or direct download. | ||
| func (SystemDetector) Detect() InstallMethod { | ||
| if _, err := exec.LookPath("brew"); err != nil { | ||
| return InstallMethodDirect | ||
| } | ||
| if exec.Command("brew", "list", "brev").Run() == nil { //nolint:gosec // intentional brew probe | ||
| return InstallMethodBrew | ||
| } | ||
| return InstallMethodDirect | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package upgrade | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
|
|
||
| "github.com/brevdev/brev-cli/pkg/terminal" | ||
| ) | ||
|
|
||
| // Upgrader executes the actual upgrade process. | ||
| type Upgrader interface { | ||
| UpgradeViaBrew(t *terminal.Terminal) error | ||
| UpgradeViaInstallScript(t *terminal.Terminal) error | ||
| } | ||
|
|
||
| // SystemUpgrader executes upgrade commands on the real system. | ||
| type SystemUpgrader struct{} | ||
|
|
||
| // UpgradeViaBrew runs "brew upgrade brev" with output connected to the terminal. | ||
| func (SystemUpgrader) UpgradeViaBrew(t *terminal.Terminal) error { | ||
| t.Vprint("Running: brew upgrade brev") | ||
| t.Vprint("") | ||
|
|
||
| cmd := exec.Command("brew", "upgrade", "brev") | ||
| cmd.Stdout = os.Stdout | ||
| cmd.Stderr = os.Stderr | ||
| if err := cmd.Run(); err != nil { | ||
| return fmt.Errorf("brew upgrade failed: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // UpgradeViaInstallScript downloads and installs the latest brev binary from GitHub. | ||
| func (SystemUpgrader) UpgradeViaInstallScript(t *terminal.Terminal) error { | ||
| osName := runtime.GOOS | ||
| arch := runtime.GOARCH | ||
|
|
||
| t.Vprintf("Detected platform: %s/%s\n", osName, arch) | ||
| t.Vprint("") | ||
|
|
||
| // Fetch latest release download URL | ||
| t.Vprint("Fetching latest release...") | ||
| pattern := fmt.Sprintf("browser_download_url.*%s.*%s", osName, arch) | ||
| curlCmd := fmt.Sprintf( | ||
| `curl -s https://api.github.com/repos/brevdev/brev-cli/releases/latest | grep "%s" | cut -d '"' -f 4`, | ||
| pattern, | ||
| ) | ||
| out, err := exec.Command("bash", "-c", curlCmd).Output() //nolint:gosec // constructing URL pattern from known values | ||
| if err != nil { | ||
| return fmt.Errorf("failed to fetch latest release: %w", err) | ||
| } | ||
| downloadURL := string(out) | ||
| if downloadURL == "" { | ||
| return fmt.Errorf("could not find release for %s/%s", osName, arch) | ||
| } | ||
| // Trim trailing newline | ||
| downloadURL = downloadURL[:len(downloadURL)-1] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this newline guaranteed to be there?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rewrote to use script instead |
||
|
|
||
| // Create temp directory | ||
| tmpDir, err := os.MkdirTemp("", "brev-upgrade-*") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. apparently this controls where the random string goes. By default it gets appended, removing. |
||
| if err != nil { | ||
| return fmt.Errorf("failed to create temp directory: %w", err) | ||
| } | ||
| defer os.RemoveAll(tmpDir) | ||
|
|
||
| // Download | ||
| t.Vprintf("Downloading %s...\n", downloadURL) | ||
| tarPath := filepath.Join(tmpDir, "brev.tar.gz") | ||
| dlCmd := exec.Command("curl", "-sL", downloadURL, "-o", tarPath) | ||
| dlCmd.Stderr = os.Stderr | ||
| if err := dlCmd.Run(); err != nil { | ||
| return fmt.Errorf("download failed: %w", err) | ||
| } | ||
|
|
||
| // Extract | ||
| extractCmd := exec.Command("tar", "-xzf", tarPath, "-C", tmpDir) | ||
| if err := extractCmd.Run(); err != nil { | ||
| return fmt.Errorf("extraction failed: %w", err) | ||
| } | ||
|
|
||
| // Install with sudo | ||
| brevPath := filepath.Join(tmpDir, "brev") | ||
| t.Vprint("Installing to /usr/local/bin/brev...") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: could throw |
||
| mvCmd := exec.Command("sudo", "mv", brevPath, "/usr/local/bin/brev") | ||
| mvCmd.Stdout = os.Stdout | ||
| mvCmd.Stderr = os.Stderr | ||
| if err := mvCmd.Run(); err != nil { | ||
| return fmt.Errorf("failed to install binary: %w", err) | ||
| } | ||
|
|
||
| chmodCmd := exec.Command("sudo", "chmod", "+x", "/usr/local/bin/brev") | ||
| if err := chmodCmd.Run(); err != nil { | ||
| return fmt.Errorf("failed to set permissions: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // Package upgrade provides the brev upgrade command. | ||
| package upgrade | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/brevdev/brev-cli/pkg/cmd/register" | ||
| "github.com/brevdev/brev-cli/pkg/cmd/version" | ||
| "github.com/brevdev/brev-cli/pkg/store" | ||
| "github.com/brevdev/brev-cli/pkg/sudo" | ||
| "github.com/brevdev/brev-cli/pkg/terminal" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // VersionStore fetches the latest release metadata from GitHub. | ||
| type VersionStore interface { | ||
| GetLatestReleaseMetadata() (*store.GithubReleaseMetadata, error) | ||
| } | ||
|
|
||
| type upgradeDeps struct { | ||
| detector Detector | ||
| upgrader Upgrader | ||
| confirmer terminal.Confirmer | ||
| } | ||
|
|
||
| func defaultUpgradeDeps() upgradeDeps { | ||
| return upgradeDeps{ | ||
| detector: SystemDetector{}, | ||
| upgrader: SystemUpgrader{}, | ||
| confirmer: register.TerminalPrompter{}, | ||
| } | ||
| } | ||
|
|
||
| var ( | ||
| upgradeLong = "Upgrade brev to the latest version." | ||
| upgradeExample = " brev upgrade" | ||
| ) | ||
|
|
||
| // NewCmdUpgrade creates the brev upgrade command. | ||
| func NewCmdUpgrade(t *terminal.Terminal, versionStore VersionStore) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Annotations: map[string]string{"configuration": ""}, | ||
| Use: "upgrade", | ||
| DisableFlagsInUseLine: true, | ||
| Short: "Upgrade brev to the latest version", | ||
| Long: upgradeLong, | ||
| Example: upgradeExample, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return runUpgrade(t, versionStore, defaultUpgradeDeps()) | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
|
|
||
| func runUpgrade(t *terminal.Terminal, vs VersionStore, deps upgradeDeps) error { | ||
| t.Vprint("") | ||
| t.Vprintf("Current version: %s\n", version.Version) | ||
|
|
||
| release, err := vs.GetLatestReleaseMetadata() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to check latest version: %w", err) | ||
| } | ||
|
|
||
| if release.TagName == version.Version { | ||
| t.Vprint(t.Green("Already up to date.")) | ||
| return nil | ||
| } | ||
|
|
||
| t.Vprintf("New version available: %s\n", release.TagName) | ||
| t.Vprint("") | ||
|
|
||
| method := deps.detector.Detect() | ||
|
|
||
| switch method { | ||
| case InstallMethodBrew: | ||
| return upgradeViaBrew(t, deps) | ||
| case InstallMethodDirect: | ||
| return upgradeViaDirect(t, deps) | ||
| default: | ||
| return fmt.Errorf("unknown install method") | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also install the latest skill? |
||
| } | ||
|
|
||
| func upgradeViaBrew(t *terminal.Terminal, deps upgradeDeps) error { | ||
| t.Vprint("Detected install method: Homebrew") | ||
| t.Vprint("This will run: brew upgrade brev") | ||
| t.Vprint("") | ||
|
|
||
| if !deps.confirmer.ConfirmYesNo("Proceed with upgrade?") { | ||
| t.Vprint("Upgrade canceled.") | ||
| return nil | ||
| } | ||
|
|
||
| t.Vprint("") | ||
| if err := deps.upgrader.UpgradeViaBrew(t); err != nil { | ||
| return fmt.Errorf("brew upgrade: %w", err) | ||
| } | ||
|
|
||
| t.Vprint("") | ||
| t.Vprint(t.Green("Upgrade complete.")) | ||
| return nil | ||
| } | ||
|
|
||
| func upgradeViaDirect(t *terminal.Terminal, deps upgradeDeps) error { | ||
| t.Vprint("Detected install method: direct binary install") | ||
| t.Vprint("This will download the latest release and install it to /usr/local/bin/brev") | ||
| t.Vprint("") | ||
|
|
||
| if err := sudo.Gate(t, deps.confirmer, "Upgrade"); err != nil { | ||
| return fmt.Errorf("sudo issue: %w", err) | ||
| } | ||
|
|
||
| t.Vprint("") | ||
| if err := deps.upgrader.UpgradeViaInstallScript(t); err != nil { | ||
| return fmt.Errorf("direct upgrade: %w", err) | ||
| } | ||
|
|
||
| t.Vprint("") | ||
| t.Vprint(t.Green("Upgrade complete.")) | ||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing
errtoeerrseems unnecessary here