Repository: bazelbuild/bazelisk
Pull request #289 (2022-01-11 21:18:24)

Teach custom fallback version behavior
This allows for smoother migrations from globally pinned Bazel to project-local pinned Bazel using Bazelisk.

sluongng: This should come with some documentation in README
bb010g: > This should come with some documentation in README

Done.
bb010g: Bump.
philwo: @bb010g It seems like an interesting feature, but it's not obvious to me how to use it - could you give an example how to use this in practice?
bb010g: @philwo My organization is currently using Bazel 3.3.0 via a pinned, non-Bazelisk installation in CI. That CI setup is used for multiple repositories, so a coordinated upgrade is difficult. We would like to move to Bazelisk-based pinning for these repositories and just install Bazelisk to CI. Without this patch, moving to Bazelisk means that every consumer of this CI installation has to set up `.bazelversion` files in every Bazel workspace before the move to avoid breaking their builds; this can be difficult with our current CI. With this patch, moving to Bazelisk means that CI can `export USE_BAZEL_FALLBACK_VERSION="warn:3.3.0"`, everyone's builds will still work as before by default, CI now tells you when you haven't set up a `.bazelversion` file (or alternatives) for a workspace, and teams can pin version 3.3.0 or whatever versions works for their projects in a decentralized manner.

In the future, after teams aren't seeing warnings about Bazel fallback versions in CI, CI can upgrade (for some or all runs) to `export USE_BAZEL_FALLBACK_VERSION="error:"` (hopefully without breaking any builds), ensuring that it's easy to figure out what version of Bazel any workspace uses.
bb010g: Additionally, on my developer setup, I can install Bazelisk and `export USE_BAZEL_FALLBACK_VERSION="warn:latest"` to keep Bazelisk handy when I need it but also avoid not pinning the Bazel version for a project workspace.
bb010g: @sluongng Re-requesting review. (GitHub's button isn't doing anything.)
philwo: Thank you for the detailed explanation! I think that's pretty cool. @fweikert WDYT?
bb010g: Bump.

Patch:
diff --git a/.bazelversion b/.bazelversion
index 0062ac9..ac14c3d 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1,1 +1,1 @@
-5.0.0
+5.1.1
diff --git a/README.md b/README.md
index ffa1164..f5838d5 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,11 @@ It uses a simple algorithm:
 - If the environment variable `USE_BAZEL_VERSION` is set, it will use the version specified in the value.
 - Otherwise, if a `.bazeliskrc` file exists in the workspace root and contains the `USE_BAZEL_VERSION` variable, this version will be used.
 - Otherwise, if a `.bazelversion` file exists in the current directory or recursively any parent directory, it will read the file and use the version specified in it.
-- Otherwise it will use the official latest Bazel release.
+- Otherwise, if the environment variable `USE_BAZEL_FALLBACK_VERSION` is set to one of the following formats:
+  - If set to a value starting with `error:`, it will report an error and version detection will fail.
+  - If set to a value starting with `warn:`, it will report a warning and use the version specified after the prefix.
+  - If set to a value starting with `silent:`, it will use the version specified after the prefix.
+- Otherwise, it will use the official latest Bazel release.
 
 A version can optionally be prefixed with a fork name.
 The fork and version should be separated by slash: `<FORK>/<VERSION>`.
diff --git a/bazelisk.go b/bazelisk.go
index 1aebe0d..5cdee07 100644
--- a/bazelisk.go
+++ b/bazelisk.go
@@ -26,9 +26,8 @@ import (
 func main() {
 	gcs := &repositories.GCSRepo{}
 	gitHub := repositories.CreateGitHubRepo(core.GetEnvOrConfig("BAZELISK_GITHUB_TOKEN"))
-	// Fetch LTS releases, release candidates and Bazel-at-commits from GCS, forks and rolling releases from GitHub.
-	// TODO(https://github.com/bazelbuild/bazelisk/issues/228): get rolling releases from GCS, too.
-	repos := core.CreateRepositories(gcs, gcs, gitHub, gcs, gitHub, true)
+	// 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)
 
 	exitCode, err := core.RunBazelisk(os.Args[1:], repos)
 	if err != nil {
diff --git a/core/core.go b/core/core.go
index 69ae736..cbc5fd5 100644
--- a/core/core.go
+++ b/core/core.go
@@ -14,7 +14,6 @@ import (
 	"os/signal"
 	"path/filepath"
 	"regexp"
-	"runtime"
 	"sort"
 	"strings"
 	"sync"
@@ -250,19 +249,30 @@ func findWorkspaceRoot(root string) string {
 	return findWorkspaceRoot(parentDirectory)
 }
 
+// TODO(go 1.18): remove backport of strings.Cut
+func cutString(s, sep string) (before, after string, found bool) {
+	if i := strings.Index(s, sep); i >= 0 {
+		return s[:i], s[i+len(sep):], true
+	}
+	return s, "", false
+}
+
 func getBazelVersion() (string, error) {
 	// Check in this order:
 	// - env var "USE_BAZEL_VERSION" is set to a specific version.
+	// - workspace_root/.bazeliskrc exists -> read contents, in contents:
+	//   var "USE_BAZEL_VERSION" is set to a specific version.
 	// - env var "USE_NIGHTLY_BAZEL" or "USE_BAZEL_NIGHTLY" is set -> latest
 	//   nightly. (TODO)
 	// - env var "USE_CANARY_BAZEL" or "USE_BAZEL_CANARY" is set -> latest
 	//   rc. (TODO)
 	// - the file workspace_root/tools/bazel exists -> that version. (TODO)
-	// - workspace_root/.bazeliskrc exists and contains a 'USE_BAZEL_VERSION'
-	//   variable -> read contents, that version.
 	// - workspace_root/.bazelversion exists -> read contents, that version.
 	// - workspace_root/WORKSPACE contains a version -> that version. (TODO)
-	// - fallback: latest release
+	// - env var "USE_BAZEL_FALLBACK_VERSION" is set to a fallback version format.
+	// - workspace_root/.bazeliskrc exists -> read contents, in contents:
+	//   var "USE_BAZEL_FALLBACK_VERSION" is set to a fallback version format.
+	// - fallback version format "silent:latest"
 	bazelVersion := GetEnvOrConfig("USE_BAZEL_VERSION")
 	if len(bazelVersion) != 0 {
 		return bazelVersion, nil
@@ -296,7 +306,25 @@ func getBazelVersion() (string, error) {
 		}
 	}
 
-	return "latest", nil
+	fallbackVersionFormat := GetEnvOrConfig("USE_BAZEL_FALLBACK_VERSION")
+	fallbackVersionMode, fallbackVersion, hasFallbackVersionMode := cutString(fallbackVersionFormat, ":")
+	if !hasFallbackVersionMode {
+		fallbackVersionMode, fallbackVersion, hasFallbackVersionMode = "silent", fallbackVersionMode, true
+	}
+	if len(fallbackVersion) == 0 {
+		fallbackVersion = "latest"
+	}
+	if fallbackVersionMode == "error" {
+		return "", fmt.Errorf("not allowed to use fallback version %q", fallbackVersion)
+	}
+	if fallbackVersionMode == "warn" {
+		log.Printf("Warning: used fallback version %q\n", fallbackVersion)
+		return fallbackVersion, nil
+	}
+	if fallbackVersionMode == "silent" {
+		return fallbackVersion, nil
+	}
+	return "", fmt.Errorf("invalid fallback version format %q (effectively %q)", fallbackVersionFormat, fmt.Sprintf("%s:%s", fallbackVersionMode, fallbackVersion))
 }
 
 func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error) {
@@ -438,9 +466,9 @@ func runBazel(bazel string, args []string, out io.Writer) (int, error) {
 	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
 	go func() {
 		s := <-c
-		if runtime.GOOS != "windows" {
-			cmd.Process.Signal(s)
-		} else {
+
+		// Only forward SIGTERM to our child process.
+		if s != os.Interrupt {
 			cmd.Process.Kill()
 		}
 	}()
diff --git a/platforms/platforms.go b/platforms/platforms.go
index 8e51ec7..6c6b4e8 100644
--- a/platforms/platforms.go
+++ b/platforms/platforms.go
@@ -13,7 +13,12 @@ var platforms = map[string]string{"darwin": "macos", "linux": "ubuntu1404", "win
 // GetPlatform returns a Bazel CI-compatible platform identifier for the current operating system.
 // TODO(fweikert): raise an error for unsupported platforms
 func GetPlatform() string {
-	return platforms[runtime.GOOS]
+	platform := platforms[runtime.GOOS]
+	arch := runtime.GOARCH
+	if arch == "arm64" {
+		platform = platform + "_arm64"
+	}
+	return platform
 }
 
 // DetermineExecutableFilenameSuffix returns the extension for binaries on the current operating system.
diff --git a/repositories/gcs.go b/repositories/gcs.go
index 82e33dd..170f172 100644
--- a/repositories/gcs.go
+++ b/repositories/gcs.go
@@ -239,3 +239,40 @@ func (gcs *GCSRepo) DownloadAtCommit(commit, destDir, destFile string) (string,
 	url := fmt.Sprintf("%s/%s/%s/bazel", nonCandidateBaseURL, platforms.GetPlatform(), commit)
 	return httputil.DownloadBinary(url, destDir, destFile)
 }
+
+// RollingRepo
+
+// GetRollingVersions returns a list of all available rolling release versions for the newest release.
+func (gcs *GCSRepo) GetRollingVersions(bazeliskHome string) ([]string, error) {
+	history, err := getVersionHistoryFromGCS()
+	if err != nil {
+		return []string{}, err
+	}
+
+	newest := history[len(history)-1]
+	versions, _, err := listDirectoriesInReleaseBucket(newest + "/rolling/")
+	if err != nil {
+		return []string{}, err
+	}
+
+	releases := make([]string, 0)
+	for _, v := range versions {
+		if !strings.Contains(v, "rc") {
+			releases = append(releases, strings.Split(v, "/")[2])
+		}
+	}
+
+	return releases, nil
+}
+
+// DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path.
+func (gcs *GCSRepo) DownloadRolling(version, destDir, destFile string) (string, error) {
+	srcFile, err := platforms.DetermineBazelFilename(version, true)
+	if err != nil {
+		return "", err
+	}
+
+	release_version := strings.Split(version, "-")[0]
+	url := fmt.Sprintf("%s/%s/rolling/%s/%s", candidateBaseURL, release_version, version, srcFile)
+	return httputil.DownloadBinary(url, destDir, destFile)
+}
diff --git a/repositories/github.go b/repositories/github.go
index 60baacb..b75edcd 100644
--- a/repositories/github.go
+++ b/repositories/github.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/bazelbuild/bazelisk/httputil"
 	"github.com/bazelbuild/bazelisk/platforms"
-	"github.com/bazelbuild/bazelisk/versions"
 )
 
 const (
@@ -89,14 +88,3 @@ func (gh *GitHubRepo) DownloadVersion(fork, version, destDir, destFile string) (
 	url := fmt.Sprintf(urlPattern, fork, version, filename)
 	return httputil.DownloadBinary(url, destDir, destFile)
 }
-
-// GetRollingVersions returns a list of all available rolling release versions.
-func (gh *GitHubRepo) GetRollingVersions(bazeliskHome string) ([]string, error) {
-	// Release candidates are uploaded to GCS only, which means that all prerelease binaries on GitHub belong to rolling releases.
-	return gh.getFilteredVersions(bazeliskHome, versions.BazelUpstream, true)
-}
-
-// DownloadRolling downloads the given Bazel version into the specified location and returns the absolute path.
-func (gh *GitHubRepo) DownloadRolling(version, destDir, destFile string) (string, error) {
-	return gh.DownloadVersion(versions.BazelUpstream, version, destDir, destFile)
-}


Test patch:
diff --git a/bazelisk_version_test.go b/bazelisk_version_test.go
index b754f81..bd83cf7 100644
--- a/bazelisk_version_test.go
+++ b/bazelisk_version_test.go
@@ -208,8 +208,8 @@ func TestResolveLatestVersion_GitHubIsDown(t *testing.T) {
 }
 
 func TestAcceptRollingReleaseName(t *testing.T) {
-	gh := repositories.CreateGitHubRepo("test_token")
-	repos := core.CreateRepositories(nil, nil, nil, nil, gh, false)
+	gcs := &repositories.GCSRepo{}
+	repos := core.CreateRepositories(nil, 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)
@@ -225,31 +225,13 @@ func TestAcceptRollingReleaseName(t *testing.T) {
 }
 
 func TestResolveLatestRollingRelease(t *testing.T) {
-	text := `
-	[
-	  {
-		"tag_name": "4.0.0",
-		"prerelease": false
-	  },
-	  {
-		"tag_name": "5.0.0-pre.20210319.1",
-		"prerelease": true
-	  },
-	  {
-		"tag_name": "5.0.0-pre.20210322.4",
-		"prerelease": true
-	  },
-	  {
-		"tag_name": "5.0.0",
-		"prerelease": false
-	  }
-	]
-	`
-	transport := installTransport()
-	transport.AddResponse("https://api.github.com/repos/bazelbuild/bazel/releases", 200, text, nil)
+	s := setUp(t)
+	s.AddVersion("11.0.0", false, nil, []string{"11.0.0/rolling/11.0.0-pre.20210503.1"})
+	s.AddVersion("12.0.0", false, nil, []string{"12.0.0/rolling/12.0.0-pre.20210504.1"})
+	s.Finish()
 
-	gh := repositories.CreateGitHubRepo("test_token")
-	repos := core.CreateRepositories(nil, nil, nil, nil, gh, false)
+	gcs := &repositories.GCSRepo{}
+	repos := core.CreateRepositories(nil, nil, nil, nil, gcs, false)
 
 	version, _, err := repos.ResolveVersion(tmpDir, "", rollingReleaseIdentifier)
 
@@ -257,7 +239,7 @@ func TestResolveLatestRollingRelease(t *testing.T) {
 		t.Fatalf("ResolveVersion(%q, \"\", %q): expected no error, but got %v", tmpDir, rollingReleaseIdentifier, err)
 	}
 
-	want := "5.0.0-pre.20210322.4"
+	want := "12.0.0-pre.20210504.1"
 	if version != want {
 		t.Fatalf("ResolveVersion(%q, \"\", %q) = %v, but expected %v", tmpDir, rollingReleaseIdentifier, version, want)
 	}
@@ -308,9 +290,7 @@ func (g *gcsSetup) AddVersion(version string, hasRelease bool, rcs []int, rollin
 		register(fmt.Sprintf("rc%d", rc))
 	}
 
-	for _, r := range rolling {
-		register(r)
-	}
+	g.addURL(fmt.Sprintf("%s/rolling/", version), false, rolling...)
 
 	// The /release/ URLs have to exist, even if there is no release. In this case GCS returns no items, though.
 	releasePrefix := fmt.Sprintf("%s/release/", version)
diff --git a/test.sh b/test.sh
index 074086d..811bf84 100755
--- a/test.sh
+++ b/test.sh
@@ -3,13 +3,15 @@
 set -euxo pipefail
 
 rm -rf "$HOME/Library/Caches/bazelisk"
-env -u USE_BAZEL_VERSION ./bin/bazelisk-darwin-amd64 version
-USE_BAZEL_VERSION="latest" ./bin/bazelisk-darwin-amd64 version
+
+arch=$(uname -m)
+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-amd64 version
-USE_BAZEL_VERSION="last_downstream_green" ./bin/bazelisk-darwin-amd64 version
-USE_BAZEL_VERSION="last_rc" ./bin/bazelisk-darwin-amd64 version
-USE_BAZEL_VERSION="bazelbuild/latest" ./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
-USE_BAZEL_VERSION="philwo/latest" ./bin/bazelisk-darwin-amd64 version
+USE_BAZEL_VERSION="philwo/latest" ./bin/bazelisk-darwin-"$arch" version
 USE_BAZEL_VERSION="philwo/0.25.0" ./bin/bazelisk-darwin-amd64 version