Skip to content

Commit 850e699

Browse files
jeroenvervaekeGustavo Bazan
andauthored
CLOUDP-294346: [AtlasCLI] Support pre-releases format in CLI plugin versions (#3571)
Co-authored-by: Gustavo Bazan <[email protected]>
1 parent 5dd3d1f commit 850e699

File tree

4 files changed

+224
-65
lines changed

4 files changed

+224
-65
lines changed

internal/cli/plugin/plugin_github_asset.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,21 @@ func (g *GithubAsset) getReleaseAssets() ([]*github.ReleaseAsset, error) {
7979

8080
// download latest release if version is not specified
8181
if g.version == nil {
82-
release, _, err = g.ghClient.Repositories.GetLatestRelease(context.Background(), g.owner, g.name)
82+
// download the 100 latest releases
83+
const MaxPerPage = 100
84+
releases, _, err := g.ghClient.Repositories.ListReleases(context.Background(), g.owner, g.name, &github.ListOptions{
85+
Page: 0,
86+
PerPage: MaxPerPage,
87+
})
8388

8489
if err != nil {
85-
return nil, fmt.Errorf("could not find latest release for %s", g.repository())
90+
return nil, fmt.Errorf("could not fetch releases for %s %w", g.repository(), err)
91+
}
92+
93+
// get the latest release that doesn't have prerelease info or metadata in the version tag
94+
release = getLatestStableRelease(releases)
95+
if release == nil {
96+
return nil, fmt.Errorf("could not find latest stable release for %s", g.repository())
8697
}
8798
} else {
8899
// 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) {
100111
return release.Assets, nil
101112
}
102113

114+
func getLatestStableRelease(releases []*github.RepositoryRelease) *github.RepositoryRelease {
115+
var latestStableVersion *semver.Version
116+
var latestStableRelease *github.RepositoryRelease
117+
118+
for _, release := range releases {
119+
version, err := semver.NewVersion(*release.TagName)
120+
121+
// if we can't parse the version tag, skip this release
122+
if err != nil {
123+
continue
124+
}
125+
126+
// if the version has pre-release info or metadata, skip this version
127+
if version.Prerelease() != "" || version.Metadata() != "" {
128+
continue
129+
}
130+
131+
if latestStableVersion == nil || version.GreaterThan(latestStableVersion) {
132+
latestStableVersion = version
133+
latestStableRelease = release
134+
}
135+
}
136+
137+
return latestStableRelease
138+
}
139+
103140
var architectureAliases = map[string][]string{
104141
"amd64": {"x86_64"},
105142
"arm64": {"aarch64"},
@@ -173,7 +210,7 @@ func (g *GithubAsset) getPluginAssetAsReadCloser(assetID int64) (io.ReadCloser,
173210
}
174211

175212
func parseGithubReleaseValues(arg string) (*GithubAsset, error) {
176-
regexPattern := `^((https?://(www\.)?)?github\.com/)?(?P<owner>[\w.\-]+)/(?P<name>[\w.\-]+)/?(@(?P<version>v?(\d+)(\.\d+)?(\.\d+)?|latest))?$`
213+
regexPattern := `^((https?://(www\.)?)?github\.com/)?(?P<owner>[\w.\-]+)/(?P<name>[\w.\-]+)/?(@(?P<version>.+))?$`
177214
regex, err := regexp.Compile(regexPattern)
178215
if err != nil {
179216
return nil, fmt.Errorf("error compiling regex: %w", err)
@@ -196,6 +233,7 @@ func parseGithubReleaseValues(arg string) (*GithubAsset, error) {
196233
githubRelease := &GithubAsset{owner: groupMap["owner"], name: groupMap["name"]}
197234

198235
if version, ok := groupMap["version"]; ok && version != latest && version != "" {
236+
version := strings.TrimPrefix(version, "v")
199237
semverVersion, err := semver.NewVersion(version)
200238
if err != nil {
201239
return nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, version)

internal/cli/plugin/plugin_github_asset_test.go

Lines changed: 144 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/Masterminds/semver/v3"
2424
"github.com/google/go-github/v61/github"
2525
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/plugin"
26+
"github.com/mongodb/mongodb-atlas-cli/atlascli/internal/pointer"
2627
"github.com/stretchr/testify/assert"
2728
"github.com/stretchr/testify/require"
2829
)
@@ -204,77 +205,96 @@ func Test_parseGithubRepoValues(t *testing.T) {
204205
expectedOwner = "mongodb"
205206
expectedName = "atlas-cli-plugin-example"
206207
)
207-
var expectedVersion, _ = semver.NewVersion("1.0.0")
208+
var v1_0_0, _ = semver.NewVersion("1.0.0")
209+
//nolint:revive,stylecheck
210+
var v1_0_0_PRE, _ = semver.NewVersion("1.0.0-prerelease")
211+
//nolint:revive,stylecheck
212+
var v1_0_0_BETA_AND_META, _ = semver.NewVersion("1.0.0-beta+very-meta")
208213

209214
tests := []struct {
210-
arg string
211-
expectVersion bool
212-
expectError bool
215+
arg string
216+
expectedVersion *semver.Version
217+
expectError bool
213218
}{
214219
{
215-
arg: "mongodb/atlas-cli-plugin-example",
216-
expectVersion: false,
217-
expectError: false,
220+
arg: "mongodb/atlas-cli-plugin-example",
221+
expectedVersion: nil,
222+
expectError: false,
223+
},
224+
{
225+
arg: "mongodb/[email protected]",
226+
expectedVersion: v1_0_0,
227+
expectError: false,
228+
},
229+
{
230+
arg: "mongodb/[email protected]",
231+
expectedVersion: v1_0_0,
232+
expectError: false,
233+
},
234+
{
235+
arg: "mongodb/[email protected]",
236+
expectedVersion: v1_0_0_PRE,
237+
expectError: false,
218238
},
219239
{
220-
arg: "mongodb/[email protected]",
221-
expectVersion: true,
222-
expectError: false,
240+
arg: "mongodb/[email protected]-beta+very-meta",
241+
expectedVersion: v1_0_0_BETA_AND_META,
242+
expectError: false,
223243
},
224244
{
225-
arg: "mongodb/atlas-cli-plugin-example@",
226-
expectVersion: false,
227-
expectError: true,
245+
arg: "mongodb/atlas-cli-plugin-example@",
246+
expectedVersion: nil,
247+
expectError: true,
228248
},
229249
{
230-
arg: "mongodb/atlas-cli-plugin-example/",
231-
expectVersion: false,
232-
expectError: false,
250+
arg: "mongodb/atlas-cli-plugin-example/",
251+
expectedVersion: nil,
252+
expectError: false,
233253
},
234254
{
235-
arg: "mongodb/atlas-cli-plugin-example/@v1",
236-
expectVersion: true,
237-
expectError: false,
255+
arg: "mongodb/atlas-cli-plugin-example/@v1",
256+
expectedVersion: v1_0_0,
257+
expectError: false,
238258
},
239259
{
240-
arg: "https://github.com/mongodb/atlas-cli-plugin-example",
241-
expectVersion: false,
242-
expectError: false,
260+
arg: "https://github.com/mongodb/atlas-cli-plugin-example",
261+
expectedVersion: nil,
262+
expectError: false,
243263
},
244264
{
245-
arg: "https://github.com/mongodb/[email protected]",
246-
expectVersion: false,
247-
expectError: false,
265+
arg: "https://github.com/mongodb/[email protected]",
266+
expectedVersion: v1_0_0,
267+
expectError: false,
248268
},
249269
{
250-
arg: "github.com/mongodb/atlas-cli-plugin-example/",
251-
expectVersion: false,
252-
expectError: false,
270+
arg: "github.com/mongodb/atlas-cli-plugin-example/",
271+
expectedVersion: nil,
272+
expectError: false,
253273
},
254274
{
255-
arg: "github.com/mongodb/atlas-cli-plugin-example/@v1.0.0",
256-
expectVersion: true,
257-
expectError: false,
275+
arg: "github.com/mongodb/atlas-cli-plugin-example/@v1.0.0",
276+
expectedVersion: v1_0_0,
277+
expectError: false,
258278
},
259279
{
260-
arg: "/mongodb/atlas-cli-plugin-example/",
261-
expectVersion: false,
262-
expectError: true,
280+
arg: "/mongodb/atlas-cli-plugin-example/",
281+
expectedVersion: nil,
282+
expectError: true,
263283
},
264284
{
265-
arg: "mongodb@atlas-cli-plugin-example",
266-
expectVersion: false,
267-
expectError: true,
285+
arg: "mongodb@atlas-cli-plugin-example",
286+
expectedVersion: nil,
287+
expectError: true,
268288
},
269289
{
270-
arg: "mongodb@[email protected]",
271-
expectVersion: false,
272-
expectError: true,
290+
arg: "mongodb@[email protected]",
291+
expectedVersion: nil,
292+
expectError: true,
273293
},
274294
{
275-
arg: "invalidArgString",
276-
expectVersion: false,
277-
expectError: true,
295+
arg: "invalidArgString",
296+
expectedVersion: nil,
297+
expectError: true,
278298
},
279299
}
280300

@@ -291,8 +311,12 @@ func Test_parseGithubRepoValues(t *testing.T) {
291311
if githubRelease.name != expectedName {
292312
t.Errorf("expected name: %s, got: %s", expectedName, githubRelease.owner)
293313
}
294-
if tt.expectVersion && !expectedVersion.Equal(githubRelease.version) {
295-
t.Errorf("expected version: %s, got: %s", expectedVersion.String(), githubRelease.version.String())
314+
if tt.expectedVersion != nil && !tt.expectedVersion.Equal(githubRelease.version) {
315+
t.Errorf("expected version: %s, got: %s", tt.expectedVersion.String(), githubRelease.version.String())
316+
}
317+
318+
if tt.expectedVersion == nil && githubRelease.version != nil {
319+
t.Errorf("expected version to be nil, got: %s", githubRelease.version.String())
296320
}
297321
}
298322
})
@@ -352,3 +376,78 @@ func Test_getPluginDirectoryName(t *testing.T) {
352376
githubAsset := &GithubAsset{owner: "owner", name: "name"}
353377
require.Equal(t, "owner@name", githubAsset.getPluginDirectoryName())
354378
}
379+
380+
func Test_getLatestStableRelease(t *testing.T) {
381+
tests := []struct {
382+
name string
383+
releases []*github.RepositoryRelease
384+
expected *github.RepositoryRelease
385+
}{
386+
{
387+
name: "Single valid value",
388+
releases: []*github.RepositoryRelease{
389+
{
390+
TagName: pointer.Get("v1.0.0"),
391+
},
392+
},
393+
expected: &github.RepositoryRelease{
394+
TagName: pointer.Get("v1.0.0"),
395+
},
396+
},
397+
{
398+
name: "Single invalid value",
399+
releases: []*github.RepositoryRelease{
400+
{
401+
TagName: pointer.Get("test"),
402+
},
403+
},
404+
expected: nil,
405+
},
406+
{
407+
name: "Single valid pre-release value",
408+
releases: []*github.RepositoryRelease{
409+
{
410+
TagName: pointer.Get("v1.0.0-pre"),
411+
},
412+
},
413+
expected: nil,
414+
},
415+
{
416+
name: "Multiple",
417+
releases: []*github.RepositoryRelease{
418+
{
419+
TagName: pointer.Get("v2.0.0-pre"),
420+
},
421+
{
422+
TagName: pointer.Get("v2.0.0-beta"),
423+
},
424+
{
425+
TagName: pointer.Get("v1.2.1"),
426+
},
427+
{
428+
TagName: pointer.Get("v1.2.0"),
429+
},
430+
{
431+
TagName: pointer.Get("v1.1.0"),
432+
},
433+
{
434+
TagName: pointer.Get("v1.0.1"),
435+
},
436+
{
437+
TagName: pointer.Get("v1.0.0"),
438+
},
439+
},
440+
expected: &github.RepositoryRelease{
441+
TagName: pointer.Get("v1.2.1"),
442+
},
443+
},
444+
}
445+
446+
for _, tt := range tests {
447+
t.Run(tt.name, func(t *testing.T) {
448+
actual := getLatestStableRelease(tt.releases)
449+
450+
assert.Equal(t, tt.expected, actual)
451+
})
452+
}
453+
}

internal/cli/plugin/update.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"os"
2222
"path"
2323
"regexp"
24+
"strings"
2425

2526
"github.com/Masterminds/semver/v3"
2627
"github.com/google/go-github/v61/github"
@@ -55,7 +56,7 @@ func printPluginUpdateWarning(p *plugin.Plugin, err error) {
5556

5657
// extract plugin specifier and version given the input argument of the update command.
5758
func extractPluginSpecifierAndVersionFromArg(arg string) (string, *semver.Version, error) {
58-
regexPattern := `^(?P<pluginValue>[^\s@]+)(@(?P<version>v?(\d+)(\.\d+)?(\.\d+)?|latest))?$`
59+
regexPattern := `^(?P<pluginValue>[^\s@]+)(@(?P<version>.+))?$`
5960
regex, err := regexp.Compile(regexPattern)
6061
if err != nil {
6162
return "", nil, fmt.Errorf("error compiling regex: %w", err)
@@ -78,6 +79,7 @@ func extractPluginSpecifierAndVersionFromArg(arg string) (string, *semver.Versio
7879
var version *semver.Version
7980

8081
if versionValue, ok := groupMap["version"]; ok && versionValue != latest && versionValue != "" {
82+
versionValue := strings.TrimPrefix(versionValue, "v")
8183
semverVersion, err := semver.NewVersion(versionValue)
8284
if err != nil {
8385
return "", nil, fmt.Errorf(`the specified version "%s" is invalid, it needs to follow the rules of Semantic Versioning`, versionValue)

0 commit comments

Comments
 (0)