cmd/coordinator, maintner/maintnerd/maintapi: consolidate TryBot branch logic

This change reverts the coordinator side to be much simpler, as it was
before CL 167382, and consolidates all the version selection in maintapi.
This direction is chosen because at this time¹ maintapi is best suited
to perform version selection.

This is a refactor CL that leaves current behavior unmodified, and test
cases provide improved coverage. The following smaller CL will make the
desired changes to behavior.

Remove TestNewTrySetBuildRepoGo110 because it's no longer needed. It was
added when x/build started requiring module mode to build successfully,
given that Go 1.10 didn't have module mode support.

Background

At this time¹ coordinator makes use of maintapi to find TryBot work,
and it is a collaborate effort between the two components that results
in determining what builds will happen. Coordinator is ultimately
responsible for starting and running the builds, but it doesn't have
information about the branches in the Go project (the Go revision at
refs/heads/master, refs/heads/release-branch.go1.16, etc.). Maintapi
has that information via the maintner corpus. So it makes the version
information available to coordinator by populating relevant fields in
apipb.GoFindTryWorkResponse.

Issue golang/go#28891 was about wanting to test golang.org/x repo CLs
on release-branch.go1.n with the corresponding Go 1.n version, rather
than Go tip. Unfortunately, it was implemented on the coordinator side,
resulting in the logic for version selection to be more spread between
the coordinator and maintapi components. There were followup issues
like golang/go#42127 and golang/go#37512, whose fixes built on top of
the coordinator side, and increased complexity there. As a consequence,
making and testing further changes became more difficult than it needs
to be.

¹ After golang/go#34744 is done, I'd like to move all the TryBot version
  selection logic into coordinator, the component responsible for TryBots.
  But not today.

For golang/go#46154.

Change-Id: I93986acefd4bf66b27ccf0323439966122b7989a
Reviewed-on: https://go-review.googlesource.com/c/build/+/319789
Trust: Dmitri Shuralyov <dmitshur@golang.org>
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 05bbccf..b5056ef 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -1218,26 +1218,6 @@
 		work.GoVersion = []*apipb.MajorMinor{{}}
 	}
 
-	// GoCommit is non-empty for x/* repos (aka "subrepos"). It
-	// is the Go revision to use to build & test the x/* repo
-	// with. The first element is the master branch. We test the
-	// master branch against all the normal builders configured to
-	// do subrepos. Any GoCommit values past the first are for older
-	// release branches, but we use a limited subset of builders for those.
-	var goRev string
-	if len(work.GoCommit) > 0 {
-		// By default, use the first GoCommit, which represents Go tip (master branch).
-		goRev = work.GoCommit[0]
-	}
-	for i, goBranch := range work.GoBranch {
-		// There are two cases where we want to change goRev to work.GoCommit[i]:
-		// 1. CL branch is like "master" or "release-branch.go1.15" and matches the Go branch exactly.
-		// 2. CL branch is like "release-branch.go1.15-suffix" and its prefix matches.
-		if work.Branch == goBranch || strings.HasPrefix(work.Branch, goBranch+"-") {
-			goRev = work.GoCommit[i]
-		}
-	}
-
 	addBuilderToSet := func(bs *buildStatus, brev buildgo.BuilderRev) {
 		bs.trySet = ts
 		status[brev] = bs
@@ -1252,6 +1232,15 @@
 		go ts.awaitTryBuild(idx, bs, brev)
 	}
 
+	var mainBuildGoCommit string
+	if key.Project != "go" && len(work.GoCommit) > 0 {
+		// work.GoCommit is non-empty when work.Project != "go".
+		// For the main build, use the first GoCommit, which represents Go tip (master branch).
+		mainBuildGoCommit = work.GoCommit[0]
+	}
+
+	// Start the main TryBot build using the selected builders.
+	// There may be additional builds, those are handled below.
 	if !testingKnobSkipBuilds {
 		go ts.notifyStarting()
 	}
@@ -1260,7 +1249,7 @@
 		if goVersion.Less(bconf.MinimumGoVersion) {
 			continue
 		}
-		brev := tryKeyToBuilderRev(bconf.Name, key, goRev)
+		brev := tryKeyToBuilderRev(bconf.Name, key, mainBuildGoCommit)
 		bs, err := newBuild(brev, noCommitDetail)
 		if err != nil {
 			log.Printf("can't create build for %q: %v", brev, err)
@@ -1269,17 +1258,16 @@
 		addBuilderToSet(bs, brev)
 	}
 
-	// For subrepos on the "master" branch, test against prior releases of Go too.
-	if key.Project != "go" && key.Branch == "master" {
-		// linuxBuilder is the standard builder we run for when testing x/* repos against
-		// the past two Go releases.
+	// If this is a golang.org/x repo and there's more than one GoCommit,
+	// that means we're testing against prior releases of Go too.
+	// The version selection logic is currently in maintapi's GoFindTryWork implementation.
+	if key.Project != "go" && len(work.GoCommit) >= 2 {
+		// linuxBuilder is the standard builder for this purpose.
 		linuxBuilder := dashboard.Builders["linux-amd64"]
 
-		// If there's more than one GoCommit, that means this is an x/* repo
-		// and we're testing against previous releases of Go.
 		for i, goRev := range work.GoCommit {
 			if i == 0 {
-				// Skip the i==0 element, which is handled above.
+				// Skip the i==0 element, which was already handled above.
 				continue
 			}
 			branch := work.GoBranch[i]
diff --git a/cmd/coordinator/coordinator_test.go b/cmd/coordinator/coordinator_test.go
index 3d83d71..4910850 100644
--- a/cmd/coordinator/coordinator_test.go
+++ b/cmd/coordinator/coordinator_test.go
@@ -161,23 +161,23 @@
 func TestIssue28891(t *testing.T) {
 	testingKnobSkipBuilds = true
 
-	work := &apipb.GerritTryWorkItem{ // Roughly based on https://go-review.googlesource.com/c/tools/+/150577/1.
-		Project:   "tools",
-		Branch:    "release-branch.go1.11",
-		ChangeId:  "Ice719ab807ce3922b885a800ac873cdbf165a8f7",
-		Commit:    "9d66f1bfdbed72f546df963194a19d56180c4ce7",
-		GoCommit:  []string{"a2e79571a9d3dbe3cf10dcaeb1f9c01732219869", "e39e43d7349555501080133bb426f1ead4b3ef97", "f5ff72d62301c4e9d0a78167fab5914ca12919bd"},
-		GoBranch:  []string{"master", "release-branch.go1.11", "release-branch.go1.10"},
-		GoVersion: []*apipb.MajorMinor{{1, 12}, {1, 11}, {1, 10}},
+	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/net CL 258478.
+		Project:   "net",
+		Branch:    "release-branch.go1.15",
+		ChangeId:  "I546597cedf3715e6617babcb3b62140bf1857a27",
+		Commit:    "a5fa9d4b7c91aa1c3fecbeb6358ec1127b910dd6",
+		GoCommit:  []string{"72ccabc99449b2cb5bb1438eb90244d55f7b02f5"},
+		GoBranch:  []string{"release-branch.go1.15"},
+		GoVersion: []*apipb.MajorMinor{{1, 15}},
 	}
 	ts := newTrySet(work)
 	if len(ts.builds) == 0 {
 		t.Fatal("no builders in try set, want at least 1")
 	}
 	for i, bs := range ts.builds {
-		const go111Revision = "e39e43d7349555501080133bb426f1ead4b3ef97"
-		if bs.BuilderRev.Rev != go111Revision {
-			t.Errorf("build[%d]: %s: x/tools on release-branch.go1.11 branch should be tested with Go 1.11, but isn't", i, bs.NameAndBranch())
+		const go115Revision = "72ccabc99449b2cb5bb1438eb90244d55f7b02f5"
+		if bs.BuilderRev.Rev != go115Revision {
+			t.Errorf("build[%d]: %s: x/net on release-branch.go1.15 branch should be tested with Go 1.15, but isn't", i, bs.NameAndBranch())
 		}
 	}
 }
@@ -188,63 +188,40 @@
 func TestIssue42127(t *testing.T) {
 	testingKnobSkipBuilds = true
 
-	work := &apipb.GerritTryWorkItem{ // Roughly based on https://go-review.googlesource.com/c/net/+/264058/1.
+	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/net CL 264058.
 		Project:   "net",
 		Branch:    "release-branch.go1.15-bundle",
 		ChangeId:  "I546597cedf3715e6617babcb3b62140bf1857a27",
-		Commit:    "286322bb8662ddff3686e42a01c33a1d47d25153",
-		GoCommit:  []string{"b2a8317b31d652b3ee293a313269b8290bcdf96c", "3b1f07fff774f86f13316f7bec6552566568fc10", "768b64711ae4292bd9a02c9cc8d44282f5fac66b"},
-		GoBranch:  []string{"master", "release-branch.go1.15", "release-branch.go1.14"},
-		GoVersion: []*apipb.MajorMinor{{1, 16}, {1, 15}, {1, 14}},
+		Commit:    "abf26a14a65b111d492067f407f32455c5b1048c",
+		GoCommit:  []string{"72ccabc99449b2cb5bb1438eb90244d55f7b02f5"},
+		GoBranch:  []string{"release-branch.go1.15"},
+		GoVersion: []*apipb.MajorMinor{{1, 15}},
 	}
 	ts := newTrySet(work)
 	if len(ts.builds) == 0 {
 		t.Fatal("no builders in try set, want at least 1")
 	}
 	for i, bs := range ts.builds {
-		const go115Revision = "3b1f07fff774f86f13316f7bec6552566568fc10"
+		const go115Revision = "72ccabc99449b2cb5bb1438eb90244d55f7b02f5"
 		if bs.BuilderRev.Rev != go115Revision {
 			t.Errorf("build[%d]: %s: x/net on release-branch.go1.15-bundle branch should be tested with Go 1.15, but isn't", i, bs.NameAndBranch())
 		}
 	}
 }
 
-// tests that we don't test Go 1.10 for the build repo
-func TestNewTrySetBuildRepoGo110(t *testing.T) {
-	testingKnobSkipBuilds = true
-
-	work := &apipb.GerritTryWorkItem{
-		Project:   "build",
-		Branch:    "master",
-		ChangeId:  "I6f05da2186b38dc8056081252563a82c50f0ce05",
-		Commit:    "a62e6a3ab11cc9cc2d9e22a50025dd33fc35d22f",
-		GoCommit:  []string{"a2e79571a9d3dbe3cf10dcaeb1f9c01732219869", "e39e43d7349555501080133bb426f1ead4b3ef97", "f5ff72d62301c4e9d0a78167fab5914ca12919bd"},
-		GoBranch:  []string{"master", "release-branch.go1.11", "release-branch.go1.10"},
-		GoVersion: []*apipb.MajorMinor{{1, 12}, {1, 11}, {1, 10}},
-	}
-	ts := newTrySet(work)
-	for i, bs := range ts.builds {
-		v := bs.NameAndBranch()
-		if strings.Contains(v, "Go 1.10.x") {
-			t.Errorf("unexpected builder: %v", v)
-		}
-		t.Logf("build[%d]: %s", i, v)
-	}
-}
-
 // Tests that TryBots run on branches of the x/ repositories, other than
 // "master" and "release-branch.go1.N". See golang.org/issue/37512.
 func TestXRepoBranches(t *testing.T) {
 	testingKnobSkipBuilds = true
 
-	work := &apipb.GerritTryWorkItem{
+	work := &apipb.GerritTryWorkItem{ // Based on what maintapi's GoFindTryWork does for x/tools CL 227356.
 		Project:   "tools",
 		Branch:    "gopls-release-branch.0.4",
 		ChangeId:  "Ica799fcf117bf607c0c59f41b08a78552339dc53",
-		Commit:    "6af4ce83c61d0f3e616b410b53b51982798c4d73",
-		GoVersion: []*apipb.MajorMinor{{1, 15}},
-		GoCommit:  []string{"74d6de03fd7db2c6faa7794620a9bcf0c4f018f2"},
+		Commit:    "13af72af5ccdfe6f1e75b57b02cfde3bb0a77a76",
+		GoCommit:  []string{"9995c6b50aa55c1cc1236d1d688929df512dad53"},
 		GoBranch:  []string{"master"},
+		GoVersion: []*apipb.MajorMinor{{1, 17}},
 	}
 	ts := newTrySet(work)
 	for i, bs := range ts.builds {
diff --git a/maintner/maintnerd/maintapi/api.go b/maintner/maintnerd/maintapi/api.go
index 8b366dc..08512cd 100644
--- a/maintner/maintnerd/maintapi/api.go
+++ b/maintner/maintnerd/maintapi/api.go
@@ -104,7 +104,14 @@
 
 // tryWorkItem creates a GerritTryWorkItem for
 // the Gerrit CL specified by cl, ci, comments.
-func tryWorkItem(cl *maintner.GerritCL, ci *gerrit.ChangeInfo, comments map[string][]gerrit.CommentInfo) *apipb.GerritTryWorkItem {
+//
+// goProj is the state of the main Go repository.
+// develVersion is the version of Go in development at HEAD of master branch.
+// supportedReleases are the supported Go releases per https://golang.org/doc/devel/release.html#policy.
+func tryWorkItem(
+	cl *maintner.GerritCL, ci *gerrit.ChangeInfo, comments map[string][]gerrit.CommentInfo,
+	goProj refer, develVersion apipb.MajorMinor, supportedReleases []*apipb.GoRelease,
+) (*apipb.GerritTryWorkItem, error) {
 	w := &apipb.GerritTryWorkItem{
 		Project:  cl.Project.Project(),
 		Branch:   strings.TrimPrefix(cl.Branch(), "refs/heads/"),
@@ -116,6 +123,7 @@
 		w.Commit = ci.CurrentRevision
 		w.Version = int32(ci.Revisions[ci.CurrentRevision].PatchSetNumber)
 	}
+
 	// Look for "TRY=" comments. Only consider messages that are accompanied
 	// by a Run-TryBot+1 vote, as a way of confirming the comment author has
 	// Trybot Access (see https://golang.org/wiki/GerritAccess#trybot-access-may-start-trybots).
@@ -153,7 +161,64 @@
 			})
 		}
 	}
-	return w
+
+	// Populate GoCommit, GoBranch, GoVersion fields
+	// according to what's being tested. Coordinator
+	// will use these to run corresponding tests.
+	if w.Project == "go" {
+		// TryBot on Go repo. Set the GoVersion field based on branch name.
+		if major, minor, ok := parseReleaseBranchVersion(w.Branch); ok {
+			// A release branch like release-branch.goX.Y.
+			// Use the major-minor Go version determined from the branch name.
+			w.GoVersion = []*apipb.MajorMinor{{major, minor}}
+		} else {
+			// A branch that is not release-branch.goX.Y: maybe
+			// "master" or a development branch like "dev.link".
+			// There isn't a way to determine the version from its name,
+			// so use the development Go version until we need to do more.
+			// TODO(golang.org/issue/42376): This can be made more precise.
+			w.GoVersion = []*apipb.MajorMinor{&develVersion}
+		}
+	} else {
+		// TryBot on a subrepo.
+		trimBundleSuffix := func(branch string) string {
+			// There was only one branch with a suffix, release-branch.go1.15-bundle in x/net, so just hardcode it.
+			// TODO: This special case can be removed when Go 1.17 is out and 1.15 is no longer supported.
+			return strings.TrimSuffix(branch, "-bundle")
+		}
+		if major, minor, ok := parseReleaseBranchVersion(trimBundleSuffix(w.Branch)); ok {
+			// An release-branch.goX.Y (or one with a -suffix) branch is used for internal needs
+			// of goX.Y only, so no reason to test it on other Go versions.
+			goBranch := fmt.Sprintf("release-branch.go%d.%d", major, minor)
+			goCommit := goProj.Ref("refs/heads/" + goBranch)
+			if goCommit == "" {
+				return nil, fmt.Errorf("branch %q doesn't exist", goBranch)
+			}
+			w.GoCommit = []string{goCommit.String()}
+			w.GoBranch = []string{goBranch}
+			w.GoVersion = []*apipb.MajorMinor{{major, minor}}
+		} else if w.Branch == "master" {
+			// For subrepos on the "master" branch, use the default policy
+			// of testing it with Go tip and the supported releases.
+			w.GoCommit = []string{goProj.Ref("refs/heads/master").String()}
+			w.GoBranch = []string{"master"}
+			w.GoVersion = []*apipb.MajorMinor{&develVersion}
+			for _, r := range supportedReleases {
+				w.GoCommit = append(w.GoCommit, r.BranchCommit)
+				w.GoBranch = append(w.GoBranch, r.BranchName)
+				w.GoVersion = append(w.GoVersion, &apipb.MajorMinor{r.Major, r.Minor})
+			}
+		} else {
+			// A branch that is neither release-branch.goX.Y nor "master":
+			// maybe some custom branch like "dev.go2go".
+			// Test it against Go tip only until we want to do more.
+			w.GoCommit = []string{goProj.Ref("refs/heads/master").String()}
+			w.GoBranch = []string{"master"}
+			w.GoVersion = []*apipb.MajorMinor{&develVersion}
+		}
+	}
+
+	return w, nil
 }
 
 func firstLine(s string) string {
@@ -251,7 +316,8 @@
 		return nil, err
 	}
 	// If Go X.Y is the latest supported release, the version in development is likely Go X.(Y+1).
-	develVersion := &apipb.MajorMinor{
+	// TODO(golang.org/issue/42376): This can be made more precise.
+	develVersion := apipb.MajorMinor{
 		Major: supportedReleases[0].Major,
 		Minor: supportedReleases[0].Minor + 1,
 	}
@@ -273,31 +339,10 @@
 		if err != nil {
 			return nil, fmt.Errorf("gerritc.ListChangeComments(ctx, %q): %v", changeID, err)
 		}
-		work := tryWorkItem(cl, ci, comments)
-		if work.Project == "go" {
-			// Trybot on Go repo. Set the GoVersion field based on branch name.
-			if major, minor, ok := parseReleaseBranchVersion(work.Branch); ok {
-				// A release branch like release-branch.goX.Y.
-				// Use the major-minor Go version determined from the branch name.
-				work.GoVersion = []*apipb.MajorMinor{{major, minor}}
-			} else {
-				// A branch that is not release-branch.goX.Y: maybe
-				// "master" or a development branch like "dev.link".
-				// There isn't a way to determine the version from its name,
-				// so use the development Go version until we need to do more.
-				// TODO(golang.org/issue/42376): This can be made more precise.
-				work.GoVersion = []*apipb.MajorMinor{develVersion}
-			}
-		} else {
-			// Trybot on a subrepo. Set the Go fields to master and the supported releases.
-			work.GoCommit = []string{goProj.Ref("refs/heads/master").String()}
-			work.GoBranch = []string{"master"}
-			work.GoVersion = []*apipb.MajorMinor{develVersion}
-			for _, r := range supportedReleases {
-				work.GoCommit = append(work.GoCommit, r.BranchCommit)
-				work.GoBranch = append(work.GoBranch, r.BranchName)
-				work.GoVersion = append(work.GoVersion, &apipb.MajorMinor{r.Major, r.Minor})
-			}
+		work, err := tryWorkItem(cl, ci, comments, goProj, develVersion, supportedReleases)
+		if err != nil {
+			log.Printf("goFindTryWork: skipping CL %v because %v\n", ci.ChangeNumber, err)
+			continue
 		}
 		res.Waiting = append(res.Waiting, work)
 	}
@@ -355,6 +400,17 @@
 	}, nil
 }
 
+// refer is implemented by *maintner.GerritProject,
+// or something that acts like it for testing.
+type refer interface {
+	// Ref returns a non-change ref, such as "HEAD", "refs/heads/master",
+	// or "refs/tags/v0.8.0",
+	// Change refs of the form "refs/changes/*" are not supported.
+	// The returned hash is the zero value (an empty string) if the ref
+	// does not exist.
+	Ref(ref string) maintner.GitHash
+}
+
 // nonChangeRefLister is implemented by *maintner.GerritProject,
 // or something that acts like it for testing.
 type nonChangeRefLister interface {
diff --git a/maintner/maintnerd/maintapi/api_test.go b/maintner/maintnerd/maintapi/api_test.go
index 93d16f8..fb62e0e 100644
--- a/maintner/maintnerd/maintapi/api_test.go
+++ b/maintner/maintnerd/maintapi/api_test.go
@@ -124,6 +124,32 @@
 
 func TestTryWorkItem(t *testing.T) {
 	c := getGoData(t)
+	goProj := gerritProject{
+		refs: []refHash{
+			{"refs/heads/master", gitHash("9995c6b50aa55c1cc1236d1d688929df512dad53")},
+			{"refs/heads/release-branch.go1.16", gitHash("e67a58b7cb2b228e04477dfdb1aacd8348e63534")},
+			{"refs/heads/release-branch.go1.15", gitHash("72ccabc99449b2cb5bb1438eb90244d55f7b02f5")},
+		},
+	}
+	develVersion := apipb.MajorMinor{
+		Major: 1, Minor: 17,
+	}
+	supportedReleases := []*apipb.GoRelease{
+		{
+			Major: 1, Minor: 16, Patch: 3,
+			TagName:      "go1.16.3",
+			TagCommit:    "9baddd3f21230c55f0ad2a10f5f20579dcf0a0bb",
+			BranchName:   "release-branch.go1.16",
+			BranchCommit: "e67a58b7cb2b228e04477dfdb1aacd8348e63534",
+		},
+		{
+			Major: 1, Minor: 15, Patch: 11,
+			TagName:      "go1.15.11",
+			TagCommit:    "8c163e85267d146274f68854fe02b4a495586584",
+			BranchName:   "release-branch.go1.15",
+			BranchCommit: "72ccabc99449b2cb5bb1438eb90244d55f7b02f5",
+		},
+	}
 	tests := []struct {
 		proj     string
 		clnum    int32
@@ -132,10 +158,32 @@
 		want     string
 	}{
 		// Same Change-Id, different branch:
-		{"go", 51430, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"master" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"45a4609c0ae214e448612e0bc0846e2f2682f1b2" `},
-		{"go", 51450, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"release-branch.go1.9" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"7320506bc58d3a55eff2c67b2ec65cfa94f7b0a7" `},
+		{"go", 51430, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"master" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"45a4609c0ae214e448612e0bc0846e2f2682f1b2" go_version:<major:1 minor:17 > `},
+		{"go", 51450, &gerrit.ChangeInfo{}, nil, `project:"go" branch:"release-branch.go1.9" change_id:"I0bcae339624e7d61037d9ea0885b7bd07491bbb6" commit:"7320506bc58d3a55eff2c67b2ec65cfa94f7b0a7" go_version:<major:1 minor:9 > `},
 		// Different project:
-		{"build", 51432, &gerrit.ChangeInfo{}, nil, `project:"build" branch:"master" change_id:"I1f71836da7008e58d3e76e2cc3170e96cd57ddf6" commit:"9251bc9950baff61d95da0761e2e4bfab61ed210" `},
+		{"build", 51432, &gerrit.ChangeInfo{}, nil, `project:"build" branch:"master" change_id:"I1f71836da7008e58d3e76e2cc3170e96cd57ddf6" commit:"9251bc9950baff61d95da0761e2e4bfab61ed210" ` +
+			// Tested on tip and two supported releases.
+			`go_commit:"9995c6b50aa55c1cc1236d1d688929df512dad53" go_commit:"e67a58b7cb2b228e04477dfdb1aacd8348e63534" go_commit:"72ccabc99449b2cb5bb1438eb90244d55f7b02f5" ` +
+			`go_branch:"master" go_branch:"release-branch.go1.16" go_branch:"release-branch.go1.15" ` +
+			`go_version:<major:1 minor:17 > go_version:<major:1 minor:16 > go_version:<major:1 minor:15 > `},
+
+		// Test that a golang.org/x repo TryBot on a branch like
+		// "release-branch.go1.N" or "release-branch.go1.N-suffix"
+		// tests with Go 1.N (rather than tip + two supported releases).
+		// See issues 28891 and 42127.
+		{"net", 314649, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"internal-branch.go1.16-vendor" change_id:"I2c54ce3b2acf1c5efdea66db0595b93a3f5ae5f3" commit:"3f4a416c7d3b3b41375d159f71ff0a801fc0102b" ` +
+			`go_commit:"9995c6b50aa55c1cc1236d1d688929df512dad53" go_branch:"master" go_version:<major:1 minor:17 > `}, // TODO(golang.org/issue/46154): This should be tested with Go 1.16, not tip.
+		{"net", 258478, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"release-branch.go1.15" change_id:"I546597cedf3715e6617babcb3b62140bf1857a27" commit:"a5fa9d4b7c91aa1c3fecbeb6358ec1127b910dd6" ` +
+			`go_commit:"72ccabc99449b2cb5bb1438eb90244d55f7b02f5" go_branch:"release-branch.go1.15" go_version:<major:1 minor:15 > `},
+		{"net", 264058, &gerrit.ChangeInfo{}, nil, `project:"net" branch:"release-branch.go1.15-bundle" change_id:"I546597cedf3715e6617babcb3b62140bf1857a27" commit:"abf26a14a65b111d492067f407f32455c5b1048c" ` +
+			`go_commit:"72ccabc99449b2cb5bb1438eb90244d55f7b02f5" go_branch:"release-branch.go1.15" go_version:<major:1 minor:15 > `},
+
+		// Test that TryBots run on branches of the x/ repositories, other than
+		// "master" and "release-branch.go1.N". See issue 37512.
+		{"tools", 227356, &gerrit.ChangeInfo{}, nil, `project:"tools" branch:"gopls-release-branch.0.4" change_id:"Ica799fcf117bf607c0c59f41b08a78552339dc53" commit:"13af72af5ccdfe6f1e75b57b02cfde3bb0a77a76" ` +
+			`go_commit:"9995c6b50aa55c1cc1236d1d688929df512dad53" go_branch:"master" go_version:<major:1 minor:17 > `},
+		{"tools", 238259, &gerrit.ChangeInfo{}, nil, `project:"tools" branch:"dev.go2go" change_id:"I24950593b517af011a636966cb98b9652d2c4134" commit:"76e917206452e73dc28cbeb58a15ea8f30487263" ` +
+			`go_commit:"9995c6b50aa55c1cc1236d1d688929df512dad53" go_branch:"master" go_version:<major:1 minor:17 > `},
 
 		// With comments:
 		{
@@ -177,7 +225,7 @@
 					},
 				},
 			},
-			want: `project:"go" branch:"master" change_id:"I358eb7b11768df8c80fb7e805abd4cd01d52bb9b" commit:"f99d33e72efdea68fce39765bc94479b5ebed0a9" version:88 try_message:<message:"foo" author_id:1234 version:1 > try_message:<message:"bar, baz" author_id:5678 version:2 > `,
+			want: `project:"go" branch:"master" change_id:"I358eb7b11768df8c80fb7e805abd4cd01d52bb9b" commit:"f99d33e72efdea68fce39765bc94479b5ebed0a9" version:88 go_version:<major:1 minor:17 > try_message:<message:"foo" author_id:1234 version:1 > try_message:<message:"bar, baz" author_id:5678 version:2 > `,
 		},
 	}
 	for _, tt := range tests {
@@ -186,9 +234,25 @@
 			t.Errorf("CL %d in %s not found", tt.clnum, tt.proj)
 			continue
 		}
-		got := fmt.Sprint(tryWorkItem(cl, tt.ci, tt.comments))
-		if got != tt.want {
-			t.Errorf("tryWorkItem(%q, %v) mismatch:\n got: %#q\nwant: %#q", tt.proj, tt.clnum, got, tt.want)
+		work, err := tryWorkItem(cl, tt.ci, tt.comments, goProj, develVersion, supportedReleases)
+		if err != nil {
+			t.Errorf("tryWorkItem(%q, %v, ...): err=%v", tt.proj, tt.clnum, err)
+			continue
+		}
+		if len(work.GoVersion) == 0 {
+			t.Errorf("tryWorkItem(%q, %v, ...): len(GoVersion) is zero, want at least one", tt.proj, tt.clnum)
+		}
+		if work.Project != "go" && (len(work.GoCommit) == 0 || len(work.GoBranch) == 0) {
+			t.Errorf("tryWorkItem(%q, %v, ...): GoCommit/GoBranch slice is empty for x/ repo, want both non-empty", tt.proj, tt.clnum)
+		}
+		if len(work.GoBranch) != len(work.GoCommit) {
+			t.Errorf("tryWorkItem(%q, %v, ...): bad correlation between GoBranch and GoCommit slices", tt.proj, tt.clnum)
+		}
+		if ok := len(work.GoVersion) == len(work.GoCommit) || (len(work.GoVersion) == 1 && len(work.GoCommit) == 0); !ok {
+			t.Errorf("tryWorkItem(%q, %v, ...): bad correlation between GoVersion and GoCommit slices", tt.proj, tt.clnum)
+		}
+		if got := fmt.Sprint(work); got != tt.want {
+			t.Errorf("tryWorkItem(%q, %v, ...) mismatch:\n got: %#q\nwant: %#q", tt.proj, tt.clnum, got, tt.want)
 		}
 	}
 }
@@ -547,6 +611,15 @@
 	refs []refHash
 }
 
+func (gp gerritProject) Ref(ref string) maintner.GitHash {
+	for _, r := range gp.refs {
+		if r.Ref == ref {
+			return r.Hash
+		}
+	}
+	return ""
+}
+
 func (gp gerritProject) ForeachNonChangeRef(fn func(ref string, hash maintner.GitHash) error) error {
 	for _, r := range gp.refs {
 		err := fn(r.Ref, r.Hash)