diff --git a/internal/cli/plugin/plugin_github_asset.go b/internal/cli/plugin/plugin_github_asset.go index e402a3f49c..c0536b9e68 100644 --- a/internal/cli/plugin/plugin_github_asset.go +++ b/internal/cli/plugin/plugin_github_asset.go @@ -79,10 +79,21 @@ func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) { // download latest release if version is not specified if g.version == nil { - release, _, err = g.ghClient.Repositories.GetLatestRelease(context.Background(), g.owner, g.name) + // download the 100 latest releases + const MaxPerPage = 100 + releases, _, err := g.ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{ + Page: 0, + PerPage: MaxPerPage, + }) if err != nil { - return nil, fmt.Errorf("could not find latest release for %s", g.repository()) + return nil, fmt.Errorf("could not fetch releases for %s %w", g.repository(), err) + } + + // get the latest release that doesn't have prerelease info or metadata in the version tag + release = getLatestStableRelease(releases) + if release == nil { + return nil, fmt.Errorf("could not find latest stable release for %s", g.repository()) } } else { // try to find the release with the version tag with v prefix, if it does not exist try again without the prefix @@ -100,6 +111,32 @@ func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) { return release.Assets, nil } +func getLatestStableRelease(releases []*github.RepositoryRelease) *github.RepositoryRelease { + var latestStableVersion *semver.Version + var latestStableRelease *github.RepositoryRelease + + for _, release := range releases { + version, err := semver.NewVersion(*release.TagName) + + // if we can't parse the version tag, skip this release + if err != nil { + continue + } + + // if the version has pre-release info or metadata, skip this version + if version.Prerelease() != "" || version.Metadata() != "" { + continue + } + + if latestStableVersion == nil || version.GreaterThan(latestStableVersion) { + latestStableVersion = version + latestStableRelease = release + } + } + + return latestStableRelease +} + var architectureAliases = map[string][]string{ "amd64": {"x86_64"}, "arm64": {"aarch64"}, @@ -173,7 +210,7 @@ func (g *GithubAsset) getPluginAssetAsReadCloser(assetID int64) (io.ReadCloser, } func parseGithubReleaseValues(arg string) (*GithubAsset, error) { - regexPattern := `^((https?://(www\.)?)?github\.com/)?(?P[\w.\-]+)/(?P[\w.\-]+)/?(@(?Pv?(\d+)(\.\d+)?(\.\d+)?|latest))?$` + regexPattern := `^((https?://(www\.)?)?github\.com/)?(?P[\w.\-]+)/(?P[\w.\-]+)/?(@(?P.+))?$` regex, err := regexp.Compile(regexPattern) if err != nil { return nil, fmt.Errorf("error compiling regex: %w", err) @@ -196,6 +233,7 @@ func parseGithubReleaseValues(arg string) (*GithubAsset, error) { githubRelease := &GithubAsset{owner: groupMap["owner"], name: groupMap["name"]} if version, ok := groupMap["version"]; ok && version != latest && version != "" { + version := strings.TrimPrefix(version, "v") semverVersion, err := semver.NewVersion(version) if err != nil { return nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, version) diff --git a/internal/cli/plugin/plugin_github_asset_test.go b/internal/cli/plugin/plugin_github_asset_test.go index d1eb7a01ba..5db8c5a655 100644 --- a/internal/cli/plugin/plugin_github_asset_test.go +++ b/internal/cli/plugin/plugin_github_asset_test.go @@ -23,6 +23,7 @@ import ( "github.com/Masterminds/semver/v3" "github.com/google/go-github/v61/github" "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/plugin" + "github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -204,77 +205,96 @@ func Test_parseGithubRepoValues(t *testing.T) { expectedOwner = "mongodb" expectedName = "atlas-cli-plugin-example" ) - var expectedVersion, _ = semver.NewVersion("1.0.0") + var v1_0_0, _ = semver.NewVersion("1.0.0") + //nolint:revive,stylecheck + var v1_0_0_PRE, _ = semver.NewVersion("1.0.0-prerelease") + //nolint:revive,stylecheck + var v1_0_0_BETA_AND_META, _ = semver.NewVersion("1.0.0-beta+very-meta") tests := []struct { - arg string - expectVersion bool - expectError bool + arg string + expectedVersion *semver.Version + expectError bool }{ { - arg: "mongodb/atlas-cli-plugin-example", - expectVersion: false, - expectError: false, + arg: "mongodb/atlas-cli-plugin-example", + expectedVersion: nil, + expectError: false, + }, + { + arg: "mongodb/atlas-cli-plugin-example@1.0.0", + expectedVersion: v1_0_0, + expectError: false, + }, + { + arg: "mongodb/atlas-cli-plugin-example@v1.0.0", + expectedVersion: v1_0_0, + expectError: false, + }, + { + arg: "mongodb/atlas-cli-plugin-example@1.0.0-prerelease", + expectedVersion: v1_0_0_PRE, + expectError: false, }, { - arg: "mongodb/atlas-cli-plugin-example@1.0.0", - expectVersion: true, - expectError: false, + arg: "mongodb/atlas-cli-plugin-example@1.0.0-beta+very-meta", + expectedVersion: v1_0_0_BETA_AND_META, + expectError: false, }, { - arg: "mongodb/atlas-cli-plugin-example@", - expectVersion: false, - expectError: true, + arg: "mongodb/atlas-cli-plugin-example@", + expectedVersion: nil, + expectError: true, }, { - arg: "mongodb/atlas-cli-plugin-example/", - expectVersion: false, - expectError: false, + arg: "mongodb/atlas-cli-plugin-example/", + expectedVersion: nil, + expectError: false, }, { - arg: "mongodb/atlas-cli-plugin-example/@v1", - expectVersion: true, - expectError: false, + arg: "mongodb/atlas-cli-plugin-example/@v1", + expectedVersion: v1_0_0, + expectError: false, }, { - arg: "https://github.com/mongodb/atlas-cli-plugin-example", - expectVersion: false, - expectError: false, + arg: "https://github.com/mongodb/atlas-cli-plugin-example", + expectedVersion: nil, + expectError: false, }, { - arg: "https://github.com/mongodb/atlas-cli-plugin-example@v1.0", - expectVersion: false, - expectError: false, + arg: "https://github.com/mongodb/atlas-cli-plugin-example@v1.0", + expectedVersion: v1_0_0, + expectError: false, }, { - arg: "github.com/mongodb/atlas-cli-plugin-example/", - expectVersion: false, - expectError: false, + arg: "github.com/mongodb/atlas-cli-plugin-example/", + expectedVersion: nil, + expectError: false, }, { - arg: "github.com/mongodb/atlas-cli-plugin-example/@v1.0.0", - expectVersion: true, - expectError: false, + arg: "github.com/mongodb/atlas-cli-plugin-example/@v1.0.0", + expectedVersion: v1_0_0, + expectError: false, }, { - arg: "/mongodb/atlas-cli-plugin-example/", - expectVersion: false, - expectError: true, + arg: "/mongodb/atlas-cli-plugin-example/", + expectedVersion: nil, + expectError: true, }, { - arg: "mongodb@atlas-cli-plugin-example", - expectVersion: false, - expectError: true, + arg: "mongodb@atlas-cli-plugin-example", + expectedVersion: nil, + expectError: true, }, { - arg: "mongodb@atlas-cli-plugin-example@1.0", - expectVersion: false, - expectError: true, + arg: "mongodb@atlas-cli-plugin-example@1.0", + expectedVersion: nil, + expectError: true, }, { - arg: "invalidArgString", - expectVersion: false, - expectError: true, + arg: "invalidArgString", + expectedVersion: nil, + expectError: true, }, } @@ -291,8 +311,12 @@ func Test_parseGithubRepoValues(t *testing.T) { if githubRelease.name != expectedName { t.Errorf("expected name: %s, got: %s", expectedName, githubRelease.owner) } - if tt.expectVersion && !expectedVersion.Equal(githubRelease.version) { - t.Errorf("expected version: %s, got: %s", expectedVersion.String(), githubRelease.version.String()) + if tt.expectedVersion != nil && !tt.expectedVersion.Equal(githubRelease.version) { + t.Errorf("expected version: %s, got: %s", tt.expectedVersion.String(), githubRelease.version.String()) + } + + if tt.expectedVersion == nil && githubRelease.version != nil { + t.Errorf("expected version to be nil, got: %s", githubRelease.version.String()) } } }) @@ -352,3 +376,78 @@ func Test_getPluginDirectoryName(t *testing.T) { githubAsset := &GithubAsset{owner: "owner", name: "name"} require.Equal(t, "owner@name", githubAsset.getPluginDirectoryName()) } + +func Test_getLatestStableRelease(t *testing.T) { + tests := []struct { + name string + releases []*github.RepositoryRelease + expected *github.RepositoryRelease + }{ + { + name: "Single valid value", + releases: []*github.RepositoryRelease{ + { + TagName: pointer.Get("v1.0.0"), + }, + }, + expected: &github.RepositoryRelease{ + TagName: pointer.Get("v1.0.0"), + }, + }, + { + name: "Single invalid value", + releases: []*github.RepositoryRelease{ + { + TagName: pointer.Get("test"), + }, + }, + expected: nil, + }, + { + name: "Single valid pre-release value", + releases: []*github.RepositoryRelease{ + { + TagName: pointer.Get("v1.0.0-pre"), + }, + }, + expected: nil, + }, + { + name: "Multiple", + releases: []*github.RepositoryRelease{ + { + TagName: pointer.Get("v2.0.0-pre"), + }, + { + TagName: pointer.Get("v2.0.0-beta"), + }, + { + TagName: pointer.Get("v1.2.1"), + }, + { + TagName: pointer.Get("v1.2.0"), + }, + { + TagName: pointer.Get("v1.1.0"), + }, + { + TagName: pointer.Get("v1.0.1"), + }, + { + TagName: pointer.Get("v1.0.0"), + }, + }, + expected: &github.RepositoryRelease{ + TagName: pointer.Get("v1.2.1"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := getLatestStableRelease(tt.releases) + + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/internal/cli/plugin/update.go b/internal/cli/plugin/update.go index e245a66a82..c39a2e1ca0 100644 --- a/internal/cli/plugin/update.go +++ b/internal/cli/plugin/update.go @@ -21,6 +21,7 @@ import ( "os" "path" "regexp" + "strings" "github.com/Masterminds/semver/v3" "github.com/google/go-github/v61/github" @@ -55,7 +56,7 @@ func printPluginUpdateWarning(p *plugin.Plugin, err error) { // extract plugin specifier and version given the input argument of the update command. func extractPluginSpecifierAndVersionFromArg(arg string) (string, *semver.Version, error) { - regexPattern := `^(?P[^\s@]+)(@(?Pv?(\d+)(\.\d+)?(\.\d+)?|latest))?$` + regexPattern := `^(?P[^\s@]+)(@(?P.+))?$` regex, err := regexp.Compile(regexPattern) if err != nil { return "", nil, fmt.Errorf("error compiling regex: %w", err) @@ -78,6 +79,7 @@ func extractPluginSpecifierAndVersionFromArg(arg string) (string, *semver.Versio var version *semver.Version if versionValue, ok := groupMap["version"]; ok && versionValue != latest && versionValue != "" { + versionValue := strings.TrimPrefix(versionValue, "v") semverVersion, err := semver.NewVersion(versionValue) if err != nil { return "", nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, versionValue) diff --git a/internal/cli/plugin/update_test.go b/internal/cli/plugin/update_test.go index 623d2d6625..3dc6311b6d 100644 --- a/internal/cli/plugin/update_test.go +++ b/internal/cli/plugin/update_test.go @@ -23,84 +23,100 @@ import ( ) func Test_extractPluginSpecifierAndVersionFromArg(t *testing.T) { - var expectedVersion, _ = semver.NewVersion("1.0.0") + var v1_0_0, _ = semver.NewVersion("1.0.0") + //nolint:revive,stylecheck + var v1_0_0_PRE, _ = semver.NewVersion("1.0.0-prerelease") + //nolint:revive,stylecheck + var v1_0_0_BETA_AND_META, _ = semver.NewVersion("1.0.0-beta+very-meta") tests := []struct { arg string expectedPluginSpecifier string - expectVersion bool + expectedVersion *semver.Version expectError bool }{ { arg: "mongodb/atlas-cli-plugin-example", expectedPluginSpecifier: "mongodb/atlas-cli-plugin-example", - expectVersion: false, + expectedVersion: nil, expectError: false, }, { arg: "atlas-cli-plugin-example@1.0.0", expectedPluginSpecifier: "atlas-cli-plugin-example", - expectVersion: true, + expectedVersion: v1_0_0, + expectError: false, + }, + { + arg: "atlas-cli-plugin-example@1.0.0-prerelease", + expectedPluginSpecifier: "atlas-cli-plugin-example", + expectedVersion: v1_0_0_PRE, + expectError: false, + }, + { + arg: "atlas-cli-plugin-example@1.0.0-beta+very-meta", + expectedPluginSpecifier: "atlas-cli-plugin-example", + expectedVersion: v1_0_0_BETA_AND_META, expectError: false, }, { arg: "atlas-cli-plugin-example@", expectedPluginSpecifier: "", - expectVersion: false, + expectedVersion: nil, expectError: true, }, { arg: "mongodb/atlas-cli-plugin-example/", expectedPluginSpecifier: "mongodb/atlas-cli-plugin-example/", - expectVersion: false, + expectedVersion: nil, expectError: false, }, { arg: "mongodb/atlas-cli-plugin-example/@v1", expectedPluginSpecifier: "mongodb/atlas-cli-plugin-example/", - expectVersion: true, + expectedVersion: v1_0_0, expectError: false, }, { arg: "https://github.com/mongodb/atlas-cli-plugin-example", expectedPluginSpecifier: "https://github.com/mongodb/atlas-cli-plugin-example", - expectVersion: false, + expectedVersion: nil, expectError: false, }, { arg: "https://github.com/mongodb/atlas-cli-plugin-example@v1.0", expectedPluginSpecifier: "https://github.com/mongodb/atlas-cli-plugin-example", - expectVersion: false, + expectedVersion: v1_0_0, expectError: false, }, { arg: "github.com/mongodb/atlas-cli-plugin-example/", expectedPluginSpecifier: "github.com/mongodb/atlas-cli-plugin-example/", - expectVersion: false, + expectedVersion: nil, expectError: false, }, { arg: "github.com/mongodb/atlas-cli-plugin-example/@v1.0.0", expectedPluginSpecifier: "github.com/mongodb/atlas-cli-plugin-example/", - expectVersion: true, + expectedVersion: v1_0_0, expectError: false, }, { arg: "/mongodb/atlas-cli-plugin-example/", expectedPluginSpecifier: "/mongodb/atlas-cli-plugin-example/", - expectVersion: false, + expectedVersion: nil, expectError: false, }, { arg: "mongodb@atlas-cli-plugin-example", expectedPluginSpecifier: "", - expectVersion: false, + expectedVersion: nil, expectError: true, }, { arg: "mongodb@atlas-cli-plugin-example@1.0", expectedPluginSpecifier: "", - expectVersion: false, + expectedVersion: nil, expectError: true, }, } @@ -117,8 +133,12 @@ func Test_extractPluginSpecifierAndVersionFromArg(t *testing.T) { t.Errorf("expected plugin specifier: %s, got: %s", tt.expectedPluginSpecifier, pluginSpecifier) } - if tt.expectVersion && !expectedVersion.Equal(version) { - t.Errorf("expected version: %s, got: %s", expectedVersion.String(), version.String()) + if tt.expectedVersion != nil && !tt.expectedVersion.Equal(version) { + t.Errorf("expected version: %s, got: %s", tt.expectedVersion.String(), version.String()) + } + + if tt.expectedVersion == nil && version != nil { + t.Errorf("expected version to be nil, got: %s", version.String()) } }) }