Repository: bazelbuild/bazelisk
Pull request #278 (2021-10-29 18:08:10)

Support floating release identifiers (e.g. "5.x")
This version format allows users to work with the most recent stable release from a particular LTS track.

Fixes https://github.com/bazelbuild/bazelisk/issues/277


Patch:
diff --git a/README.md b/README.md
index f33a47c..b1ff4b0 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ Bazelisk currently understands the following formats for version labels:
   Previous releases can be specified via `latest-1`, `latest-2` etc.
 - A version number like `0.17.2` means that exact version of Bazel.
   It can also be a release candidate version like `0.20.0rc3`, or a rolling release version like `5.0.0-pre.20210317.1`.
+- A floating version identifier like `4.x` that returns the latest release from the LTS series started by Bazel 4.0.0.
 - The hash of a Git commit. Please note that Bazel binaries are only available for commits that passed [Bazel CI](https://buildkite.com/bazel/bazel-bazel).
 
 Additionally, a few special version names are supported for our official releases only (these formats do not work when using a fork):
diff --git a/core/repositories.go b/core/repositories.go
index f179d1c..8a2919c 100644
--- a/core/repositories.go
+++ b/core/repositories.go
@@ -3,6 +3,7 @@ package core
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/platforms"
@@ -116,7 +117,13 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info) (stri
 
 func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info) (string, DownloadFunc, error) {
 	lister := func(bazeliskHome string) ([]string, error) {
-		return r.Releases.GetReleaseVersions(bazeliskHome, vi.LatestOffset+1)
+		var lastN int
+		// Optimization: only fetch last (x+1) releases if the version is "latest-x".
+		// This does not work if the version is "4.x", i.e. if TrackRestriction is set.
+		if vi.TrackRestriction == 0 {
+			lastN = vi.LatestOffset + 1
+		}
+		return r.Releases.GetReleaseVersions(bazeliskHome, lastN)
 	}
 	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi)
 	if err != nil {
@@ -179,6 +186,11 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF
 	if err != nil {
 		return "", fmt.Errorf("unable to determine latest version: %v", err)
 	}
+
+	if vi.TrackRestriction > 0 {
+		available = restrictToTrack(available, vi.TrackRestriction)
+	}
+
 	index := len(available) - 1 - vi.LatestOffset
 	if index < 0 {
 		return "", fmt.Errorf("cannot resolve version \"%s\": There are only %d Bazel versions", vi.Value, len(available))
@@ -187,6 +199,17 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF
 	return sorted[index], nil
 }
 
+func restrictToTrack(versions []string, track int) []string {
+	filtered := make([]string, 0)
+	prefix := fmt.Sprintf("%d.", track)
+	for _, v := range versions {
+		if strings.HasPrefix(v, prefix) {
+			filtered = append(filtered, v)
+		}
+	}
+	return filtered
+}
+
 // DownloadFromBaseURL can download Bazel binaries from a specific URL while ignoring the predefined repositories.
 func (r *Repositories) DownloadFromBaseURL(baseURL, version, destDir, destFile string) (string, error) {
 	if !r.supportsBaseURL {
diff --git a/versions/versions.go b/versions/versions.go
index 4ef6245..8ea3a9b 100644
--- a/versions/versions.go
+++ b/versions/versions.go
@@ -17,7 +17,7 @@ const (
 )
 
 var (
-	releasePattern       = regexp.MustCompile(`^(\d+\.\d+\.\d+)$`)
+	releasePattern       = regexp.MustCompile(`^(\d+)\.(x|\d+\.\d+)$`)
 	candidatePattern     = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`)
 	rollingPattern       = regexp.MustCompile(`^\d+\.0\.0-pre\.\d{8}(\.\d+){1,2}$`)
 	latestReleasePattern = regexp.MustCompile(`^latest(?:-(?P<offset>\d+))?$`)
@@ -27,16 +27,24 @@ var (
 // Info represents a structured Bazel version identifier.
 type Info struct {
 	IsRelease, IsCandidate, IsCommit, IsFork, IsRolling, IsRelative, IsDownstream bool
-	Fork, Value                                                        string
-	LatestOffset                                                       int
+	Fork, Value                                                                   string
+	LatestOffset, TrackRestriction                                                int
 }
 
 // Parse extracts and returns structured information about the given Bazel version label.
 func Parse(fork, version string) (*Info, error) {
 	vi := &Info{Fork: fork, Value: version, IsFork: isFork(fork)}
 
-	if releasePattern.MatchString(version) {
+	if m := releasePattern.FindStringSubmatch(version); m != nil {
 		vi.IsRelease = true
+		if m[2] == "x" {
+			track, err := strconv.Atoi(m[1])
+			if err != nil {
+				return nil, fmt.Errorf("invalid version %q, expected something like '5.2.1' or '5.x'", version)
+			}
+			vi.IsRelative = true
+			vi.TrackRestriction = track
+		}
 	} else if m := latestReleasePattern.FindStringSubmatch(version); m != nil {
 		vi.IsRelease = true
 		vi.IsRelative = true


Test patch:
diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go
index 0de0f62..1a3e06a 100644
--- a/bazelisk_version_test.go
+++ b/bazelisk_version_test.go
@@ -227,6 +227,29 @@ func TestResolveLatestRollingRelease(t *testing.T) {
 	}
 }
 
+func TestAcceptFloatingReleaseVersions(t *testing.T) {
+	s := setUp(t)
+	s.AddVersion("3.0.0", true, nil, []string{"4.0.0-pre.20210504.1"})
+	s.AddVersion("4.0.0", true, nil, nil)
+	s.AddVersion("4.1.0", true, nil, nil)
+	s.AddVersion("4.2.0", true, nil, nil)
+	s.AddVersion("4.2.1", true, []int{1, 2}, nil)
+	s.AddVersion("5.0.0", true, nil, nil)
+	s.Finish()
+
+	gcs := &repositories.GCSRepo{}
+	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
+	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.x")
+
+	if err != nil {
+		t.Fatalf("Version resolution failed unexpectedly: %v", err)
+	}
+	expectedVersion := "4.2.1"
+	if version != expectedVersion {
+		t.Fatalf("Expected version %s, but got %s", expectedVersion, version)
+	}
+}
+
 type gcsSetup struct {
 	baseURL         string
 	versionPrefixes []string