cmd/gopherbot: consider Kokoro to be a bot

We're using a distinct Gerrit account to post Kokoro CI results.
Add it to the list of known bots to stop treating it as a human
reviewer.

Also future-proof a little by considering any Gerrit account with the
SERVICE_USER tag to be a bot, since all bot accounts are expected¹ to
be a part of that group.

¹ https://gerrit-review.googlesource.com/Documentation/user-attention-set.html#_important_note_for_all_host_owners_project_owners_and_bot_owners

For golang/go#38906.

Change-Id: If33def5b73fdaadec81b6fdc03a0eaf3042f8095
Reviewed-on: https://go-review.googlesource.com/c/build/+/401514
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/gopherbot/gopherbot.go b/cmd/gopherbot/gopherbot.go
index 84a9ed8..0edbf3d 100644
--- a/cmd/gopherbot/gopherbot.go
+++ b/cmd/gopherbot/gopherbot.go
@@ -2291,6 +2291,7 @@
 	const (
 		gobotID     = 5976
 		gerritbotID = 12446
+		kokoroID    = 37747
 	)
 	// The CL's owner will be GerritBot if it is imported from a PR.
 	// In that case, if the CL's author has a Gerrit account, they will be
@@ -2315,15 +2316,30 @@
 		log.Printf("Could not list reviewers on change %q: %v", change.ID(), err)
 		return nil, true
 	}
-	var count int
 	var ids []string
 	for _, r := range reviewers {
-		if r.NumericID != gobotID && r.NumericID != gerritbotID && r.NumericID != ownerID {
-			ids = append(ids, strconv.FormatInt(r.NumericID, 10))
-			count++
+		switch id := r.NumericID; {
+		case id == gobotID, id == gerritbotID, id == kokoroID,
+			hasServiceUserTag(r.AccountInfo):
+			// Skip bots.
+			continue
+		case id == ownerID:
+			// Skip owner.
+			continue
+		}
+		ids = append(ids, strconv.FormatInt(r.NumericID, 10))
+	}
+	return ids, len(ids) >= minHumans
+}
+
+// hasServiceUserTag reports whether the account has a SERVICE_USER tag.
+func hasServiceUserTag(a gerrit.AccountInfo) bool {
+	for _, t := range a.Tags {
+		if t == "SERVICE_USER" {
+			return true
 		}
 	}
-	return ids, count >= minHumans
+	return false
 }
 
 // autoSubmitCLs submits CLs which are labelled "Auto-Submit", are submittable according to Gerrit,
@@ -2502,6 +2518,7 @@
 	var (
 		gobotEmail     = "5976" + gerritInstanceID
 		gerritbotEmail = "12446" + gerritInstanceID
+		kokoroEmail    = "37747" + gerritInstanceID
 	)
 	var count int
 	var ids []string
@@ -2514,7 +2531,8 @@
 			if !strings.HasPrefix(ln, "Reviewer:") && !strings.HasPrefix(ln, "CC:") {
 				return nil
 			}
-			if !strings.Contains(ln, gobotEmail) && !strings.Contains(ln, gerritbotEmail) {
+			if !strings.Contains(ln, gobotEmail) && !strings.Contains(ln, gerritbotEmail) &&
+				!strings.Contains(ln, kokoroEmail) {
 				match := reviewerRe.FindStringSubmatch(ln)
 				if match == nil {
 					return nil
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index 9fd2d8f..4b6cccc 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -286,17 +286,17 @@
 
 // AccountInfo is a Gerrit data structure. It's used both for getting the details
 // for a single account, as well as for querying multiple accounts.
+// See https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-info.
 type AccountInfo struct {
-	NumericID int64  `json:"_account_id"`
-	Name      string `json:"name,omitempty"`
-	Email     string `json:"email,omitempty"`
-	Username  string `json:"username,omitempty"`
+	NumericID int64    `json:"_account_id"`
+	Name      string   `json:"name,omitempty"`
+	Email     string   `json:"email,omitempty"`
+	Username  string   `json:"username,omitempty"`
+	Tags      []string `json:"tags,omitempty"`
 
 	// MoreAccounts is set on the last account from QueryAccounts if
 	// the result set is truncated by an 'n' parameter (or has more).
 	MoreAccounts bool `json:"_more_accounts"`
-
-	// TODO: "avatars" is also returned, but not added here yet (add if required)
 }
 
 func (ai *AccountInfo) Equal(v *AccountInfo) bool {