Repository: bazelbuild/bazelisk
Pull request #494 (2023-08-25 00:58:54)

Teach bazelisk.py about .bazeliskrc
This allows for suggesting (but not requiring) a bazel version using the bazeliskrc dotfile.

jwnimmer-tri: Hi team. Is there anything I can do to help move this forward?  I just fixed the merge conflict errors with a rebase.
jwnimmer-tri: Hi team. Is there anything I can do to help move this forward?
jwnimmer-tri: Hi team. Is there anything I can do to help move this forward?  All tests are passing.
albertocavalcante: @jwnimmer-tri just wondering - is this still relevant? (I didn't take a look yet)
jwnimmer-tri: Yes, I believe so.

I just rebased and re-tested locally and all tests passed.

Patch:
diff --git a/.bazelci/config.yml b/.bazelci/config.yml
index 072811d..dbb8221 100644
--- a/.bazelci/config.yml
+++ b/.bazelci/config.yml
@@ -1,6 +1,6 @@
 ---
 platforms:
-  ubuntu1804:
+  ubuntu2204:
     build_targets:
       - //...
     test_targets:
@@ -16,11 +16,14 @@ platforms:
     test_flags:
       - --flaky_test_attempts=1
       - --test_output=streamed
-  macos:
+  macos_arm64:
     build_targets:
       - //...
     test_targets:
       - //...
+      # The python version doesn't support macos arm64
+      - "-//:py_bazelisk_test"
+      - "-//:py3_bazelisk_test"
     test_flags:
       - --flaky_test_attempts=1
       - --test_output=streamed
diff --git a/BUILD b/BUILD
index 7c66ddf..2e74f36 100644
--- a/BUILD
+++ b/BUILD
@@ -147,4 +147,5 @@ npm_package(
         ":package",
     ],
     package = "@bazel/bazelisk",
+    publishable = True,
 )
diff --git a/MODULE.bazel b/MODULE.bazel
index bfc2480..841ac99 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -9,7 +9,7 @@ bazel_dep(name = "gazelle", version = "0.38.0")
 bazel_dep(name = "platforms", version = "0.0.10")
 bazel_dep(name = "rules_go", version = "0.50.0")
 bazel_dep(name = "rules_pkg", version = "0.10.1")
-bazel_dep(name = "aspect_rules_js", version = "1.42.3")
+bazel_dep(name = "aspect_rules_js", version = "2.0.1")
 
 go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
 go_sdk.download(version = "1.23.0")
diff --git a/README.md b/README.md
index 8eba404..1c44944 100644
--- a/README.md
+++ b/README.md
@@ -60,17 +60,19 @@ 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.
+- A floating version identifier like `4.x` that returns the latest **release** from the LTS series started by Bazel 4.0.0.
+- A wildcard version identifier like `4.*` that returns the latest **release or candidate** 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):
 - `last_green` refers to the Bazel binary that was built at the most recent commit that passed [Bazel CI](https://buildkite.com/bazel/bazel-bazel).
   Ideally this binary should be very close to Bazel-at-head.
-- `last_downstream_green` points to the most recent Bazel binary that builds and tests all [downstream projects](https://buildkite.com/bazel/bazel-at-head-plus-downstream) successfully.
 - `last_rc` points to the most recent release candidate.
   If there is no active release candidate, Bazelisk uses the latest Bazel release instead.
 - `rolling` refers to the latest rolling release (even if there is a newer LTS release).
 
+Note: `last_downstream_green` support has been removed, please use `last_green` instead.
+
 ## Where does Bazelisk get Bazel from?
 
 By default Bazelisk retrieves Bazel releases, release candidates and binaries built at green commits from Google Cloud Storage. The downloaded artifacts are validated against the SHA256 value recorded in `BAZELISK_VERIFY_SHA256` if this variable is set in the configuration file.
@@ -92,6 +94,14 @@ If for any reason none of this works, you can also override the URL format altog
 - `%%`: Literal `%` for escaping purposes.
 - All other characters after `%` are reserved for future use and result in a processing error.
 
+## Environment variables set by Bazelisk
+
+Bazelisk prepends a directory to `PATH` that contains the downloaded Bazel binary.
+This ensures that Bazel targets that invoke `bazel` will use the same Bazel binary as the outer invocation.
+
+Bazelisk also sets the environment variable `BAZELISK` to its own path.
+This can be useful for scripts that want to know if they are running under Bazelisk and can also be used to run specific Bazel versions from within a Bazel run, e.g. to generate version-specific test data.
+
 ## Ensuring that your developers use Bazelisk rather than Bazel
 
 Bazel installers typically provide Bazel's [shell wrapper script] as the `bazel` on the PATH.
@@ -130,10 +140,11 @@ This will show you which flags can safely enabled, and which flags require a mig
 ### --bisect
 
 `--bisect` flag allows you to bisect Bazel versions to find which version introduced a build failure. You can specify the range of versions to bisect with `--bisect=<GOOD>..<BAD>`, where GOOD is the last known working Bazel version and BAD is the first known non-working Bazel version. Bazelisk uses [GitHub's compare API](https://docs.github.com/en/rest/commits/commits#compare-two-commits) to get the list of commits to bisect. When GOOD is not an ancestor of BAD, GOOD is reset to their merge base commit.
+The meaning of GOOD and BAD can be reversed by prefixing the range with `~`, e.g. `--bisect=~6.0.0..HEAD` will find the first version 6.0.0 and HEAD that *fixes* the build.
 
 Examples:
 ```shell
-# Bisect between 6.0.0 and Bazel at HEAD
+# Bisect between 6.0.0 and Bazel at HEAD to find the first commit that breaks the build.
 bazelisk --bisect=6.0.0..HEAD test //foo:bar_test
 
 # Bisect between 6.1.0 and the second release candidate of Bazel 6.2.0
@@ -141,6 +152,9 @@ bazelisk --bisect=6.1.0..release-6.2.0rc2 test //foo:bar_test
 
 # Bisect between two commits on the main branch (or branches with `release-` prefix) of the Bazel GitHub repository.
 bazelisk --bisect=<good commit hash>..<bad commit hash> test //foo:bar_test
+
+# Bisect between 6.0.0 and Bazel at HEAD to find the first commit that *fixes* the build.
+bazelisk --bisect=~6.0.0..HEAD test //foo:bar_test
 ```
 
 Note that, Bazelisk uses prebuilt Bazel binaries at commits on the main and release branches, therefore you cannot bisect your local commits.
@@ -166,7 +180,7 @@ You can control the user agent that Bazelisk sends in all HTTP requests by setti
 
 # .bazeliskrc configuration file
 
-The Go version supports a `.bazeliskrc` file in the root directory of a workspace and the user home directory. This file allows users to set environment variables persistently.
+A `.bazeliskrc` file in the root directory of a workspace or the user home directory allows users to set environment variables persistently. (The Python implementation of Bazelisk doesn't check the user home directory yet, only the workspace directory.)
 
 Example file content:
 
@@ -207,7 +221,7 @@ Additionally, the Bazelisk home directory is also evaluated in precedence order.
 
 For ease of use, the Python version of Bazelisk is written to work with Python 2.7 and 3.x and only uses modules provided by the standard library.
 
-The Go version can be compiled to run natively on Linux, macOS and Windows. 
+The Go version can be compiled to run natively on Linux, macOS and Windows.
 
 To install it, run:
 
@@ -227,8 +241,6 @@ For more information, you may read about the [`GOPATH` environment variable](htt
 
 - Add support for checked-in Bazel binaries.
 - When the version label is set to a commit hash, first download a matching binary version of Bazel, then build Bazel automatically at that commit and use the resulting binary.
-- Add support to automatically bisect a build failure to a culprit commit in Bazel.
-  If you notice that you could successfully build your project using version X, but not using version X+1, then Bazelisk should be able to figure out the commit that caused the breakage and the Bazel team can easily fix the problem.
 
 ## FAQ
 
diff --git a/bazelisk.go b/bazelisk.go
index a25041c..45da21d 100644
--- a/bazelisk.go
+++ b/bazelisk.go
@@ -27,8 +27,8 @@ func main() {
 	gcs := &repositories.GCSRepo{}
 	config := core.MakeDefaultConfig()
 	gitHub := repositories.CreateGitHubRepo(config.Get("BAZELISK_GITHUB_TOKEN"))
-	// Fetch LTS releases, release candidates, rolling releases and Bazel-at-commits from GCS, forks from GitHub.
-	repos := core.CreateRepositories(gcs, gcs, gitHub, gcs, gcs, true)
+	// Fetch LTS releases & candidates, rolling releases and Bazel-at-commits from GCS, forks from GitHub.
+	repos := core.CreateRepositories(gcs, gitHub, gcs, gcs, true)
 
 	exitCode, err := core.RunBazeliskWithArgsFuncAndConfig(func(string) []string { return os.Args[1:] }, repos, config)
 	if err != nil {
diff --git a/bazelisk.py b/bazelisk.py
index 5cdd754..01a4c57 100755
--- a/bazelisk.py
+++ b/bazelisk.py
@@ -46,15 +46,10 @@ ONE_HOUR = 1 * 60 * 60
 
 LATEST_PATTERN = re.compile(r"latest(-(?P<offset>\d+))?$")
 
-LAST_GREEN_COMMIT_BASE_PATH = (
-    "https://storage.googleapis.com/bazel-untrusted-builds/last_green_commit/"
+LAST_GREEN_COMMIT_PATH = (
+    "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries"
 )
 
-LAST_GREEN_COMMIT_PATH_SUFFIXES = {
-    "last_green": "github.com/bazelbuild/bazel.git/bazel-bazel",
-    "last_downstream_green": "downstream_pipeline",
-}
-
 BAZEL_GCS_PATH_PATTERN = (
     "https://storage.googleapis.com/bazel-builds/artifacts/{platform}/{commit}/bazel"
 )
@@ -67,6 +62,50 @@ BAZEL_REAL = "BAZEL_REAL"
 
 BAZEL_UPSTREAM = "bazelbuild"
 
+_dotfiles_dict = None
+
+
+def get_dotfiles_dict():
+    """Loads all supported dotfiles and returns a unified name=value dictionary
+    for their config settings. The dictionary is only loaded on the first call
+    to this function; subsequent calls used a cached result, so won't change.
+    """
+    global _dotfiles_dict
+    if _dotfiles_dict is not None:
+        return _dotfiles_dict
+    _dotfiles_dict = dict()
+    env_files = []
+    # Here we're only checking the workspace root. Ideally, we would also check
+    # the user's home directory to match the Go version. When making that edit,
+    # be sure to obey the correctly prioritization of workspace / home rcfiles.
+    root = find_workspace_root()
+    if root:
+        env_files.append(os.path.join(root, ".bazeliskrc"))
+    for env_file in env_files:
+        try:
+            with open(env_file, "r") as f:
+                rcdata = f.read()
+        except Exception:
+            continue
+        for line in rcdata.splitlines():
+            line = line.split("#", 1)[0].strip()
+            if not line:
+                continue
+            some_name, some_value = line.split("=", 1)
+            _dotfiles_dict[some_name] = some_value
+    return _dotfiles_dict
+
+
+def get_env_or_config(name, default=None):
+    """Reads a configuration value from the environment, but falls back to
+    reading it from .bazeliskrc in the workspace root."""
+    if name in os.environ:
+        return os.environ[name]
+    dotfiles = get_dotfiles_dict()
+    if name in dotfiles:
+        return dotfiles[name]
+    return default
+
 
 def decide_which_bazel_version_to_use():
     # Check in this order:
@@ -79,8 +118,9 @@ def decide_which_bazel_version_to_use():
     # - workspace_root/.bazelversion exists -> read contents, that version.
     # - workspace_root/WORKSPACE contains a version -> that version. (TODO)
     # - fallback: latest release
-    if "USE_BAZEL_VERSION" in os.environ:
-        return os.environ["USE_BAZEL_VERSION"]
+    use_bazel_version = get_env_or_config("USE_BAZEL_VERSION")
+    if use_bazel_version is not None:
+        return use_bazel_version
 
     workspace_root = find_workspace_root()
     if workspace_root:
@@ -119,9 +159,8 @@ def resolve_version_label_to_number_or_commit(bazelisk_directory, version):
             of an unreleased Bazel binary,
         2. An indicator for whether the returned version refers to a commit.
     """
-    suffix = LAST_GREEN_COMMIT_PATH_SUFFIXES.get(version)
-    if suffix:
-        return get_last_green_commit(suffix), True
+    if version == "last_green":
+        return get_last_green_commit(), True
 
     if "latest" in version:
         match = LATEST_PATTERN.match(version)
@@ -140,8 +179,11 @@ def resolve_version_label_to_number_or_commit(bazelisk_directory, version):
     return version, False
 
 
-def get_last_green_commit(path_suffix):
-    return read_remote_text_file(LAST_GREEN_COMMIT_BASE_PATH + path_suffix).strip()
+def get_last_green_commit():
+    commit = read_remote_text_file(LAST_GREEN_COMMIT_PATH).strip()
+    if not re.match(r"^[0-9a-f]{40}$", commit):
+        raise Exception("Invalid commit hash: {}".format(commit))
+    return commit
 
 
 def get_releases_json(bazelisk_directory):
@@ -229,7 +271,7 @@ def determine_bazel_filename(version):
 
     filename_suffix = determine_executable_filename_suffix()
     bazel_flavor = "bazel"
-    if os.environ.get("BAZELISK_NOJDK", "0") != "0":
+    if get_env_or_config("BAZELISK_NOJDK", "0") != "0":
         bazel_flavor = "bazel_nojdk"
     return "{}-{}-{}-{}{}".format(bazel_flavor, version, operating_system, machine, filename_suffix)
 
@@ -280,8 +322,9 @@ def determine_url(version, is_commit, bazel_filename):
     # Example: '0.19.1' -> ('0.19.1', None), '0.20.0rc1' -> ('0.20.0', 'rc1')
     (version, rc) = re.match(r"(\d*\.\d*(?:\.\d*)?)(rc\d+)?", version).groups()
 
-    if "BAZELISK_BASE_URL" in os.environ:
-        return "{}/{}/{}".format(os.environ["BAZELISK_BASE_URL"], version, bazel_filename)
+    bazelisk_base_url = get_env_or_config("BAZELISK_BASE_URL")
+    if bazelisk_base_url is not None:
+        return "{}/{}/{}".format(bazelisk_base_url, version, bazel_filename)
     else:
         return "https://releases.bazel.build/{}/{}/{}".format(
             version, rc if rc else "release", bazel_filename
@@ -345,7 +388,7 @@ def download_bazel_into_directory(version, is_commit, directory):
 def download(url, destination_path):
     sys.stderr.write("Downloading {}...\n".format(url))
     request = Request(url)
-    if "BAZELISK_BASE_URL" in os.environ:
+    if get_env_or_config("BAZELISK_BASE_URL") is not None:
         parts = urlparse(url)
         creds = None
         try:
@@ -360,7 +403,7 @@ def download(url, destination_path):
 
 
 def get_bazelisk_directory():
-    bazelisk_home = os.environ.get("BAZELISK_HOME")
+    bazelisk_home = get_env_or_config("BAZELISK_HOME")
     if bazelisk_home is not None:
         return bazelisk_home
 
diff --git a/build.sh b/build.sh
index 2744f2e..7b45788 100755
--- a/build.sh
+++ b/build.sh
@@ -28,7 +28,9 @@ go build
     //:bazelisk-linux-amd64 \
     //:bazelisk-linux-arm64 \
     //:bazelisk-windows-amd64 \
-    //:bazelisk-windows-arm64
+    //:bazelisk-windows-arm64 \
+    //deb:bazelisk-amd64_deb \
+    //deb:bazelisk-arm64_deb
 echo
 
 cp bazel-out/*-opt*/bin/bazelisk-darwin_amd64 bin/bazelisk-darwin-amd64
@@ -38,6 +40,8 @@ cp bazel-out/*-opt*/bin/bazelisk-linux_amd64 bin/bazelisk-linux-amd64
 cp bazel-out/*-opt*/bin/bazelisk-linux_arm64 bin/bazelisk-linux-arm64
 cp bazel-out/*-opt*/bin/bazelisk-windows_amd64.exe bin/bazelisk-windows-amd64.exe
 cp bazel-out/*-opt*/bin/bazelisk-windows_arm64.exe bin/bazelisk-windows-arm64.exe
+cp bazel-out/*-opt*/bin/deb/bazelisk-amd64.deb bin/bazelisk-amd64.deb
+cp bazel-out/*-opt*/bin/deb/bazelisk-arm64.deb bin/bazelisk-arm64.deb
 rm -f bazelisk
 
 ### Build release artifacts using `go build`.
diff --git a/core/core.go b/core/core.go
index 7a8cf22..ee98bd8 100644
--- a/core/core.go
+++ b/core/core.go
@@ -33,6 +33,7 @@ import (
 const (
 	bazelReal               = "BAZEL_REAL"
 	skipWrapperEnv          = "BAZELISK_SKIP_WRAPPER"
+	bazeliskEnv             = "BAZELISK"
 	defaultWrapperDirectory = "./tools"
 	defaultWrapperName      = "bazel"
 	maxDirLength            = 255
@@ -165,14 +166,14 @@ func RunBazeliskWithArgsFuncAndConfigAndOut(argsFunc ArgsFunc, repos *Repositori
 	} else if len(args) > 0 && strings.HasPrefix(args[0], "--bisect") {
 		// When --bisect is present, we run the bisect logic.
 		if !strings.HasPrefix(args[0], "--bisect=") {
-			return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=<good bazel commit>..<bad bazel commit>'")
+			return -1, fmt.Errorf("Error: --bisect must have a value. Expected format: '--bisect=[~]<good bazel commit>..<bad bazel commit>'")
 		}
 		value := args[0][len("--bisect="):]
 		commits := strings.Split(value, "..")
 		if len(commits) == 2 {
 			bisect(commits[0], commits[1], args[1:], bazeliskHome, repos, config)
 		} else {
-			return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=<good bazel commit>..<bad bazel commit>'")
+			return -1, fmt.Errorf("Error: Invalid format for --bisect. Expected format: '--bisect=[~]<good bazel commit>..<bad bazel commit>'")
 		}
 	}
 
@@ -214,11 +215,11 @@ func getBazelCommand(args []string) (string, error) {
 
 // getBazeliskHome returns the path to the Bazelisk home directory.
 func getBazeliskHome(config config.Config) (string, error) {
-  bazeliskHome := config.Get("BAZELISK_HOME_" + strings.ToUpper(runtime.GOOS))
+	bazeliskHome := config.Get("BAZELISK_HOME_" + strings.ToUpper(runtime.GOOS))
 	if len(bazeliskHome) == 0 {
 		bazeliskHome = config.Get("BAZELISK_HOME")
 	}
-  
+
 	if len(bazeliskHome) == 0 {
 		userCacheDir, err := os.UserCacheDir()
 		if err != nil {
@@ -331,7 +332,7 @@ func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error
 	} else if len(versionInfo) == 2 {
 		bazelFork, bazelVersion = versionInfo[0], versionInfo[1]
 	} else {
-		return "", "", fmt.Errorf("invalid version \"%s\", could not parse version with more than one slash", bazelForkAndVersion)
+		return "", "", fmt.Errorf("invalid version %q, could not parse version with more than one slash", bazelForkAndVersion)
 	}
 
 	return bazelFork, bazelVersion, nil
@@ -403,10 +404,17 @@ func downloadBazelIfNecessary(version string, bazeliskHome string, bazelForkOrUR
 }
 
 func atomicWriteFile(path string, contents []byte, perm os.FileMode) error {
-	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+	parent := filepath.Dir(path)
+	if err := os.MkdirAll(parent, 0755); err != nil {
 		return fmt.Errorf("failed to MkdirAll parent of %s: %w", path, err)
 	}
-	tmpPath := path + ".tmp"
+	tmpFile, err := os.CreateTemp(parent, filepath.Base(path)+".tmp")
+	if err != nil {
+		return fmt.Errorf("failed to create temporary file in %s: %w", parent, err)
+	}
+	tmpFile.Close()
+	defer os.Remove(tmpFile.Name())
+	tmpPath := tmpFile.Name()
 	if err := os.WriteFile(tmpPath, contents, perm); err != nil {
 		return fmt.Errorf("failed to write file %s: %w", tmpPath, err)
 	}
@@ -457,12 +465,20 @@ func downloadBazelToCAS(version string, bazeliskHome string, repos *Repositories
 	f.Close()
 	actualSha256 := strings.ToLower(fmt.Sprintf("%x", h.Sum(nil)))
 
-	pathToBazelInCAS := filepath.Join(casDir, actualSha256, "bin", "bazel"+platforms.DetermineExecutableFilenameSuffix())
-	if err := os.MkdirAll(filepath.Dir(pathToBazelInCAS), 0755); err != nil {
+	bazelInCASBasename := "bazel" + platforms.DetermineExecutableFilenameSuffix()
+	pathToBazelInCAS := filepath.Join(casDir, actualSha256, "bin", bazelInCASBasename)
+	dirForBazelInCAS := filepath.Dir(pathToBazelInCAS)
+	if err := os.MkdirAll(dirForBazelInCAS, 0755); err != nil {
 		return "", "", fmt.Errorf("failed to MkdirAll parent of %s: %w", pathToBazelInCAS, err)
 	}
 
-	tmpPathInCorrectDirectory := pathToBazelInCAS + ".tmp"
+	tmpPathFile, err := os.CreateTemp(dirForBazelInCAS, bazelInCASBasename+".tmp")
+	if err != nil {
+		return "", "", fmt.Errorf("failed to create temporary file in %s: %w", dirForBazelInCAS, err)
+	}
+	tmpPathFile.Close()
+	defer os.Remove(tmpPathFile.Name())
+	tmpPathInCorrectDirectory := tmpPathFile.Name()
 	if err := os.Rename(tmpDestPath, tmpPathInCorrectDirectory); err != nil {
 		return "", "", fmt.Errorf("failed to move %s to %s: %w", tmpDestPath, tmpPathInCorrectDirectory, err)
 	}
@@ -581,6 +597,10 @@ func makeBazelCmd(bazel string, args []string, out io.Writer, config config.Conf
 	if execPath != bazel {
 		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", bazelReal, bazel))
 	}
+	selfPath, err := os.Executable()
+	if err != nil {
+		cmd.Env = append(cmd.Env, bazeliskEnv+"="+selfPath)
+	}
 	prependDirToPathList(cmd, filepath.Dir(execPath))
 	cmd.Stdin = os.Stdin
 	if out == nil {
@@ -766,47 +786,47 @@ func sendRequest(url string, config config.Config) (*http.Response, error) {
 	return client.Do(req)
 }
 
-func getBazelCommitsBetween(goodCommit string, badCommit string, config config.Config) (string, []string, error) {
+func getBazelCommitsBetween(oldCommit string, newCommit string, config config.Config) (string, []string, error) {
 	commitList := make([]string, 0)
 	page := 1
 	perPage := 250 // 250 is the maximum number of commits per page
 
 	for {
-		url := fmt.Sprintf("https://api.github.com/repos/bazelbuild/bazel/compare/%s...%s?page=%d&per_page=%d", goodCommit, badCommit, page, perPage)
+		url := fmt.Sprintf("https://api.github.com/repos/bazelbuild/bazel/compare/%s...%s?page=%d&per_page=%d", oldCommit, newCommit, page, perPage)
 
 		response, err := sendRequest(url, config)
 		if err != nil {
-			return goodCommit, nil, fmt.Errorf("Error fetching commit data: %v", err)
+			return oldCommit, nil, fmt.Errorf("Error fetching commit data: %v", err)
 		}
 		defer response.Body.Close()
 
 		body, err := io.ReadAll(response.Body)
 		if err != nil {
-			return goodCommit, nil, fmt.Errorf("Error reading response body: %v", err)
+			return oldCommit, nil, fmt.Errorf("Error reading response body: %v", err)
 		}
 
 		if response.StatusCode == http.StatusNotFound {
-			return goodCommit, nil, fmt.Errorf("repository or commit not found: %s", string(body))
+			return oldCommit, nil, fmt.Errorf("repository or commit not found: %s", string(body))
 		} else if response.StatusCode == 403 {
-			return goodCommit, nil, fmt.Errorf("github API rate limit hit, consider setting BAZELISK_GITHUB_TOKEN: %s", string(body))
+			return oldCommit, nil, fmt.Errorf("github API rate limit hit, consider setting BAZELISK_GITHUB_TOKEN: %s", string(body))
 		} else if response.StatusCode != http.StatusOK {
-			return goodCommit, nil, fmt.Errorf("unexpected response status code %d: %s", response.StatusCode, string(body))
+			return oldCommit, nil, fmt.Errorf("unexpected response status code %d: %s", response.StatusCode, string(body))
 		}
 
 		var compResp compareResponse
 		err = json.Unmarshal(body, &compResp)
 		if err != nil {
-			return goodCommit, nil, fmt.Errorf("Error unmarshaling JSON: %v", err)
+			return oldCommit, nil, fmt.Errorf("Error unmarshaling JSON: %v", err)
 		}
-	
+
 		if len(compResp.Commits) == 0 {
 			break
 		}
 
 		mergeBaseCommit := compResp.MergeBaseCommit.SHA
+		oldCommit = mergeBaseCommit
 		if mergeBaseCommit != compResp.BaseCommit.SHA {
-			fmt.Printf("The good Bazel commit is not an ancestor of the bad Bazel commit, overriding the good Bazel commit to the merge base commit %s\n", mergeBaseCommit)
-			goodCommit = mergeBaseCommit
+			fmt.Printf("The old Bazel commit is not an ancestor of the new Bazel commit, overriding the old Bazel commit to the merge base commit %s\n", mergeBaseCommit)
 		}
 
 		for _, commit := range compResp.Commits {
@@ -825,32 +845,38 @@ func getBazelCommitsBetween(goodCommit string, badCommit string, config config.C
 	}
 
 	if len(commitList) == 0 {
-		return goodCommit, nil, fmt.Errorf("no commits found between (%s, %s], the good commit should be first, maybe try with --bisect=%s..%s ?", goodCommit, badCommit, badCommit, goodCommit)
+		return oldCommit, nil, fmt.Errorf("no commits found between (%s, %s], the old commit should be first, maybe try with --bisect=%s..%s or --bisect=~%s..%s?", oldCommit, newCommit, newCommit, oldCommit, oldCommit, newCommit)
 	}
-	fmt.Printf("Found %d commits between (%s, %s]\n", len(commitList), goodCommit, badCommit)
-	return goodCommit, commitList, nil
+	fmt.Printf("Found %d commits between (%s, %s]\n", len(commitList), oldCommit, newCommit)
+	return oldCommit, commitList, nil
 }
 
-func bisect(goodCommit string, badCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) {
+func bisect(oldCommit string, newCommit string, args []string, bazeliskHome string, repos *Repositories, config config.Config) {
+	var oldCommitIs string
+	if strings.HasPrefix(oldCommit, "~") {
+		oldCommit = oldCommit[1:]
+		oldCommitIs = "bad"
+	} else {
+		oldCommitIs = "good"
+	}
 
-	// 1. Get the list of commits between goodCommit and badCommit
-	fmt.Printf("\n\n--- Getting the list of commits between %s and %s\n\n", goodCommit, badCommit)
-	goodCommit, commitList, err := getBazelCommitsBetween(goodCommit, badCommit, config)
+	// 1. Get the list of commits between oldCommit and newCommit
+	fmt.Printf("\n\n--- Getting the list of commits between %s and %s\n\n", oldCommit, newCommit)
+	oldCommit, commitList, err := getBazelCommitsBetween(oldCommit, newCommit, config)
 	if err != nil {
 		log.Fatalf("Failed to get commits: %v", err)
-		os.Exit(1)
 	}
 
-	// 2. Check if goodCommit is actually good
-	fmt.Printf("\n\n--- Verifying if the given good Bazel commit (%s) is actually good\n\n", goodCommit)
-	bazelExitCode, err := testWithBazelAtCommit(goodCommit, args, bazeliskHome, repos, config)
+	// 2. Check if oldCommit is actually good/bad as specified
+	fmt.Printf("\n\n--- Verifying if the given %s Bazel commit (%s) is actually %s\n\n", oldCommitIs, oldCommit, oldCommitIs)
+	bazelExitCode, err := testWithBazelAtCommit(oldCommit, args, bazeliskHome, repos, config)
 	if err != nil {
 		log.Fatalf("could not run Bazel: %v", err)
-		os.Exit(1)
 	}
-	if bazelExitCode != 0 {
+	if oldCommitIs == "good" && bazelExitCode != 0 {
 		fmt.Printf("Failure: Given good bazel commit is already broken.\n")
-		os.Exit(1)
+	} else if oldCommitIs == "bad" && bazelExitCode == 0 {
+		fmt.Printf("Failure: Given bad bazel commit is already fixed.\n")
 	}
 
 	// 3. Bisect commits
@@ -864,24 +890,39 @@ func bisect(goodCommit string, badCommit string, args []string, bazeliskHome str
 		bazelExitCode, err := testWithBazelAtCommit(midCommit, args, bazeliskHome, repos, config)
 		if err != nil {
 			log.Fatalf("could not run Bazel: %v", err)
-			os.Exit(1)
 		}
 		if bazelExitCode == 0 {
 			fmt.Printf("\n\n--- Succeeded at %s\n\n", midCommit)
-			left = mid + 1
+			if oldCommitIs == "good" {
+				left = mid + 1
+			} else {
+				right = mid
+			}
 		} else {
 			fmt.Printf("\n\n--- Failed at %s\n\n", midCommit)
-			right = mid
+			if oldCommitIs == "good" {
+				right = mid
+			} else {
+				left = mid + 1
+			}
 		}
 	}
 
 	// 4. Print the result
 	fmt.Printf("\n\n--- Bisect Result\n\n")
 	if right == len(commitList) {
-		fmt.Printf("first bad commit not found, every commit succeeded.\n")
+		if oldCommitIs == "good" {
+			fmt.Printf("first bad commit not found, every commit succeeded.\n")
+		} else {
+			fmt.Printf("first good commit not found, every commit failed.\n")
+		}
 	} else {
-		firstBadCommit := commitList[right]
-		fmt.Printf("first bad commit is https://github.com/bazelbuild/bazel/commit/%s\n", firstBadCommit)
+		flippingCommit := commitList[right]
+		if oldCommitIs == "good" {
+			fmt.Printf("first bad commit is https://github.com/bazelbuild/bazel/commit/%s\n", flippingCommit)
+		} else {
+			fmt.Printf("first good commit is https://github.com/bazelbuild/bazel/commit/%s\n", flippingCommit)
+		}
 	}
 
 	os.Exit(0)
diff --git a/core/repositories.go b/core/repositories.go
index ba1a24d..66dc6b9 100644
--- a/core/repositories.go
+++ b/core/repositories.go
@@ -22,40 +22,24 @@ const (
 // DownloadFunc downloads a specific Bazel binary to the given location and returns the absolute path.
 type DownloadFunc func(destDir, destFile string) (string, error)
 
-// ReleaseFilter filters Bazel versions based on specific criteria.
-type ReleaseFilter func(matchesSoFar int, currentVersion string) bool
+// LTSFilter filters Bazel versions based on specific criteria.
+type LTSFilter func(string) bool
 
-func lastNReleases(max int) ReleaseFilter {
-	return func(matchesSoFar int, currentVersion string) bool {
-		return max < 1 || matchesSoFar < max
-	}
-}
-
-// filterReleasesByTrack only works reliably if iterating on Bazel versions in descending order.
-func filterReleasesByTrack(track int) ReleaseFilter {
-	prefix := fmt.Sprintf("%d.", track)
-	return func(matchesSoFar int, currentVersion string) bool {
-		return matchesSoFar == 0 && strings.HasPrefix(currentVersion, prefix)
-	}
-}
-
-// ReleaseRepo represents a repository that stores LTS Bazel releases.
-type ReleaseRepo interface {
-	// GetReleaseVersions returns a list of all available release versions that match the given filter function.
-	// Warning: the filter only works reliably if the versions are processed in descending order!
-	GetReleaseVersions(bazeliskHome string, filter ReleaseFilter) ([]string, error)
-
-	// DownloadRelease downloads the given Bazel version into the specified location and returns the absolute path.
-	DownloadRelease(version, destDir, destFile string, config config.Config) (string, error)
+// FilterOpts represents options relevant to filtering Bazel versions.
+type FilterOpts struct {
+	MaxResults int
+	Track      int
+	Filter     LTSFilter
 }
 
-// CandidateRepo represents a repository that stores Bazel release candidates.
-type CandidateRepo interface {
-	// GetCandidateVersions returns the versions of all available release candidates.
-	GetCandidateVersions(bazeliskHome string) ([]string, error)
+// LTSRepo represents a repository that stores LTS Bazel releases and their candidates.
+type LTSRepo interface {
+	// GetLTSVersions returns a list of all available LTS release (candidates) that match the given filter options.
+	// Warning: Filters only work reliably if the versions are processed in descending order!
+	GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([]string, error)
 
-	// DownloadCandidate downloads the given Bazel release candidate into the specified location and returns the absolute path.
-	DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error)
+	// DownloadLTS downloads the given Bazel version into the specified location and returns the absolute path.
+	DownloadLTS(version, destDir, destFile string, config config.Config) (string, error)
 }
 
 // ForkRepo represents a repository that stores a fork of Bazel (releases).
@@ -70,10 +54,8 @@ type ForkRepo interface {
 // CommitRepo represents a repository that stores Bazel binaries built at specific commits.
 // It can also return the hashes of the most recent commits that passed Bazel CI pipelines successfully.
 type CommitRepo interface {
-	// GetLastGreenCommit returns the most recent commit at which a Bazel binary passed a specific Bazel CI pipeline.
-	// If downstreamGreen is true, the pipeline is https://buildkite.com/bazel/bazel-at-head-plus-downstream, otherwise
-	// it's https://buildkite.com/bazel/bazel-bazel
-	GetLastGreenCommit(bazeliskHome string, downstreamGreen bool) (string, error)
+	// GetLastGreenCommit returns the most recent commit at which a Bazel binary is successfully built.
+	GetLastGreenCommit(bazeliskHome string) (string, error)
 
 	// DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path.
 	DownloadAtCommit(commit, destDir, destFile string, config config.Config) (string, error)
@@ -90,8 +72,7 @@ type RollingRepo interface {
 
 // Repositories offers access to different types of Bazel repositories, mainly for finding and downloading the correct version of Bazel.
 type Repositories struct {
-	Releases        ReleaseRepo
-	Candidates      CandidateRepo
+	LTS             LTSRepo
 	Fork            ForkRepo
 	Commits         CommitRepo
 	Rolling         RollingRepo
@@ -107,22 +88,20 @@ func (r *Repositories) ResolveVersion(bazeliskHome, fork, version string, config
 
 	if vi.IsFork {
 		return r.resolveFork(bazeliskHome, vi, config)
-	} else if vi.IsRelease {
-		return r.resolveRelease(bazeliskHome, vi, config)
-	} else if vi.IsCandidate {
-		return r.resolveCandidate(bazeliskHome, vi, config)
+	} else if vi.IsLTS {
+		return r.resolveLTS(bazeliskHome, vi, config)
 	} else if vi.IsCommit {
 		return r.resolveCommit(bazeliskHome, vi, config)
 	} else if vi.IsRolling {
 		return r.resolveRolling(bazeliskHome, vi, config)
 	}
 
-	return "", nil, fmt.Errorf("Unsupported version identifier '%s'", version)
+	return "", nil, fmt.Errorf("unsupported version identifier '%s'", version)
 }
 
 func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
-	if vi.IsRelative && (vi.IsCandidate || vi.IsCommit) {
-		return "", nil, errors.New("forks do not support last_rc, last_green and last_downstream_green")
+	if vi.IsRelative && (vi.MustBeCandidate || vi.IsCommit) {
+		return "", nil, errors.New("forks do not support last_rc and last_green")
 	}
 	lister := func(bazeliskHome string) ([]string, error) {
 		return r.Fork.GetVersions(bazeliskHome, vi.Fork)
@@ -137,35 +116,39 @@ func (r *Repositories) resolveFork(bazeliskHome string, vi *versions.Info, confi
 	return version, downloader, nil
 }
 
-func (r *Repositories) resolveRelease(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
-	lister := func(bazeliskHome string) ([]string, error) {
-		var filter ReleaseFilter
-		if vi.TrackRestriction > 0 {
-			// Optimization: only fetch matching releases if an LTS track is specified.
-			filter = filterReleasesByTrack(vi.TrackRestriction)
-		} else {
-			// Optimization: only fetch last (x+1) releases if the version is "latest-x".
-			filter = lastNReleases(vi.LatestOffset + 1)
-		}
-		return r.Releases.GetReleaseVersions(bazeliskHome, filter)
-	}
-	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi)
-	if err != nil {
-		return "", nil, err
+var IsRelease = func(version string) bool {
+	return !strings.Contains(version, "rc")
+}
+
+var IsCandidate = func(version string) bool {
+	return strings.Contains(version, "rc")
+}
+
+func (r *Repositories) resolveLTS(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
+	opts := &FilterOpts{
+		// Optimization: only fetch last (x+1) releases if the version is "latest-x".
+		MaxResults: vi.LatestOffset + 1,
+		Track:      vi.TrackRestriction,
 	}
-	downloader := func(destDir, destFile string) (string, error) {
-		return r.Releases.DownloadRelease(version, destDir, destFile, config)
+
+	if vi.MustBeRelease {
+		opts.Filter = IsRelease
+	} else if vi.MustBeCandidate {
+		opts.Filter = IsCandidate
+	} else {
+		// Wildcard -> can be either release or candidate
+		opts.Filter = func(v string) bool { return true }
 	}
-	return version, downloader, nil
-}
 
-func (r *Repositories) resolveCandidate(bazeliskHome string, vi *versions.Info, config config.Config) (string, DownloadFunc, error) {
-	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, r.Candidates.GetCandidateVersions, vi)
+	lister := func(bazeliskHome string) ([]string, error) {
+		return r.LTS.GetLTSVersions(bazeliskHome, opts)
+	}
+	version, err := resolvePotentiallyRelativeVersion(bazeliskHome, lister, vi)
 	if err != nil {
 		return "", nil, err
 	}
 	downloader := func(destDir, destFile string) (string, error) {
-		return r.Candidates.DownloadCandidate(version, destDir, destFile, config)
+		return r.LTS.DownloadLTS(version, destDir, destFile, config)
 	}
 	return version, downloader, nil
 }
@@ -174,7 +157,7 @@ func (r *Repositories) resolveCommit(bazeliskHome string, vi *versions.Info, con
 	version := vi.Value
 	if vi.IsRelative {
 		var err error
-		version, err = r.Commits.GetLastGreenCommit(bazeliskHome, vi.IsDownstream)
+		version, err = r.Commits.GetLastGreenCommit(bazeliskHome)
 		if err != nil {
 			return "", nil, fmt.Errorf("cannot resolve last green commit: %v", err)
 		}
@@ -213,7 +196,7 @@ func resolvePotentiallyRelativeVersion(bazeliskHome string, lister listVersionsF
 
 	index := len(available) - 1 - vi.LatestOffset
 	if index < 0 {
-		return "", fmt.Errorf("cannot resolve version \"%s\": There are not enough matching Bazel releases (%d)", vi.Value, len(available))
+		return "", fmt.Errorf("cannot resolve version %q: There are not enough matching Bazel releases (%d)", vi.Value, len(available))
 	}
 	sorted := versions.GetInAscendingOrder(available)
 	return sorted[index], nil
@@ -297,19 +280,13 @@ func (r *Repositories) DownloadFromFormatURL(config config.Config, formatURL, ve
 }
 
 // CreateRepositories creates a new Repositories instance with the given repositories. Any nil repository will be replaced by a dummy repository that raises an error whenever a download is attempted.
-func CreateRepositories(releases ReleaseRepo, candidates CandidateRepo, fork ForkRepo, commits CommitRepo, rolling RollingRepo, supportsBaseURL bool) *Repositories {
+func CreateRepositories(lts LTSRepo, fork ForkRepo, commits CommitRepo, rolling RollingRepo, supportsBaseURL bool) *Repositories {
 	repos := &Repositories{supportsBaseURL: supportsBaseURL}
 
-	if releases == nil {
-		repos.Releases = &noReleaseRepo{err: errors.New("Bazel LTS releases are not supported")}
-	} else {
-		repos.Releases = releases
-	}
-
-	if candidates == nil {
-		repos.Candidates = &noCandidateRepo{err: errors.New("Bazel release candidates are not supported")}
+	if lts == nil {
+		repos.LTS = &noLTSRepo{err: errors.New("Bazel LTS releases & candidates are not supported")}
 	} else {
-		repos.Candidates = candidates
+		repos.LTS = lts
 	}
 
 	if fork == nil {
@@ -333,31 +310,19 @@ func CreateRepositories(releases ReleaseRepo, candidates CandidateRepo, fork For
 	return repos
 }
 
-// The whole point of the structs below this line is that users can simply call repos.Releases.GetReleaseVersions()
-// (etc) without having to worry whether `Releases` points at an actual repo.
-
-type noReleaseRepo struct {
-	err error
-}
-
-func (nrr *noReleaseRepo) GetReleaseVersions(bazeliskHome string, filter ReleaseFilter) ([]string, error) {
-	return nil, nrr.err
-}
-
-func (nrr *noReleaseRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) {
-	return "", nrr.err
-}
+// The whole point of the structs below this line is that users can simply call repos.LTS.GetLTSVersions()
+// (etc) without having to worry whether `LTS` points at an actual repo.
 
-type noCandidateRepo struct {
+type noLTSRepo struct {
 	err error
 }
 
-func (ncc *noCandidateRepo) GetCandidateVersions(bazeliskHome string) ([]string, error) {
-	return nil, ncc.err
+func (nolts *noLTSRepo) GetLTSVersions(bazeliskHome string, opts *FilterOpts) ([]string, error) {
+	return nil, nolts.err
 }
 
-func (ncc *noCandidateRepo) DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) {
-	return "", ncc.err
+func (nolts *noLTSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) {
+	return "", nolts.err
 }
 
 type noForkRepo struct {
@@ -376,7 +341,7 @@ type noCommitRepo struct {
 	err error
 }
 
-func (nlgr *noCommitRepo) GetLastGreenCommit(bazeliskHome string, downstreamGreen bool) (string, error) {
+func (nlgr *noCommitRepo) GetLastGreenCommit(bazeliskHome string) (string, error) {
 	return "", nlgr.err
 }
 
diff --git a/deb/BUILD b/deb/BUILD
index a74f52b..6a32ce8 100644
--- a/deb/BUILD
+++ b/deb/BUILD
@@ -35,14 +35,18 @@ ARCHES = ["amd64", "arm64"]
 [
     pkg_deb(
         name = "bazelisk-{}_deb".format(arch),
+        out = "bazelisk-{}.deb".format(arch),
         architecture = arch,
-        conflicts = ["bazel"],
+        conflicts = [
+            "bazel",
+            "bazel-bootstrap",
+        ],
         data = ":bazelisk-{}_tar".format(arch),
+        depends = ["ca-certificates"],
         description_file = ":description.txt",
         homepage = "https://github.com/bazelbuild/bazelisk",
         license = "Apache-2.0",
         maintainer = "The Bazel Authors <bazel-dev@googlegroups.com>",
-        out = "bazelisk-{}.deb".format(arch),
         package = "bazelisk",
         section = "contrib/devel",
         version_file = ":version.txt",
diff --git a/go.mod b/go.mod
index 5ff1656..92540ba 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
 	github.com/hashicorp/go-version v1.7.0
 	github.com/mitchellh/go-homedir v1.1.0
-	golang.org/x/term v0.23.0
+	golang.org/x/term v0.26.0
 )
 
-require golang.org/x/sys v0.23.0 // indirect
+require golang.org/x/sys v0.27.0 // indirect
diff --git a/go.sum b/go.sum
index bd3479e..ad8f4f8 100644
--- a/go.sum
+++ b/go.sum
@@ -4,7 +4,7 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
 github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
-golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
-golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
diff --git a/httputil/fake.go b/httputil/fake.go
index 4b280a8..f870af6 100644
--- a/httputil/fake.go
+++ b/httputil/fake.go
@@ -9,6 +9,8 @@ import (
 // FakeTransport represents a fake http.Transport that returns prerecorded responses.
 type FakeTransport struct {
 	responses map[string]*responseCollection
+
+	RequestedURLs []string
 }
 
 // NewFakeTransport creates a new FakeTransport instance without any responses.
@@ -38,6 +40,7 @@ func (ft *FakeTransport) AddError(url string, err error) {
 
 // RoundTrip returns a prerecorded response to the given request, if one exists. Otherwise its response indicates 404 - not found.
 func (ft *FakeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+	ft.RequestedURLs = append(ft.RequestedURLs, req.URL.String())
 	if responses, ok := ft.responses[req.URL.String()]; ok {
 		return responses.Next()
 	}
diff --git a/httputil/httputil.go b/httputil/httputil.go
index 2180ce9..c020e06 100644
--- a/httputil/httputil.go
+++ b/httputil/httputil.go
@@ -95,6 +95,12 @@ func get(url, auth string) (*http.Response, error) {
 			return res, err
 		}
 
+		if (res != nil) {
+			// Need to retry, close the response body immediately to release resources.
+			// See https://github.com/googleapis/google-cloud-go/issues/7440#issuecomment-1491008639
+			res.Body.Close()
+		}
+
 		if err == nil {
 			lastFailure = fmt.Sprintf("HTTP %d", res.StatusCode)
 		} else {
@@ -107,7 +113,7 @@ func get(url, auth string) (*http.Response, error) {
 
 		nextTryAt := RetryClock.Now().Add(waitFor)
 		if nextTryAt.After(deadline) {
-			return nil, fmt.Errorf("unable to complete request to %s within %v", url, MaxRequestDuration)
+			return nil, fmt.Errorf("unable to complete %d requests to %s within %v. Most recent failure: %s", attempt+1, url, MaxRequestDuration, lastFailure)
 		}
 		if attempt < MaxRetries {
 			RetryClock.Sleep(waitFor)
@@ -121,7 +127,7 @@ func shouldRetry(res *http.Response, err error) bool {
 	if err != nil {
 		return true
 	}
-	// For HTTP: only retry on permanent/fatal errors.
+	// For HTTP: only retry on non-permanent/fatal errors.
 	return res.StatusCode == 429 || (500 <= res.StatusCode && res.StatusCode <= 504)
 }
 
diff --git a/platforms/platforms.go b/platforms/platforms.go
index ebf6032..6f4c7e8 100644
--- a/platforms/platforms.go
+++ b/platforms/platforms.go
@@ -64,7 +64,7 @@ func DetermineArchitecture(osName, version string) (string, error) {
 	case "arm64":
 		machineName = "arm64"
 	default:
-		return "", fmt.Errorf("unsupported machine architecture \"%s\", must be arm64 or x86_64", runtime.GOARCH)
+		return "", fmt.Errorf("unsupported machine architecture %q, must be arm64 or x86_64", runtime.GOARCH)
 	}
 
 	if osName == "darwin" {
@@ -80,7 +80,7 @@ func DetermineOperatingSystem() (string, error) {
 	case "darwin", "linux", "windows":
 		return runtime.GOOS, nil
 	default:
-		return "", fmt.Errorf("unsupported operating system \"%s\", must be Linux, macOS or Windows", runtime.GOOS)
+		return "", fmt.Errorf("unsupported operating system %q, must be Linux, macOS or Windows", runtime.GOOS)
 	}
 }
 
diff --git a/repositories/gcs.go b/repositories/gcs.go
index e53133f..025b82f 100644
--- a/repositories/gcs.go
+++ b/repositories/gcs.go
@@ -5,9 +5,9 @@ package repositories
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 	"log"
+	"strconv"
 	"strings"
 	"time"
 
@@ -19,43 +19,39 @@ import (
 )
 
 const (
-	candidateBaseURL    = "https://releases.bazel.build"
-	nonCandidateBaseURL = "https://storage.googleapis.com/bazel-builds/artifacts"
-	lastGreenBaseURL    = "https://storage.googleapis.com/bazel-untrusted-builds/last_green_commit/"
-)
-
-var (
-	// key == includeDownstream
-	lastGreenCommitPathSuffixes = map[bool]string{
-		false: "github.com/bazelbuild/bazel.git/bazel-bazel",
-		true:  "downstream_pipeline",
-	}
+	ltsBaseURL         = "https://releases.bazel.build"
+	commitBaseURL      = "https://storage.googleapis.com/bazel-builds/artifacts"
+	lastGreenCommitURL = "https://storage.googleapis.com/bazel-builds/last_green_commit/github.com/bazelbuild/bazel.git/publish-bazel-binaries"
 )
 
 // GCSRepo represents a Bazel repository on Google Cloud Storage that contains Bazel releases, release candidates and Bazel binaries built at arbitrary commits.
 // It can return all available Bazel versions, as well as downloading a specific version.
 type GCSRepo struct{}
 
-// ReleaseRepo
+// LTSRepo
 
-// GetReleaseVersions returns the versions of all available Bazel releases in this repository that match the given filter.
-func (gcs *GCSRepo) GetReleaseVersions(bazeliskHome string, filter core.ReleaseFilter) ([]string, error) {
+// GetLTSVersions returns the versions of all available Bazel releases in this repository that match the given filter.
+func (gcs *GCSRepo) GetLTSVersions(bazeliskHome string, opts *core.FilterOpts) ([]string, error) {
 	history, err := getVersionHistoryFromGCS()
 	if err != nil {
 		return []string{}, err
 	}
-	releases, err := gcs.removeCandidates(history, filter)
+	matches, err := gcs.matchingVersions(history, opts)
 	if err != nil {
 		return []string{}, err
 	}
-	if len(releases) == 0 {
-		return []string{}, errors.New("there are no releases available")
+	if len(matches) == 0 {
+		var suffix string
+		if opts.Track > 0 {
+			suffix = fmt.Sprintf(" for track %d", opts.Track)
+		}
+		return []string{}, fmt.Errorf("could not find any LTS Bazel binaries%s", suffix)
 	}
-	return releases, nil
+	return matches, nil
 }
 
 func getVersionHistoryFromGCS() ([]string, error) {
-	prefixes, _, err := listDirectoriesInReleaseBucket("")
+	prefixes, err := listDirectoriesInBucket("")
 	if err != nil {
 		return []string{}, fmt.Errorf("could not list Bazel versions in GCS bucket: %v", err)
 	}
@@ -65,14 +61,13 @@ func getVersionHistoryFromGCS() ([]string, error) {
 	return sorted, nil
 }
 
-func listDirectoriesInReleaseBucket(prefix string) ([]string, bool, error) {
+func listDirectoriesInBucket(prefix string) ([]string, error) {
 	baseURL := "https://www.googleapis.com/storage/v1/b/bazel/o?delimiter=/"
 	if prefix != "" {
 		baseURL = fmt.Sprintf("%s&prefix=%s", baseURL, prefix)
 	}
 
 	var prefixes []string
-	var isRelease = false
 	var nextPageToken = ""
 	for {
 		var url = baseURL
@@ -96,32 +91,22 @@ func listDirectoriesInReleaseBucket(prefix string) ([]string, bool, error) {
 		}
 
 		if err != nil {
-			return nil, false, fmt.Errorf("could not list GCS objects at %s: %v", url, err)
+			return nil, fmt.Errorf("could not list GCS objects at %s: %v", url, err)
 		}
 
 		var response GcsListResponse
 		if err := json.Unmarshal(content, &response); err != nil {
-			return nil, false, fmt.Errorf("could not parse GCS index JSON: %v", err)
+			return nil, fmt.Errorf("could not parse GCS index JSON: %v", err)
 		}
 
 		prefixes = append(prefixes, response.Prefixes...)
-		isRelease = isRelease || len(response.Items) > 0
 
 		if response.NextPageToken == "" {
 			break
 		}
 		nextPageToken = response.NextPageToken
 	}
-	return prefixes, isRelease, nil
-}
-
-func getVersionsFromGCSPrefixes(versions []string) []string {
-	result := make([]string, len(versions))
-	for i, v := range versions {
-		noSlashes := strings.Replace(v, "/", "", -1)
-		result[i] = strings.TrimSuffix(noSlashes, "release")
-	}
-	return result
+	return prefixes, nil
 }
 
 // GcsListResponse represents the result of listing the contents of a GCS bucket.
@@ -137,128 +122,99 @@ type GcsListResponse struct {
 	NextPageToken string `json:"nextPageToken"`
 }
 
-// DownloadRelease downloads the given Bazel release into the specified location and returns the absolute path.
-func (gcs *GCSRepo) DownloadRelease(version, destDir, destFile string, config config.Config) (string, error) {
-	srcFile, err := platforms.DetermineBazelFilename(version, true, config)
-	if err != nil {
-		return "", err
+func getVersionsFromGCSPrefixes(versions []string) []string {
+	result := make([]string, len(versions))
+	for i, v := range versions {
+		noSlashes := strings.Replace(v, "/", "", -1)
+		result[i] = strings.TrimSuffix(noSlashes, "release")
 	}
-
-	url := fmt.Sprintf("%s/%s/release/%s", candidateBaseURL, version, srcFile)
-	return httputil.DownloadBinary(url, destDir, destFile, config)
+	return result
 }
 
-func (gcs *GCSRepo) removeCandidates(history []string, filter core.ReleaseFilter) ([]string, error) {
-	descendingReleases := make([]string, 0)
-	filterPassed := false
-	// Iteration in descending order is important:
-	// - ReleaseFilter won't work correctly otherwise
-	// - It makes more sense in the "latest-n" use case
+func (gcs *GCSRepo) matchingVersions(history []string, opts *core.FilterOpts) ([]string, error) {
+	descendingMatches := make([]string, 0)
+	// history is a list of base versions in ascending order (i.e. X.Y.Z, no rolling releases or candidates).
 	for hpos := len(history) - 1; hpos >= 0; hpos-- {
-		latestVersion := history[hpos]
-		pass := filter(len(descendingReleases), latestVersion)
-		if pass {
-			filterPassed = true
-		} else {
-			// Underlying assumption: all matching versions are in a single continuous sequence.
-			// Consequently, if the filter returns false, this means:
-			if filterPassed {
-				// a) "break" if we've seen a match before (because all future version won't match either)
-				break
-			} else {
-				// b) "continue looking" if we've never seen a match before
+		baseVersion := history[hpos]
+		if opts.Track > 0 {
+			track, err := getTrack(baseVersion)
+			if err != nil {
+				continue // Ignore invalid GCS entries for now
+			}
+			if track > opts.Track {
 				continue
+			} else if track < opts.Track {
+				break
 			}
 		}
-		// Obviously this only works for the existing filters (lastN and track-based) and
-		// because versions in history are sorted.
 
-		_, isRelease, err := listDirectoriesInReleaseBucket(latestVersion + "/release/")
-		if err != nil {
-			return []string{}, fmt.Errorf("could not list available releases for %v: %v", latestVersion, err)
-		}
-		if isRelease {
-			descendingReleases = append(descendingReleases, latestVersion)
-		}
-	}
-	return reverseInPlace(descendingReleases), nil
-}
-
-func reverseInPlace(values []string) []string {
-	for i := 0; i < len(values)/2; i++ {
-		j := len(values) - 1 - i
-		values[i], values[j] = values[j], values[i]
-	}
-	return values
-}
-
-// CandidateRepo
-
-// GetCandidateVersions returns all versions of available release candidates for the latest release in this repository.
-func (gcs *GCSRepo) GetCandidateVersions(bazeliskHome string) ([]string, error) {
-	history, err := getVersionHistoryFromGCS()
-	if err != nil {
-		return []string{}, err
-	}
-
-	if len(history) == 0 {
-		return []string{}, errors.New("could not find any Bazel versions")
-	}
-
-	// Find most recent directory that contains any release candidates.
-	// Typically it should be the last or second-to-last, depending on whether there are new rolling releases.
-	for pos := len(history) - 1; pos >= 0; pos-- {
 		// Append slash to match directories
-		bucket := fmt.Sprintf("%s/", history[pos])
-		rcPrefixes, _, err := listDirectoriesInReleaseBucket(bucket)
+		bucket := fmt.Sprintf("%s/", history[hpos])
+		prefixes, err := listDirectoriesInBucket(bucket)
 		if err != nil {
-			return []string{}, fmt.Errorf("could not list release candidates for latest release: %v", err)
+			return []string{}, fmt.Errorf("could not list LTS releases/candidates: %v", err)
 		}
 
-		rcs := make([]string, 0)
-		for _, v := range getVersionsFromGCSPrefixes(rcPrefixes) {
-			// Remove full and rolling releases
-			if strings.Contains(v, "rc") {
-				rcs = append(rcs, v)
+		// Ascending list of rc versions, followed by the release version (if it exists) and a rolling identifier (if there are rolling releases).
+		versions := getVersionsFromGCSPrefixes(prefixes)
+		for vpos := len(versions) - 1; vpos >= 0; vpos-- {
+			curr := versions[vpos]
+			if strings.Contains(curr, "rolling") || !opts.Filter(curr) {
+				continue
+			}
+
+			descendingMatches = append(descendingMatches, curr)
+			if len(descendingMatches) == opts.MaxResults {
+				return descendingMatches, nil
 			}
-		}
-		if len(rcs) > 0 {
-			return rcs, nil
 		}
 	}
-	return nil, nil
+	return descendingMatches, nil
 }
 
-// DownloadCandidate downloads the given release candidate into the specified location and returns the absolute path.
-func (gcs *GCSRepo) DownloadCandidate(version, destDir, destFile string, config config.Config) (string, error) {
-	if !strings.Contains(version, "rc") {
-		return "", fmt.Errorf("'%s' does not refer to a release candidate", version)
+func getTrack(version string) (int, error) {
+	major, _, _ := strings.Cut(version, ".")
+	if val, err := strconv.Atoi(major); err == nil {
+		return val, nil
 	}
+	return 0, fmt.Errorf("invalid version %q", version)
+}
 
+// DownloadLTS downloads the given Bazel LTS release (candidate) into the specified location and returns the absolute path.
+func (gcs *GCSRepo) DownloadLTS(version, destDir, destFile string, config config.Config) (string, error) {
 	srcFile, err := platforms.DetermineBazelFilename(version, true, config)
 	if err != nil {
 		return "", err
 	}
 
-	versionComponents := strings.Split(version, "rc")
-	baseVersion := versionComponents[0]
-	rcVersion := "rc" + versionComponents[1]
-	url := fmt.Sprintf("%s/%s/%s/%s", candidateBaseURL, baseVersion, rcVersion, srcFile)
+	var baseVersion, folder string
+	if strings.Contains(version, "rc") {
+		versionComponents := strings.Split(version, "rc")
+		baseVersion, folder = versionComponents[0], "rc"+versionComponents[1]
+	} else {
+		baseVersion, folder = version, "release"
+	}
+
+	url := fmt.Sprintf("%s/%s/%s/%s", ltsBaseURL, baseVersion, folder, srcFile)
 	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
 // CommitRepo
 
-// GetLastGreenCommit returns the most recent commit at which a Bazel binary passed a specific Bazel CI pipeline.
-// If downstreamGreen is true, the pipeline is https://buildkite.com/bazel/bazel-at-head-plus-downstream, otherwise
-// it's https://buildkite.com/bazel/bazel-bazel
-func (gcs *GCSRepo) GetLastGreenCommit(bazeliskHome string, downstreamGreen bool) (string, error) {
-	pathSuffix := lastGreenCommitPathSuffixes[downstreamGreen]
-	content, _, err := httputil.ReadRemoteFile(lastGreenBaseURL+pathSuffix, "")
+// GetLastGreenCommit returns the most recent commit at which a Bazel binary is successfully built.
+func (gcs *GCSRepo) GetLastGreenCommit(bazeliskHome string) (string, error) {
+	content, _, err := httputil.ReadRemoteFile(lastGreenCommitURL, "")
 	if err != nil {
 		return "", fmt.Errorf("could not determine last green commit: %v", err)
 	}
-	return strings.TrimSpace(string(content)), nil
+
+	// Validate the content does look like a commit hash
+	commit := strings.TrimSpace(string(content))
+	if !versions.MatchCommitPattern(commit) {
+		return "", fmt.Errorf("invalid commit hash: %s", commit)
+	}
+
+	return commit, nil
 }
 
 // DownloadAtCommit downloads a Bazel binary built at the given commit into the specified location and returns the absolute path.
@@ -268,7 +224,7 @@ func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string, config co
 	if err != nil {
 		return "", err
 	}
-	url := fmt.Sprintf("%s/%s/%s/bazel", nonCandidateBaseURL, platform, commit)
+	url := fmt.Sprintf("%s/%s/%s/bazel", commitBaseURL, platform, commit)
 	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
 
@@ -282,7 +238,7 @@ func (gcs *GCSRepo) GetRollingVersions(bazeliskHome string) ([]string, error) {
 	}
 
 	newest := history[len(history)-1]
-	versions, _, err := listDirectoriesInReleaseBucket(newest + "/rolling/")
+	versions, err := listDirectoriesInBucket(newest + "/rolling/")
 	if err != nil {
 		return []string{}, err
 	}
@@ -305,6 +261,6 @@ func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string, config co
 	}
 
 	releaseVersion := strings.Split(version, "-")[0]
-	url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, releaseVersion, version, srcFile)
+	url := fmt.Sprintf("%s/%s/rolling/%s/%s", ltsBaseURL, releaseVersion, version, srcFile)
 	return httputil.DownloadBinary(url, destDir, destFile, config)
 }
diff --git a/versions/versions.go b/versions/versions.go
index a567045..c4decb5 100644
--- a/versions/versions.go
+++ b/versions/versions.go
@@ -17,7 +17,8 @@ const (
 )
 
 var (
-	releasePattern       = regexp.MustCompile(`^(\d+)\.(x|\d+\.\d+)$`)
+	releasePattern       = regexp.MustCompile(`^(\d+)\.\d+\.\d+$`)
+	trackPattern         = regexp.MustCompile(`^(\d+)\.(x|\*)$`)
 	patchPattern         = regexp.MustCompile(`^(\d+\.\d+\.\d+)-([\w\d]+)$`)
 	candidatePattern     = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`)
 	rollingPattern       = regexp.MustCompile(`^\d+\.0\.0-pre\.\d{8}(\.\d+){1,2}$`)
@@ -27,9 +28,11 @@ var (
 
 // Info represents a structured Bazel version identifier.
 type Info struct {
-	IsRelease, IsCandidate, IsCommit, IsFork, IsRolling, IsRelative, IsDownstream bool
-	Fork, Value                                                                   string
-	LatestOffset, TrackRestriction                                                int
+	MustBeRelease, MustBeCandidate bool
+	IsLTS, IsRolling               bool
+	IsCommit, IsFork, IsRelative   bool
+	Fork, Value                    string
+	LatestOffset, TrackRestriction int
 }
 
 // Parse extracts and returns structured information about the given Bazel version label.
@@ -37,48 +40,50 @@ func Parse(fork, version string) (*Info, error) {
 	vi := &Info{Fork: fork, Value: version, IsFork: isFork(fork)}
 
 	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
+		vi.IsLTS = true
+		vi.MustBeRelease = true
+	} else if m := trackPattern.FindStringSubmatch(version); m != nil {
+		track, err := strconv.Atoi(m[1])
+		if err != nil {
+			return nil, fmt.Errorf("invalid version %q, expected something like '5.x' or '5.*'", version)
 		}
+		vi.IsLTS = true
+		vi.MustBeRelease = (m[2] == "x")
+		vi.IsRelative = true
+		vi.TrackRestriction = track
 	} else if patchPattern.MatchString(version) {
-		vi.IsRelease = true
+		vi.IsLTS = true
+		vi.MustBeRelease = true
 	} else if m := latestReleasePattern.FindStringSubmatch(version); m != nil {
-		vi.IsRelease = true
+		vi.IsLTS = true
+		vi.MustBeRelease = true
 		vi.IsRelative = true
 		if m[1] != "" {
 			offset, err := strconv.Atoi(m[1])
 			if err != nil {
-				return nil, fmt.Errorf("invalid version \"%s\", could not parse offset: %v", version, err)
+				return nil, fmt.Errorf("invalid version %q, could not parse offset: %v", version, err)
 			}
 			vi.LatestOffset = offset
 		}
 	} else if candidatePattern.MatchString(version) {
-		vi.IsCandidate = true
+		vi.IsLTS = true
+		vi.MustBeCandidate = true
 	} else if version == "last_rc" {
-		vi.IsCandidate = true
+		vi.IsLTS = true
+		vi.MustBeCandidate = true
 		vi.IsRelative = true
 	} else if commitPattern.MatchString(version) {
 		vi.IsCommit = true
 	} else if version == "last_green" {
 		vi.IsCommit = true
 		vi.IsRelative = true
-	} else if version == "last_downstream_green" {
-		vi.IsCommit = true
-		vi.IsRelative = true
-		vi.IsDownstream = true
 	} else if rollingPattern.MatchString(version) {
 		vi.IsRolling = true
 	} else if version == "rolling" {
 		vi.IsRolling = true
 		vi.IsRelative = true
 	} else {
-		return nil, fmt.Errorf("Invalid version '%s'", version)
+		return nil, fmt.Errorf("invalid version '%s'", version)
 	}
 	return vi, nil
 }
@@ -106,7 +111,11 @@ func GetInAscendingOrder(versions []string) []string {
 	return sorted
 }
 
+func MatchCommitPattern(version string) bool {
+	return commitPattern.MatchString(version)
+}
+
 // IsCommit returns whether the given version refers to a commit.
 func IsCommit(version string) bool {
-	return version == "last_green" || version == "last_downstream_green" || commitPattern.MatchString(version)
+	return version == "last_green" || commitPattern.MatchString(version)
 }


Test patch:
diff --git a/bazelisk_test.sh b/bazelisk_test.sh
index cf3a7de..d61d6d9 100755
--- a/bazelisk_test.sh
+++ b/bazelisk_test.sh
@@ -47,7 +47,7 @@ function setup() {
   USER_HOME="$(mktemp -d $TEST_TMPDIR/user.XXXXXX)"
   BAZELISK_HOME="$(mktemp -d $TEST_TMPDIR/home.XXXXXX)"
 
-  cp "$(rlocation __main__/releases_for_tests.json)" "${BAZELISK_HOME}/bazelbuild-releases.json"
+  cp "$(rlocation _main/releases_for_tests.json)" "${BAZELISK_HOME}/bazelbuild-releases.json"
   touch "${BAZELISK_HOME}/bazelbuild-releases.json"
   ln -s "${BAZELISK_HOME}/bazelbuild-releases.json" "${BAZELISK_HOME}/releases.json"
 
@@ -56,30 +56,30 @@ function setup() {
 }
 
 function bazelisk() {
-  if [[ -n $(rlocation __main__/bazelisk.py) ]]; then
+  if [[ -n $(rlocation _main/bazelisk.py) ]]; then
     if [[ $BAZELISK_VERSION == "PY3" ]]; then
       echo "Running Bazelisk with $(python3 -V)..."
-      python3 "$(rlocation __main__/bazelisk.py)" "$@"
+      python3 "$(rlocation _main/bazelisk.py)" "$@"
     else
       echo "Running Bazelisk with $(python -V)..."
-      python "$(rlocation __main__/bazelisk.py)" "$@"
+      python "$(rlocation _main/bazelisk.py)" "$@"
     fi
-  elif [[ -n $(rlocation __main__/windows_amd64_debug/bazelisk.exe) ]]; then
-    "$(rlocation __main__/windows_amd64_debug/bazelisk.exe)" "$@"
-  elif [[ -n $(rlocation __main__/darwin_amd64_debug/bazelisk) ]]; then
-    "$(rlocation __main__/darwin_amd64_debug/bazelisk)" "$@"
-  elif [[ -n $(rlocation __main__/linux_amd64_debug/bazelisk) ]]; then
-    "$(rlocation __main__/linux_amd64_debug/bazelisk)" "$@"
-  elif [[ -n $(rlocation __main__/windows_amd64_stripped/bazelisk.exe) ]]; then
-    "$(rlocation __main__/windows_amd64_stripped/bazelisk.exe)" "$@"
-  elif [[ -n $(rlocation __main__/darwin_amd64_stripped/bazelisk) ]]; then
-    "$(rlocation __main__/darwin_amd64_stripped/bazelisk)" "$@"
-  elif [[ -n $(rlocation __main__/linux_amd64_stripped/bazelisk) ]]; then
-    "$(rlocation __main__/linux_amd64_stripped/bazelisk)" "$@"
-  elif [[ -n $(rlocation __main__/bazelisk_/bazelisk) ]]; then
-    "$(rlocation __main__/bazelisk_/bazelisk)" "$@"
-  elif [[ -n $(rlocation __main__/bazelisk_/bazelisk.exe) ]]; then
-    "$(rlocation __main__/bazelisk_/bazelisk.exe)" "$@"
+  elif [[ -n $(rlocation _main/windows_amd64_debug/bazelisk.exe) ]]; then
+    "$(rlocation _main/windows_amd64_debug/bazelisk.exe)" "$@"
+  elif [[ -n $(rlocation _main/darwin_amd64_debug/bazelisk) ]]; then
+    "$(rlocation _main/darwin_amd64_debug/bazelisk)" "$@"
+  elif [[ -n $(rlocation _main/linux_amd64_debug/bazelisk) ]]; then
+    "$(rlocation _main/linux_amd64_debug/bazelisk)" "$@"
+  elif [[ -n $(rlocation _main/windows_amd64_stripped/bazelisk.exe) ]]; then
+    "$(rlocation _main/windows_amd64_stripped/bazelisk.exe)" "$@"
+  elif [[ -n $(rlocation _main/darwin_amd64_stripped/bazelisk) ]]; then
+    "$(rlocation _main/darwin_amd64_stripped/bazelisk)" "$@"
+  elif [[ -n $(rlocation _main/linux_amd64_stripped/bazelisk) ]]; then
+    "$(rlocation _main/linux_amd64_stripped/bazelisk)" "$@"
+  elif [[ -n $(rlocation _main/bazelisk_/bazelisk) ]]; then
+    "$(rlocation _main/bazelisk_/bazelisk)" "$@"
+  elif [[ -n $(rlocation _main/bazelisk_/bazelisk.exe) ]]; then
+    "$(rlocation _main/bazelisk_/bazelisk.exe)" "$@"
   else
     echo "Could not find the bazelisk executable, listing files:"
     find .
@@ -257,17 +257,6 @@ function test_bazel_last_green() {
       (echo "FAIL: 'bazelisk version' of an unreleased binary must not print a build label."; exit 1)
 }
 
-function test_bazel_last_downstream_green() {
-  setup
-
-  USE_BAZEL_VERSION="last_downstream_green" \
-      BAZELISK_HOME="$BAZELISK_HOME" \
-      bazelisk version 2>&1 | tee log
-
-  ! grep "Build label:" log || \
-      (echo "FAIL: 'bazelisk version' of an unreleased binary must not print a build label."; exit 1)
-}
-
 function test_BAZELISK_NOJDK() {
   setup
 
@@ -329,7 +318,7 @@ EOF
 function test_path_is_consistent_regardless_of_base_url() {
   setup
 
-  echo 6.2.0 > .bazelversion
+  echo 7.3.1 > .bazelversion
 
   cat >WORKSPACE <<EOF
 load("//:print_path.bzl", "print_path")
@@ -426,7 +415,7 @@ function test_bazel_verify_sha256() {
   local os="$(uname -s | tr A-Z a-z)"
   case "${os}" in
     darwin)
-      expected_sha256="6E9274042665163de666f13b87aBfE187a9127ed90fF1142bf42cfd45b32CD2b"
+      expected_sha256="dae351f491ead382bfc7c14d8957b9c8d735300c566c2161e34035eab994c1f2"
       ;;
     linux)
       expected_sha256="D93508529d41136065c7b1E5ff555fbfb9d18fd00e768886F2fc7dfb53b05B43"
@@ -508,6 +497,14 @@ echo "# test_bazel_version_from_environment"
 test_bazel_version_from_environment
 echo
 
+echo "# test_bazel_version_prefer_environment_to_bazeliskrc"
+test_bazel_version_prefer_environment_to_bazeliskrc
+echo
+
+echo "# test_bazel_version_from_workspace_bazeliskrc"
+test_bazel_version_from_workspace_bazeliskrc
+echo
+
 echo "# test_bazel_version_from_file"
 test_bazel_version_from_file
 echo
@@ -516,10 +513,6 @@ echo "# test_bazel_last_green"
 test_bazel_last_green
 echo
 
-echo "# test_bazel_last_downstream_green"
-test_bazel_last_downstream_green
-echo
-
 echo "# test_BAZELISK_NOJDK"
 test_BAZELISK_NOJDK
 echo
@@ -545,14 +538,6 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
   test_bazel_version_from_base_url
   echo
 
-  echo "# test_bazel_version_prefer_environment_to_bazeliskrc"
-  test_bazel_version_prefer_environment_to_bazeliskrc
-  echo
-
-  echo "# test_bazel_version_from_workspace_bazeliskrc"
-  test_bazel_version_from_workspace_bazeliskrc
-  echo
-
   echo "# test_bazel_version_from_user_home_bazeliskrc"
   test_bazel_version_from_user_home_bazeliskrc
   echo
@@ -577,10 +562,9 @@ if [[ $BAZELISK_VERSION == "GO" ]]; then
   test_bazel_prepend_binary_directory_to_path_go
   echo
 
-  # Disabled due to https://github.com/bazelbuild/bazelisk/pull/465#issuecomment-1805905082
-  # echo "# test_path_is_consistent_regardless_of_base_url"
-  # test_path_is_consistent_regardless_of_base_url
-  # echo
+  echo "# test_path_is_consistent_regardless_of_base_url"
+  test_path_is_consistent_regardless_of_base_url
+  echo
 
   case "$(uname -s)" in
     MSYS*)
diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go
index 5851640..1aec802 100644
--- a/bazelisk_version_test.go
+++ b/bazelisk_version_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"slices"
 	"strings"
 	"testing"
 
@@ -23,6 +24,16 @@ var (
 	tmpDir = ""
 )
 
+func TestGetInAscendingOrder(t *testing.T) {
+	input := []string{"7.0.1rc2", "6.1.0", "6.0.1", "10.11.12", "6.0.0", "7.0.1", "6.0.0rc2", "7.0.1rc1", "10.11.12rc1", "6.0.0rc1"}
+	want := []string{"6.0.0rc1", "6.0.0rc2", "6.0.0", "6.0.1", "6.1.0", "7.0.1rc1", "7.0.1rc2", "7.0.1", "10.11.12rc1", "10.11.12"}
+
+	got := versions.GetInAscendingOrder(input)
+	if !slices.Equal(got, want) {
+		t.Errorf("GetInAscendingOrder(): got %s, want %s", got, want)
+	}
+}
+
 func TestMain(m *testing.M) {
 	var err error
 	tmpDir, err = os.MkdirTemp("", "version_test")
@@ -41,7 +52,7 @@ func TestResolveVersion(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0", config.Null())
 
 	if err != nil {
@@ -59,7 +70,7 @@ func TestResolvePatchVersion(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "4.0.0-patch1", config.Null())
 
 	if err != nil {
@@ -81,7 +92,7 @@ func TestResolveLatestRcVersion(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null())
 
 	if err != nil {
@@ -99,7 +110,7 @@ func TestResolveLatestRcVersion_WithFullRelease(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, gcs, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "last_rc", config.Null())
 
 	if err != nil {
@@ -119,7 +130,7 @@ func TestResolveLatestVersion_TwoLatestVersionsDoNotHaveAReleaseYet(t *testing.T
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null())
 
 	if err != nil {
@@ -141,7 +152,7 @@ func TestResolveLatestVersion_ShouldOnlyReturnStableReleases(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null())
 
 	if err != nil {
@@ -153,6 +164,58 @@ func TestResolveLatestVersion_ShouldOnlyReturnStableReleases(t *testing.T) {
 	}
 }
 
+func TestResolveLatestAvoidsUnnecessaryRequests(t *testing.T) {
+	tests := []struct{
+		name string
+		specifiedVersion string
+		wantVersion string
+	} {
+		{
+			name: "Release",
+			specifiedVersion: "latest",
+			wantVersion: "7.0.0",
+		},
+		{
+			name: "Candidate",
+			specifiedVersion: "last_rc",
+			wantVersion: "7.0.0rc1",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T){
+		s := setUp(t)
+		s.AddVersion("4.0.0", true, []int{1}, nil)
+		s.AddVersion("5.0.0", true, []int{1}, nil)
+		s.AddVersion("6.0.0", true, []int{1}, nil)
+		s.AddVersion("7.0.0", true, []int{1}, nil)
+		s.AddVersion("8.0.0", false, nil, nil)
+		s.Finish()
+
+		gcs := &repositories.GCSRepo{}
+		repos := core.CreateRepositories(gcs, nil, nil, nil, false)
+		gotVersion, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, test.specifiedVersion, config.Null())
+
+		if err != nil {
+			t.Fatalf("Version resolution failed unexpectedly: %v", err)
+		}
+		if gotVersion != test.wantVersion {
+			t.Errorf("Expected version %s, but got %s", test.wantVersion, gotVersion)
+		}
+		/*
+		We expect three requests:
+		- One to get a list of all Bazel version tracks
+		- One to check for the existence of the 8.* release (candidates), which returns no results
+		- One to check for the existence of the 7.* release (candidates), which returns a match
+		*/
+		wantRequests := 3
+		if gotRequests := len(s.Transport.RequestedURLs); gotRequests != wantRequests {
+			t.Errorf("Expected exactly %d requests (one for the top-level, one for 8.0.0, one for 7.0.0), but got %d:\n%s", wantRequests, gotRequests, strings.Join(s.Transport.RequestedURLs, "\n"))
+		}
+	})
+	}
+}
+
 func TestResolveLatestVersion_ShouldFailIfNotEnoughReleases(t *testing.T) {
 	s := setUp(t)
 	s.AddVersion("3.0.0", true, nil, nil)
@@ -160,7 +223,7 @@ func TestResolveLatestVersion_ShouldFailIfNotEnoughReleases(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest-1", config.Null())
 
 	if err == nil {
@@ -177,7 +240,7 @@ func TestResolveLatestVersion_GCSIsDown(t *testing.T) {
 	g.Transport.AddResponse("https://www.googleapis.com/storage/v1/b/bazel/o?delimiter=/", 500, "", nil)
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(gcs, nil, nil, nil, nil, false)
+	repos := core.CreateRepositories(gcs, nil, nil, nil, false)
 	_, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, "latest", config.Null())
 
 	if err == nil {
@@ -194,7 +257,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) {
 	transport.AddResponse("https://api.github.com/repos/bazelbuild/bazel/releases", 500, "", nil)
 
 	gh := repositories.CreateGitHubRepo("test_token")
-	repos := core.CreateRepositories(nil, nil, gh, nil, nil, false)
+	repos := core.CreateRepositories(nil, gh, nil, nil, false)
 
 	_, _, err := repos.ResolveVersion(tmpDir, "some_fork", "latest", config.Null())
 
@@ -209,7 +272,7 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) {
 
 func TestAcceptRollingReleaseName(t *testing.T) {
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false)
+	repos := core.CreateRepositories(nil, nil, nil, gcs, false)
 
 	for _, version := range []string{"10.0.0-pre.20201103.4", "10.0.0-pre.20201103.4.2"} {
 		resolvedVersion, _, err := repos.ResolveVersion(tmpDir, "", version, config.Null())
@@ -231,7 +294,7 @@ func TestResolveLatestRollingRelease(t *testing.T) {
 	s.Finish()
 
 	gcs := &repositories.GCSRepo{}
-	repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false)
+	repos := core.CreateRepositories(nil, nil, nil, gcs, false)
 
 	version, _, err := repos.ResolveVersion(tmpDir, "", rollingReleaseIdentifier, config.Null())
 
@@ -245,26 +308,60 @@ 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", config.Null())
-
-	if err != nil {
-		t.Fatalf("Version resolution failed unexpectedly: %v", err)
+func TestAcceptTrackBasedReleaseVersions(t *testing.T) {
+	tests := []struct {
+		name             string
+		requestedVersion string
+		releaseExists    bool
+		wantVersion      string
+	}{
+		{
+			name:             "Floating_ReleaseExists",
+			requestedVersion: "4.x",
+			releaseExists:    true,
+			wantVersion:      "4.2.1",
+		},
+		{
+			name:             "Floating_NoRelease",
+			requestedVersion: "4.x",
+			releaseExists:    false,
+			wantVersion:      "4.2.0",
+		},
+		{
+			name:             "Wildcard_ReleaseExists",
+			requestedVersion: "4.*",
+			releaseExists:    true,
+			wantVersion:      "4.2.1",
+		},
+		{
+			name:             "Wildcard_NoRelease",
+			requestedVersion: "4.*",
+			releaseExists:    false,
+			wantVersion:      "4.2.1rc3",
+		},
 	}
-	expectedVersion := "4.2.1"
-	if version != expectedVersion {
-		t.Fatalf("Expected version %s, but got %s", expectedVersion, version)
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			s := setUp(t)
+			s.AddVersion("4.0.0", true, nil, []string{"4.0.0-pre.20210504.1"})
+			s.AddVersion("4.1.0", true, nil, nil)
+			s.AddVersion("4.2.0", true, nil, nil)
+			s.AddVersion("4.2.1", test.releaseExists, []int{1, 2, 3}, nil)
+			s.AddVersion("5.0.0", true, nil, nil)
+			s.Finish()
+
+			gcs := &repositories.GCSRepo{}
+			repos := core.CreateRepositories(gcs, nil, nil, nil, false)
+			version, _, err := repos.ResolveVersion(tmpDir, versions.BazelUpstream, test.requestedVersion, config.Null())
+
+			if err != nil {
+				t.Fatalf("Version resolution failed unexpectedly: %v", err)
+			}
+			if version != test.wantVersion {
+				t.Fatalf("Expected version %s, but got %s", test.wantVersion, version)
+			}
+		})
 	}
 }
 
diff --git a/httputil/httputil_test.go b/httputil/httputil_test.go
index 254e429..1a3f228 100644
--- a/httputil/httputil_test.go
+++ b/httputil/httputil_test.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"net/http"
 	"strconv"
+	"strings"
 	"testing"
 	"time"
 )
@@ -219,10 +220,13 @@ func TestDeadlineExceeded(t *testing.T) {
 		t.Fatal("Expected request to fail with code 500")
 	}
 
-	wanted := "could not fetch http://bar: unable to complete request to http://bar within 8s"
+	wantedPrefix := "could not fetch http://bar: unable to complete "
+	wantedSuffix := " requests to http://bar within 8s. Most recent failure: HTTP 500"
+
 	got := err.Error()
-	if wanted != got {
-		t.Fatalf("Expected error %q, but got %q", wanted, got)
+	sameError := strings.HasPrefix(got, wantedPrefix) && strings.HasSuffix(got, wantedSuffix)
+	if !sameError {
+		t.Fatalf("Expected error %q, but got %q", wantedPrefix+"???"+wantedSuffix, got)
 	}
 }
 
diff --git a/test.sh b/test.sh
index 811bf84..3614e21 100755
--- a/test.sh
+++ b/test.sh
@@ -9,7 +9,6 @@ env -u USE_BAZEL_VERSION ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="latest" ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="0.28.0" ./bin/bazelisk-darwin-amd64 version
 USE_BAZEL_VERSION="last_green" ./bin/bazelisk-darwin-"$arch" version
-USE_BAZEL_VERSION="last_downstream_green" ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="last_rc" ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="bazelbuild/latest" ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="bazelbuild/0.27.0" ./bin/bazelisk-darwin-amd64 version