From 46c38a569c5aec19c1a560ab5c54a7ed496b129e Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 19:20:59 +0100 Subject: [PATCH 01/12] Copy logic from planetscale/cli --- cli/internal/update/update.go | 233 +++++++++++++++++++++++++++++ cli/internal/update/update_test.go | 122 +++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 cli/internal/update/update.go create mode 100644 cli/internal/update/update_test.go diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go new file mode 100644 index 0000000000000..6c5b449f89da8 --- /dev/null +++ b/cli/internal/update/update.go @@ -0,0 +1,233 @@ +// Package update is checking for a new version of Turbo and informs the user +// to update. Most of the logic is copied from planetscale/cli: +// https://github.com/planetscale/cli/blob/main/internal/update/update.go +// and updated to our own needs. +package update + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/fatih/color" + "github.com/hashicorp/go-version" + "github.com/planetscale/cli/internal/cmdutil" + "github.com/planetscale/cli/internal/config" + "gopkg.in/yaml.v2" + + exec "golang.org/x/sys/execabs" +) + +type UpdateInfo struct { + Update bool + Reason string + ReleaseInfo *ReleaseInfo +} + +// ReleaseInfo stores information about a release +type ReleaseInfo struct { + Version string `json:"tag_name"` + URL string `json:"html_url"` + PublishedAt time.Time `json:"published_at"` +} + +// StateEntry stores the information we have checked for a new version. It's +// used to decide whether to check for a new version or not. +type StateEntry struct { + CheckedForUpdateAt time.Time `yaml:"checked_for_update_at"` + LatestRelease ReleaseInfo `yaml:"latest_release"` +} + +// CheckVersion checks for the given build version whether there is a new +// version of the CLI or not. +func CheckVersion(ctx context.Context, buildVersion string) error { + if ctx.Err() != nil { + return ctx.Err() + } + + path, err := stateFilePath() + if err != nil { + return err + } + + updateInfo, err := checkVersion( + ctx, + buildVersion, + path, + latestVersion, + ) + if err != nil { + return fmt.Errorf("skipping update, error: %s", err) + } + + if !updateInfo.Update { + return fmt.Errorf("skipping update, reason: %s", updateInfo.Reason) + } + + fmt.Fprintf(color.Error, "\n%s %s → %s\n", + color.BlueString("A new release of pscale is available:"), + color.CyanString(buildVersion), + color.CyanString(updateInfo.ReleaseInfo.Version)) + + var binpath string + if exepath, err := os.Executable(); err == nil { + binpath = exepath + } else if path, err := exec.LookPath("pscale"); err == nil { + binpath = path + } + + if cmdutil.IsUnderHomebrew(binpath) { + fmt.Fprintf(os.Stderr, "To upgrade, run: %s\n", "brew update && brew upgrade pscale") + } + fmt.Fprintf(color.Error, "%s\n", color.YellowString(updateInfo.ReleaseInfo.URL)) + return nil +} + +func checkVersion( + ctx context.Context, + buildVersion, path string, + latestVersionFn func(ctx context.Context, addr string) (*ReleaseInfo, error), +) (*UpdateInfo, error) { + if _, exists := os.LookupEnv("PSCALE_NO_UPDATE_NOTIFIER"); exists { + return &UpdateInfo{ + Update: false, + Reason: "PSCALE_NO_UPDATE_NOTIFIER is set", + }, nil + } + + stateEntry, _ := getStateEntry(path) + if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + return &UpdateInfo{ + Update: false, + Reason: "Latest version was already checked", + }, nil + } + + addr := "https://api.github.com/repos/planetscale/cli/releases/latest" + info, err := latestVersionFn(ctx, addr) + if err != nil { + return nil, err + } + + err = setStateEntry(path, time.Now(), *info) + if err != nil { + return nil, err + } + + v1, err := version.NewVersion(info.Version) + if err != nil { + return nil, err + } + + v2, err := version.NewVersion(buildVersion) + if err != nil { + return nil, err + } + + if v1.LessThanOrEqual(v2) { + return &UpdateInfo{ + Update: false, + Reason: fmt.Sprintf("Latest version (%s) is less than or equal to current build version (%s)", + info.Version, buildVersion), + ReleaseInfo: info, + }, nil + } + + return &UpdateInfo{ + Update: true, + Reason: fmt.Sprintf("Latest version (%s) is greater than the current build version (%s)", + info.Version, buildVersion), + ReleaseInfo: info, + }, nil + +} + +func latestVersion(ctx context.Context, addr string) (*ReleaseInfo, error) { + req, err := http.NewRequestWithContext(ctx, "GET", addr, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Accept", "application/vnd.github.v3+json") + + getToken := func() string { + if t := os.Getenv("GH_TOKEN"); t != "" { + return t + } + return os.Getenv("GITHUB_TOKEN") + } + + if token := getToken(); token != "" { + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + } + + client := &http.Client{Timeout: time.Second * 15} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + out, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return nil, fmt.Errorf("error fetching latest release: %v", string(out)) + } + + var info *ReleaseInfo + err = json.Unmarshal(out, &info) + if err != nil { + return nil, err + } + + return info, nil +} + +func getStateEntry(stateFilePath string) (*StateEntry, error) { + content, err := ioutil.ReadFile(stateFilePath) + if err != nil { + return nil, err + } + + var stateEntry StateEntry + err = yaml.Unmarshal(content, &stateEntry) + if err != nil { + return nil, err + } + + return &stateEntry, nil +} + +func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error { + data := StateEntry{ + CheckedForUpdateAt: t, + LatestRelease: r, + } + + content, err := yaml.Marshal(data) + if err != nil { + return err + } + _ = ioutil.WriteFile(stateFilePath, content, 0600) + + return nil +} + +func stateFilePath() (string, error) { + dir, err := config.ConfigDir() + if err != nil { + return "", err + } + + return filepath.Join(dir, "state.yml"), nil +} diff --git a/cli/internal/update/update_test.go b/cli/internal/update/update_test.go new file mode 100644 index 0000000000000..1f9b17d63b4e8 --- /dev/null +++ b/cli/internal/update/update_test.go @@ -0,0 +1,122 @@ +package update + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + "time" + + qt "github.com/frankban/quicktest" +) + +func TestLatestVersion(t *testing.T) { + c := qt.New(t) + + var tests = []struct { + name string + resp *ReleaseInfo + statusCode int + }{ + { + name: "valid response", + statusCode: 200, + resp: &ReleaseInfo{ + Version: "v0.1.0", + }, + }, + { + name: "non valid response", + statusCode: 400, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + _ = json.NewEncoder(w).Encode(tt.resp) + })) + defer ts.Close() + + info, err := latestVersion(context.Background(), ts.URL) + + success := tt.statusCode >= 200 && tt.statusCode < 300 + if !success { + c.Assert(err, qt.Not(qt.IsNil)) + } else { + c.Assert(err, qt.IsNil) + c.Assert(info, qt.DeepEquals, tt.resp) + } + + }) + } + +} + +func TestCheckVersion(t *testing.T) { + c := qt.New(t) + + var tests = []struct { + name string + buildVersion string + latestVersion string + update bool + lastChecked time.Time + }{ + { + name: "new version", + buildVersion: "v0.1.0", + latestVersion: "v0.2.0", + update: true, + }, + { + name: "same version", + buildVersion: "v0.2.0", + latestVersion: "v0.2.0", + update: false, + }, + { + name: "higher version", + buildVersion: "v0.3.0", + latestVersion: "v0.2.0", + update: false, + }, + { + name: "new version, but we already checked in the past 24 hours", + buildVersion: "v0.1.0", + latestVersion: "v0.2.0", + update: false, + lastChecked: time.Now().Add(-time.Hour), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + + dir := t.TempDir() + path := filepath.Join(dir, "state.yml") + + if !tt.lastChecked.IsZero() { + err := setStateEntry(path, tt.lastChecked, ReleaseInfo{Version: tt.latestVersion}) + c.Assert(err, qt.IsNil) + } + + updateInfo, err := checkVersion( + context.Background(), + tt.buildVersion, + path, + func(ctx context.Context, addr string) (*ReleaseInfo, error) { + return &ReleaseInfo{Version: tt.latestVersion}, nil + }, + ) + + c.Assert(err, qt.IsNil) + c.Assert(updateInfo.Update, qt.Equals, tt.update, qt.Commentf("reason: %s", updateInfo.Reason)) + + }) + } + +} From f85d2ef9813b94853e2caddabe3547e2089383c1 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 19:45:49 +0100 Subject: [PATCH 02/12] install go-version package --- cli/go.mod | 1 + cli/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cli/go.mod b/cli/go.mod index 34ff75c130d28..13bbb019e2291 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -17,6 +17,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-hclog v0.16.2 github.com/hashicorp/go-retryablehttp v0.6.8 + github.com/hashicorp/go-version v1.3.0 // indirect github.com/karrick/godirwalk v1.16.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/kr/text v0.2.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 7d793cfd72e47..3379afffe1480 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -51,6 +51,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= From 17802bb759ae2bf13f962b05b6c9b931a2f9bb2a Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 19:50:57 +0100 Subject: [PATCH 03/12] Refactor tests to use testify instead of quicktest --- cli/internal/update/update_test.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/cli/internal/update/update_test.go b/cli/internal/update/update_test.go index 1f9b17d63b4e8..1d728c680d146 100644 --- a/cli/internal/update/update_test.go +++ b/cli/internal/update/update_test.go @@ -9,11 +9,10 @@ import ( "testing" "time" - qt "github.com/frankban/quicktest" + "github.com/stretchr/testify/assert" ) func TestLatestVersion(t *testing.T) { - c := qt.New(t) var tests = []struct { name string @@ -45,10 +44,11 @@ func TestLatestVersion(t *testing.T) { success := tt.statusCode >= 200 && tt.statusCode < 300 if !success { - c.Assert(err, qt.Not(qt.IsNil)) + assert.NotNil(t, err) } else { - c.Assert(err, qt.IsNil) - c.Assert(info, qt.DeepEquals, tt.resp) + assert.Nil(t, err) + assert.EqualValues(t, tt.resp, info) + } }) @@ -57,7 +57,6 @@ func TestLatestVersion(t *testing.T) { } func TestCheckVersion(t *testing.T) { - c := qt.New(t) var tests = []struct { name string @@ -101,7 +100,7 @@ func TestCheckVersion(t *testing.T) { if !tt.lastChecked.IsZero() { err := setStateEntry(path, tt.lastChecked, ReleaseInfo{Version: tt.latestVersion}) - c.Assert(err, qt.IsNil) + assert.Nil(t, err) } updateInfo, err := checkVersion( @@ -113,8 +112,8 @@ func TestCheckVersion(t *testing.T) { }, ) - c.Assert(err, qt.IsNil) - c.Assert(updateInfo.Update, qt.Equals, tt.update, qt.Commentf("reason: %s", updateInfo.Reason)) + assert.Nil(t, err) + assert.EqualValues(t, tt.update, updateInfo.Update) }) } From 71fdab4a133b854e8661fd4e74c385521dc2c211 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 20:00:34 +0100 Subject: [PATCH 04/12] remove update with brew message --- cli/internal/update/update.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 6c5b449f89da8..43148b5e52380 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -16,11 +16,8 @@ import ( "github.com/fatih/color" "github.com/hashicorp/go-version" - "github.com/planetscale/cli/internal/cmdutil" "github.com/planetscale/cli/internal/config" "gopkg.in/yaml.v2" - - exec "golang.org/x/sys/execabs" ) type UpdateInfo struct { @@ -74,16 +71,6 @@ func CheckVersion(ctx context.Context, buildVersion string) error { color.CyanString(buildVersion), color.CyanString(updateInfo.ReleaseInfo.Version)) - var binpath string - if exepath, err := os.Executable(); err == nil { - binpath = exepath - } else if path, err := exec.LookPath("pscale"); err == nil { - binpath = path - } - - if cmdutil.IsUnderHomebrew(binpath) { - fmt.Fprintf(os.Stderr, "To upgrade, run: %s\n", "brew update && brew upgrade pscale") - } fmt.Fprintf(color.Error, "%s\n", color.YellowString(updateInfo.ReleaseInfo.URL)) return nil } From 3058bb0cca3e3ac8e60c07fa117a867058695d3c Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 20:10:37 +0100 Subject: [PATCH 05/12] add GetConfigDir to config_file.go --- cli/internal/config/config.go | 3 ++- cli/internal/config/config_file.go | 12 ++++++++++++ cli/internal/update/update.go | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index c62c6145e5886..3d3456d81dfd8 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -19,7 +19,8 @@ import ( const ( // EnvLogLevel is the environment log level - EnvLogLevel = "TURBO_LOG_LEVEL" + EnvLogLevel = "TURBO_LOG_LEVEL" + defaultConfigPath = "~/.config/turborepo" ) // IsCI returns true if running in a CI/CD environment diff --git a/cli/internal/config/config_file.go b/cli/internal/config/config_file.go index f4c513827bed6..647f112112ef5 100644 --- a/cli/internal/config/config_file.go +++ b/cli/internal/config/config_file.go @@ -2,10 +2,12 @@ package config import ( "encoding/json" + "fmt" "io/ioutil" "path/filepath" "github.com/adrg/xdg" + "github.com/mitchellh/go-homedir" ) // TurborepoConfig is a configuration object for the logged-in turborepo.com user @@ -83,3 +85,13 @@ func ReadUserConfigFile() (*TurborepoConfig, error) { func DeleteUserConfigFile() error { return WriteUserConfigFile(&TurborepoConfig{}) } + +// GetConfigDir is the directory for Turbo config. +func GetConfigDir() (string, error) { + dir, err := homedir.Expand(defaultConfigPath) + if err != nil { + return "", fmt.Errorf("can't expand path %q: %s", defaultConfigPath, err) + } + + return dir, nil +} diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 43148b5e52380..ea183aea5ffa3 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -13,10 +13,10 @@ import ( "os" "path/filepath" "time" + "turbo/internal/config" "github.com/fatih/color" "github.com/hashicorp/go-version" - "github.com/planetscale/cli/internal/config" "gopkg.in/yaml.v2" ) @@ -211,7 +211,7 @@ func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error { } func stateFilePath() (string, error) { - dir, err := config.ConfigDir() + dir, err := config.GetConfigDir() if err != nil { return "", err } From 8c0aae3c81c1951416fa70777465151e46951150 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 20:11:13 +0100 Subject: [PATCH 06/12] change api url --- cli/internal/update/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index ea183aea5ffa3..ee46f69f8dc23 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -95,7 +95,7 @@ func checkVersion( }, nil } - addr := "https://api.github.com/repos/planetscale/cli/releases/latest" + addr := "https://api.github.com/repos/vercel/turborepo/releases/latest" info, err := latestVersionFn(ctx, addr) if err != nil { return nil, err From 7a2d8ea5ef66e8f767db53aafc1fe61ba0b4d758 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 20:13:25 +0100 Subject: [PATCH 07/12] edit update message --- cli/internal/update/update.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index ee46f69f8dc23..60915a10177db 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -1,4 +1,4 @@ -// Package update is checking for a new version of Turbo and informs the user +// Package update is checking for a new version of Turborepo and informs the user // to update. Most of the logic is copied from planetscale/cli: // https://github.com/planetscale/cli/blob/main/internal/update/update.go // and updated to our own needs. @@ -67,7 +67,7 @@ func CheckVersion(ctx context.Context, buildVersion string) error { } fmt.Fprintf(color.Error, "\n%s %s → %s\n", - color.BlueString("A new release of pscale is available:"), + color.BlueString("A new release of turborepo is available:"), color.CyanString(buildVersion), color.CyanString(updateInfo.ReleaseInfo.Version)) From 3b66cb01af5fecb055e3fb06668264cda2f8f551 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Sun, 19 Dec 2021 20:19:39 +0100 Subject: [PATCH 08/12] add TURBO_NO_UPDATE_NOTIFIER --- cli/internal/update/update.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 60915a10177db..8852b68fa6b96 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -80,10 +80,10 @@ func checkVersion( buildVersion, path string, latestVersionFn func(ctx context.Context, addr string) (*ReleaseInfo, error), ) (*UpdateInfo, error) { - if _, exists := os.LookupEnv("PSCALE_NO_UPDATE_NOTIFIER"); exists { + if _, exists := os.LookupEnv("TURBO_NO_UPDATE_NOTIFIER"); exists { return &UpdateInfo{ Update: false, - Reason: "PSCALE_NO_UPDATE_NOTIFIER is set", + Reason: "TURBO_NO_UPDATE_NOTIFIER is set", }, nil } From 27530d05074b3890c2566a55ddf0e7be1ace237d Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Mon, 20 Dec 2021 06:04:11 +0100 Subject: [PATCH 09/12] use util.Printf instead of fatih/color --- cli/internal/update/update.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 8852b68fa6b96..48563eefe7d87 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -14,8 +14,8 @@ import ( "path/filepath" "time" "turbo/internal/config" + "turbo/internal/util" - "github.com/fatih/color" "github.com/hashicorp/go-version" "gopkg.in/yaml.v2" ) @@ -66,12 +66,8 @@ func CheckVersion(ctx context.Context, buildVersion string) error { return fmt.Errorf("skipping update, reason: %s", updateInfo.Reason) } - fmt.Fprintf(color.Error, "\n%s %s → %s\n", - color.BlueString("A new release of turborepo is available:"), - color.CyanString(buildVersion), - color.CyanString(updateInfo.ReleaseInfo.Version)) - - fmt.Fprintf(color.Error, "%s\n", color.YellowString(updateInfo.ReleaseInfo.URL)) + util.Printf("\n${BLUE}A new release of turborepo is available: ${CYAN}%s → %s\n", buildVersion, updateInfo.ReleaseInfo.Version) + util.Printf("${YELLOW}%s${RESET}\n", updateInfo.ReleaseInfo.URL) return nil } From 0268b23b92b146ce3ea2bdaf8af09c945c87ec27 Mon Sep 17 00:00:00 2001 From: Ayoub Baali Date: Mon, 20 Dec 2021 18:22:02 +0100 Subject: [PATCH 10/12] style: use dash case --- cli/internal/update/update.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 48563eefe7d87..98fa4d3918120 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -36,8 +36,8 @@ type ReleaseInfo struct { // StateEntry stores the information we have checked for a new version. It's // used to decide whether to check for a new version or not. type StateEntry struct { - CheckedForUpdateAt time.Time `yaml:"checked_for_update_at"` - LatestRelease ReleaseInfo `yaml:"latest_release"` + CheckedForUpdateAt time.Time `yaml:"checked-for-update-at"` + LatestRelease ReleaseInfo `yaml:"latest-release"` } // CheckVersion checks for the given build version whether there is a new From 97d9d05d4b822721ed8ee88e4ccdca89126596f4 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Tue, 21 Dec 2021 15:43:17 -0500 Subject: [PATCH 11/12] Spiffy updater --- cli/cmd/turbo/main.go | 7 ++++ cli/go.mod | 3 +- cli/go.sum | 3 +- cli/internal/api/types.go | 4 ++ cli/internal/backends/nodejs/nodejs.go | 12 ++++++ cli/internal/config/config.go | 5 +-- cli/internal/config/config_file.go | 25 ++++++------- cli/internal/login/link.go | 2 +- cli/internal/login/unlink.go | 2 +- cli/internal/update/update.go | 51 ++++++++++++++++++-------- 10 files changed, 78 insertions(+), 36 deletions(-) diff --git a/cli/cmd/turbo/main.go b/cli/cmd/turbo/main.go index 47922b379eeea..09be57d3fb6bc 100644 --- a/cli/cmd/turbo/main.go +++ b/cli/cmd/turbo/main.go @@ -1,7 +1,9 @@ package main import ( + "context" "fmt" + "log" "os" "runtime/debug" "strings" @@ -12,6 +14,7 @@ import ( prune "turbo/internal/prune" "turbo/internal/run" uiPkg "turbo/internal/ui" + "turbo/internal/update" "turbo/internal/util" "github.com/fatih/color" @@ -63,6 +66,7 @@ func main() { ui.Error(fmt.Sprintf("%s %s", uiPkg.ERROR_PREFIX, color.RedString(err.Error()))) os.Exit(1) } + c.HiddenCommands = []string{"graph"} c.Commands = map[string]cli.CommandFactory{ "run": func() (cli.Command, error) { @@ -164,5 +168,8 @@ func main() { } } }() + if err := update.CheckVersion(context.Background(), cf, turboVersion); err != nil { + log.Fatal(err) + } os.Exit(exitCode) } diff --git a/cli/go.mod b/cli/go.mod index 13bbb019e2291..9a41d928de828 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -15,9 +15,10 @@ require ( github.com/google/go-cmp v0.5.5 // indirect github.com/google/uuid v1.2.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v0.16.2 github.com/hashicorp/go-retryablehttp v0.6.8 - github.com/hashicorp/go-version v1.3.0 // indirect + github.com/hashicorp/go-version v1.3.0 github.com/karrick/godirwalk v1.16.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/kr/text v0.2.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 3379afffe1480..183182b27d085 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -41,8 +41,9 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= diff --git a/cli/internal/api/types.go b/cli/internal/api/types.go index c79addb68e925..365cd9135fa29 100644 --- a/cli/internal/api/types.go +++ b/cli/internal/api/types.go @@ -34,4 +34,8 @@ type LanguageBackend struct { GetWorkspaceGlobs func() ([]string, error) // Returns run command GetRunCommand func() []string + // GetInstallCommand returns the install command + GetInstallCommand func() []string + // GetTurboInstallCommand returns the turbo install command + GetTurboInstallCommand func() []string } diff --git a/cli/internal/backends/nodejs/nodejs.go b/cli/internal/backends/nodejs/nodejs.go index 20c8cc35e6fe2..6d64fdbe161be 100644 --- a/cli/internal/backends/nodejs/nodejs.go +++ b/cli/internal/backends/nodejs/nodejs.go @@ -31,6 +31,9 @@ var NodejsYarnBackend = api.LanguageBackend{ GetRunCommand: func() []string { return []string{"yarn", "run"} }, + GetTurboInstallCommand: func() []string { + return []string{"yarn", "add", "turbo", "--dev", "-w"} + }, } // PnpmWorkspaces is a representation of workspace package globs found @@ -61,6 +64,12 @@ var NodejsPnpmBackend = api.LanguageBackend{ GetRunCommand: func() []string { return []string{"pnpm", "run"} }, + GetInstallCommand: func() []string { + return []string{"pnpm", "install"} + }, + GetTurboInstallCommand: func() []string { + return []string{"pnpm", "install", "turbo", "-D", "-w"} + }, } var NodejsNpmBackend = api.LanguageBackend{ @@ -81,4 +90,7 @@ var NodejsNpmBackend = api.LanguageBackend{ GetRunCommand: func() []string { return []string{"npm", "run"} }, + GetTurboInstallCommand: func() []string { + return []string{"npm", "install", "turbo", "-D", "-w"} + }, } diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index 3d3456d81dfd8..d01923a03ff3b 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -19,8 +19,7 @@ import ( const ( // EnvLogLevel is the environment log level - EnvLogLevel = "TURBO_LOG_LEVEL" - defaultConfigPath = "~/.config/turborepo" + EnvLogLevel = "TURBO_LOG_LEVEL" ) // IsCI returns true if running in a CI/CD environment @@ -87,7 +86,7 @@ func ParseAndValidate(args []string, ui cli.Ui, turboVersion string) (c *Config, if err != nil { // not logged in } - partialConfig, err := ReadConfigFile(filepath.Join(".turbo", "config.json")) + partialConfig, err := ReadTurboConfigFile(filepath.Join(".turbo", "config.json")) if err != nil { // not linked } diff --git a/cli/internal/config/config_file.go b/cli/internal/config/config_file.go index 647f112112ef5..e08c79cb3eaf0 100644 --- a/cli/internal/config/config_file.go +++ b/cli/internal/config/config_file.go @@ -7,7 +7,6 @@ import ( "path/filepath" "github.com/adrg/xdg" - "github.com/mitchellh/go-homedir" ) // TurborepoConfig is a configuration object for the logged-in turborepo.com user @@ -25,7 +24,7 @@ type TurborepoConfig struct { } // WriteUserConfigFile writes config file at a oath -func WriteConfigFile(path string, config *TurborepoConfig) error { +func WriteTurboConfigFile(path string, config *TurborepoConfig) error { jsonBytes, marhsallError := json.Marshal(config) if marhsallError != nil { return marhsallError @@ -39,15 +38,15 @@ func WriteConfigFile(path string, config *TurborepoConfig) error { // WriteUserConfigFile writes a user config file func WriteUserConfigFile(config *TurborepoConfig) error { - path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json")) + path, err := GetConfigFilePath("config.json") if err != nil { return err } - return WriteConfigFile(path, config) + return WriteTurboConfigFile(path, config) } -// ReadConfigFile reads a config file at a path -func ReadConfigFile(path string) (*TurborepoConfig, error) { +// ReadTurboConfigFile reads a config file at a path +func ReadTurboConfigFile(path string) (*TurborepoConfig, error) { var config = &TurborepoConfig{ Token: "", TeamId: "", @@ -68,7 +67,7 @@ func ReadConfigFile(path string) (*TurborepoConfig, error) { // ReadUserConfigFile reads a user config file func ReadUserConfigFile() (*TurborepoConfig, error) { - path, err := xdg.ConfigFile(filepath.Join("turborepo", "config.json")) + path, err := GetConfigFilePath("config.json") if err != nil { return &TurborepoConfig{ Token: "", @@ -78,7 +77,7 @@ func ReadUserConfigFile() (*TurborepoConfig, error) { TeamSlug: "", }, err } - return ReadConfigFile(path) + return ReadTurboConfigFile(path) } // DeleteUserConfigFile deletes a user config file @@ -86,12 +85,12 @@ func DeleteUserConfigFile() error { return WriteUserConfigFile(&TurborepoConfig{}) } -// GetConfigDir is the directory for Turbo config. -func GetConfigDir() (string, error) { - dir, err := homedir.Expand(defaultConfigPath) +// GetConfigFilePath is the path to the config file on the machine +func GetConfigFilePath(name string) (string, error) { + file, err := xdg.ConfigFile(filepath.Join("turborepo", name)) if err != nil { - return "", fmt.Errorf("can't expand path %q: %s", defaultConfigPath, err) + return "", fmt.Errorf("cannot get configuration file %q: %s", name, err) } - return dir, nil + return file, nil } diff --git a/cli/internal/login/link.go b/cli/internal/login/link.go index 4f6162f5aca79..298cc4129ad75 100644 --- a/cli/internal/login/link.go +++ b/cli/internal/login/link.go @@ -142,7 +142,7 @@ func (c *LinkCommand) Run(args []string) int { } } fs.EnsureDir(filepath.Join(".turbo", "config.json")) - fsErr := config.WriteConfigFile(filepath.Join(".turbo", "config.json"), &config.TurborepoConfig{ + fsErr := config.WriteTurboConfigFile(filepath.Join(".turbo", "config.json"), &config.TurborepoConfig{ TeamId: chosenTeam.ID, ApiUrl: c.Config.ApiUrl, }) diff --git a/cli/internal/login/unlink.go b/cli/internal/login/unlink.go index e44764b5f39a6..d83ea5fb6ab03 100644 --- a/cli/internal/login/unlink.go +++ b/cli/internal/login/unlink.go @@ -36,7 +36,7 @@ Usage: turbo unlink // Run executes tasks in the monorepo func (c *UnlinkCommand) Run(args []string) int { - if err := config.WriteConfigFile(filepath.Join(".turbo", "config.json"), &config.TurborepoConfig{}); err != nil { + if err := config.WriteTurboConfigFile(filepath.Join(".turbo", "config.json"), &config.TurborepoConfig{}); err != nil { c.logError(c.Config.Logger, "", fmt.Errorf("Could not unlink. Something went wrong: %w", err)) return 1 } diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index 98fa4d3918120..f6b65fbbb67c6 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -11,15 +11,21 @@ import ( "io/ioutil" "net/http" "os" - "path/filepath" + "strings" "time" + "turbo/internal/backends" "turbo/internal/config" "turbo/internal/util" + "unicode/utf8" + + cleanhttp "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-version" "gopkg.in/yaml.v2" ) +const RELEASE_URL = "https://api.github.com/repos/vercel/turborepo/releases/latest" + type UpdateInfo struct { Update bool Reason string @@ -42,7 +48,7 @@ type StateEntry struct { // CheckVersion checks for the given build version whether there is a new // version of the CLI or not. -func CheckVersion(ctx context.Context, buildVersion string) error { +func CheckVersion(ctx context.Context, config *config.Config, buildVersion string) error { if ctx.Err() != nil { return ctx.Err() } @@ -59,15 +65,31 @@ func CheckVersion(ctx context.Context, buildVersion string) error { latestVersion, ) if err != nil { - return fmt.Errorf("skipping update, error: %s", err) + config.Logger.Debug("No update available", "latest version", updateInfo.ReleaseInfo.Version) + return nil } if !updateInfo.Update { - return fmt.Errorf("skipping update, reason: %s", updateInfo.Reason) + config.Logger.Debug("No update available, latest version is currently installed", "version", buildVersion) + return nil } - util.Printf("\n${BLUE}A new release of turborepo is available: ${CYAN}%s → %s\n", buildVersion, updateInfo.ReleaseInfo.Version) - util.Printf("${YELLOW}%s${RESET}\n", updateInfo.ReleaseInfo.URL) + backend, err := backends.GetBackend() + if err != nil { + return fmt.Errorf("cannot infer language backend and package manager: %w", err) + } + installCmdStr := strings.Join(backend.GetTurboInstallCommand(), " ") + installCommandLen := utf8.RuneCountInString((installCmdStr)) + util.Printf("${YELLOW}+----------------------------------------------------------------+${RESET}\n") + util.Printf("${YELLOW}|${RESET} ${YELLOW}|${RESET}\n") + util.Printf("${YELLOW}|${RESET} Update avaiable for turbo: ${GREY}%s${RESET} → ${CYAN}%s${RESET}%s${YELLOW}|${RESET}\n", fmt.Sprintf("v%s", buildVersion), updateInfo.ReleaseInfo.Version, strings.Repeat(" ", 14-len(updateInfo.ReleaseInfo.Version))) + util.Printf("${YELLOW}|${RESET} Run ${CYAN}%s${RESET} to update%s${YELLOW}|${RESET}\n", installCmdStr, strings.Repeat(" ", 46-installCommandLen)) + util.Printf("${YELLOW}|${RESET} ${YELLOW}|${RESET}\n") + util.Printf("${YELLOW}+----------------------------------------------------------------+${RESET}\n") + util.Printf("\n") + util.Printf("${GREY}For more information and release notes, visit:${RESET}\n") + util.Printf("${GREY}${UNDERLINE}%s${RESET}\n", updateInfo.ReleaseInfo.URL) + util.Printf("\n") return nil } @@ -84,14 +106,14 @@ func checkVersion( } stateEntry, _ := getStateEntry(path) - if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { + if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 1 { return &UpdateInfo{ Update: false, Reason: "Latest version was already checked", }, nil } - addr := "https://api.github.com/repos/vercel/turborepo/releases/latest" + addr := RELEASE_URL info, err := latestVersionFn(ctx, addr) if err != nil { return nil, err @@ -131,6 +153,7 @@ func checkVersion( } func latestVersion(ctx context.Context, addr string) (*ReleaseInfo, error) { + cleanClient := cleanhttp.DefaultClient() req, err := http.NewRequestWithContext(ctx, "GET", addr, nil) if err != nil { return nil, err @@ -150,8 +173,9 @@ func latestVersion(ctx context.Context, addr string) (*ReleaseInfo, error) { req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) } - client := &http.Client{Timeout: time.Second * 15} - resp, err := client.Do(req) + cleanClient.Timeout = time.Second * 15 + + resp, err := cleanClient.Do(req) if err != nil { return nil, err } @@ -207,10 +231,5 @@ func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error { } func stateFilePath() (string, error) { - dir, err := config.GetConfigDir() - if err != nil { - return "", err - } - - return filepath.Join(dir, "state.yml"), nil + return config.GetConfigFilePath("state.yml") } From 74c715d0cfaad926f9d7a35bfc22970dcb344962 Mon Sep 17 00:00:00 2001 From: Jared Palmer Date: Fri, 24 Dec 2021 14:31:29 -0500 Subject: [PATCH 12/12] Update cli/internal/update/update.go Co-authored-by: Weyert de Boer --- cli/internal/update/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/internal/update/update.go b/cli/internal/update/update.go index f6b65fbbb67c6..93a532d0027fc 100644 --- a/cli/internal/update/update.go +++ b/cli/internal/update/update.go @@ -82,7 +82,7 @@ func CheckVersion(ctx context.Context, config *config.Config, buildVersion strin installCommandLen := utf8.RuneCountInString((installCmdStr)) util.Printf("${YELLOW}+----------------------------------------------------------------+${RESET}\n") util.Printf("${YELLOW}|${RESET} ${YELLOW}|${RESET}\n") - util.Printf("${YELLOW}|${RESET} Update avaiable for turbo: ${GREY}%s${RESET} → ${CYAN}%s${RESET}%s${YELLOW}|${RESET}\n", fmt.Sprintf("v%s", buildVersion), updateInfo.ReleaseInfo.Version, strings.Repeat(" ", 14-len(updateInfo.ReleaseInfo.Version))) + util.Printf("${YELLOW}|${RESET} Update available for turbo: ${GREY}%s${RESET} → ${CYAN}%s${RESET}%s${YELLOW}|${RESET}\n", fmt.Sprintf("v%s", buildVersion), updateInfo.ReleaseInfo.Version, strings.Repeat(" ", 14-len(updateInfo.ReleaseInfo.Version))) util.Printf("${YELLOW}|${RESET} Run ${CYAN}%s${RESET} to update%s${YELLOW}|${RESET}\n", installCmdStr, strings.Repeat(" ", 46-installCommandLen)) util.Printf("${YELLOW}|${RESET} ${YELLOW}|${RESET}\n") util.Printf("${YELLOW}+----------------------------------------------------------------+${RESET}\n")