diff --git a/cmd/src/cmd.go b/cmd/src/cmd.go index 96a6fcec9e..b684a072f3 100644 --- a/cmd/src/cmd.go +++ b/cmd/src/cmd.go @@ -1,15 +1,19 @@ package main import ( + "context" "flag" "fmt" "log" "os" "strings" + "github.com/Masterminds/semver" "github.com/sourcegraph/sourcegraph/lib/errors" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/cmderrors" + "github.com/sourcegraph/src-cli/internal/version" ) // command is a subcommand handler and its flag set. @@ -54,6 +58,17 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] _ = flagSet.Parse(args) } + // Read global configuration. + var err error + cfg, err = readConfig() + if err != nil { + log.Fatal("reading config: ", err) + } + + // Check and warn about outdated version. + client := cfg.apiClient(api.NewFlags(flagSet), flagSet.Output()) + checkForOutdatedVersion(client) + // Print usage if the command is "help". if flagSet.Arg(0) == "help" || flagSet.NArg() == 0 { flagSet.Usage() @@ -80,13 +95,6 @@ func (c commander) run(flagSet *flag.FlagSet, cmdName, usageText string, args [] continue } - // Read global configuration now. - var err error - cfg, err = readConfig() - if err != nil { - log.Fatal("reading config: ", err) - } - // Parse subcommand flags. args := flagSet.Args()[1:] if err := cmd.flagSet.Parse(args); err != nil { @@ -126,3 +134,29 @@ func didYouMeanOtherCommand(actual string, suggested []string) *command { usageFunc: func() { log.Println(msg) }, } } + +func checkForOutdatedVersion(client api.Client) { + if version.BuildTag != version.DefaultBuildTag { + recommendedVersion, err := getRecommendedVersion(context.Background(), client) + if err != nil { + log.Fatal("failed to get recommended version for Sourcegraph deployment: ", err) + } + if recommendedVersion == "" { + log.Println("Recommended version: \nThis Sourcegraph instance does not support this feature.") + } else { + constraints, err := semver.NewConstraint(fmt.Sprintf("<=%s", version.BuildTag)) + if err != nil { + log.Fatal("failed to check current version: ", err) + } + + recommendedVersionInstance, err := semver.NewVersion(recommendedVersion) + if err != nil { + log.Fatal("failed to check version returned by Sourcegraph: ", err) + } + + if !constraints.Check(recommendedVersionInstance) { + log.Printf("⚠️ You are using an outdated version %s. Please upgrade to %s or later.\n", version.BuildTag, recommendedVersion) + } + } + } +} diff --git a/cmd/src/main_test.go b/cmd/src/main_test.go index 6b25654e7f..3f3309444d 100644 --- a/cmd/src/main_test.go +++ b/cmd/src/main_test.go @@ -1,12 +1,22 @@ package main import ( + "bytes" "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/mock" + + mockclient "github.com/sourcegraph/src-cli/internal/api/mock" + "github.com/sourcegraph/src-cli/internal/version" ) func TestReadConfig(t *testing.T) { @@ -219,3 +229,78 @@ func TestReadConfig(t *testing.T) { }) } } + +func TestCheckRecommendedVersion(t *testing.T) { + tests := []struct { + name string + buildTag string + recommendedVersion string + expectedWarning string + }{ + { + name: "Unknown version", + buildTag: "4.3.0", + recommendedVersion: "", + expectedWarning: "Recommended version: \nThis Sourcegraph instance does not support this feature.\n", + }, + { + name: "Same versions", + buildTag: "5.2.1", + recommendedVersion: "5.2.1", + expectedWarning: "", + }, + { + name: "Outdated version", + buildTag: "4.3.0", + recommendedVersion: "5.2.1", + expectedWarning: "⚠️ You are using an outdated version 4.3.0. Please upgrade to 5.2.1 or later.\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + version.BuildTag = test.buildTag + + client := &mockclient.Client{} + + req := httptest.NewRequest(http.MethodGet, "http://fake.com/.api/src-cli/version", nil) + client.On("NewHTTPRequest", mock.Anything, http.MethodGet, ".api/src-cli/version", mock.Anything). + Return(req, nil). + Once() + + resp := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(fmt.Sprintf(`{"version": "%s"}`, test.recommendedVersion))), + } + + client.On("Do", mock.Anything). + Return(resp, nil). + Once() + + output := redirectStdout(t) + + checkForOutdatedVersion(client) + + actualOutput := output.String() + + client.AssertExpectations(t) + + if actualOutput != test.expectedWarning { + t.Errorf("Expected warning message: %s, got: %s", test.expectedWarning, actualOutput) + } + }) + } +} + +// Redirect stdout for testing +func redirectStdout(t *testing.T) *bytes.Buffer { + t.Helper() + + var buf bytes.Buffer + log.SetFlags(0) + log.SetOutput(&buf) + t.Cleanup(func() { + log.SetOutput(os.Stdout) + }) + return &buf +}