cmd/releasebot: check that the security branch is merged into the release branch

The existing release process doesn't have any automated checks that
ensure that the changes contained in the security release branch are
in the newly created release branch. This change ensures that
non-security releases contain the HEAD commit from the security
release branch if such a branch exists.

Fixes golang/go#34505

Change-Id: I03d000177ece16548d54b6d6ad7fb0969df3946e
Reviewed-on: https://go-review.googlesource.com/c/build/+/206437
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/releasebot/git.go b/cmd/releasebot/git.go
index 8f0045a..208f872 100644
--- a/cmd/releasebot/git.go
+++ b/cmd/releasebot/git.go
@@ -11,6 +11,13 @@
 	"strings"
 )
 
+const (
+	// publicGoRepoURL contains the Gerrit repository URL.
+	publicGoRepoURL = "https://go.googlesource.com/go"
+	// privateGoRepoURL contains the internal repository URL.
+	privateGoRepoURL = "sso://team/golang/go-private"
+)
+
 // gitCheckout sets up a fresh git checkout in which to work,
 // in $HOME/go-releasebot-work/<release>/gitwork
 // (where <release> is a string like go1.8.5).
@@ -25,9 +32,9 @@
 		w.log.Panic(err)
 	}
 
-	origin := "https://go.googlesource.com/go"
+	origin := publicGoRepoURL
 	if w.Security {
-		origin = "sso://team/golang/go-private"
+		origin = privateGoRepoURL
 	}
 
 	// Check out a local mirror to work-mirror, to speed future checkouts for this point release.
@@ -47,12 +54,13 @@
 		w.log.Panic(err)
 	}
 	w.runner(w.Dir).run("git", "clone", "--reference", mirror, "-b", w.ReleaseBranch, origin, gitDir)
+
 	r = w.runner(gitDir)
 	r.run("git", "codereview", "change", "relwork")
 	r.run("git", "config", "gc.auto", "0") // don't throw away refs we fetch
 }
 
-// gitTagExists returns whether git git tag is already present in the repository.
+// gitTagExists returns whether a git tag is already present in the repository.
 func (w *Work) gitTagExists() bool {
 	_, err := w.runner(filepath.Join(w.Dir, "gitwork")).runErr("git", "rev-parse", w.Version)
 	return err == nil
@@ -97,3 +105,21 @@
 	out := r.runOut("git", "rev-parse", "HEAD")
 	return strings.TrimSpace(string(out))
 }
+
+// gitRemoteBranchCommit returns the hash of the HEAD commit on the branch located
+// on a remote repository. It will return false when the branch does not exist or there
+// is a problem communicating with the remote repository.
+func (w *Work) gitRemoteBranchCommit(repositoryURL, branch string) (string, bool) {
+	out := w.runner(w.Dir).runOut("git", "ls-remote", "--heads", repositoryURL, "refs/heads/"+branch)
+	if len(out) == 0 {
+		return "", false
+	}
+	sha := strings.SplitN(string(out), "\t", 2)[0]
+	return sha, true
+}
+
+// gitCommitExistsInBranch reports whether the commit hash exists in the current branch.
+func (w *Work) gitCommitExistsInBranch(commitSHA string) bool {
+	_, err := w.runner(filepath.Join(w.Dir, "gitwork")).runErr("git", "merge-base", "--is-ancestor", commitSHA, "HEAD")
+	return err == nil
+}
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index c1f17f9..52d4ecf 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -79,7 +79,10 @@
 
 	release := flag.Arg(0)
 
-	if strings.Contains(release, "beta") || strings.Contains(release, "rc") {
+	isBeta := strings.Contains(release, "beta")
+	isRC := strings.Contains(release, "rc")
+
+	if isBeta || isRC {
 		if *security {
 			log.Printf("error: only minor releases are supported in security mode")
 			usage()
@@ -87,8 +90,8 @@
 		w := &Work{
 			Prepare:     *modeFlag == "prepare",
 			Version:     release,
-			BetaRelease: strings.Contains(release, "beta"),
-			RCRelease:   strings.Contains(release, "rc"),
+			BetaRelease: isBeta,
+			RCRelease:   isRC,
 		}
 		w.doRelease()
 		return
@@ -299,9 +302,11 @@
 
 	if w.BetaRelease {
 		w.ReleaseBranch = "master"
+
 	} else if w.RCRelease {
 		shortRel := strings.Split(w.Version, "rc")[0]
 		w.ReleaseBranch = "release-branch." + shortRel
+
 	} else if strings.Count(w.Version, ".") == 1 {
 		// Major release like "go1.X".
 		if w.Security {
@@ -327,6 +332,11 @@
 
 	w.checkSpelling()
 	w.gitCheckout()
+
+	if !w.Security {
+		w.mustIncludeSecurityBranch()
+	}
+
 	// In release mode we carry on even if the tag exists, in case we
 	// need to resume a failed build.
 	if w.Prepare && w.gitTagExists() {
@@ -819,3 +829,24 @@
 	w.releaseMu.Unlock()
 	return nil
 }
+
+// mustIncludeSecurityBranch remotely checks if there is an associated release branch
+// for the current release. If one exists, it ensures that the HEAD commit in the latest
+// security release branch exists within the current release branch. If the latest security
+// branch has changes which have not been merged into the proposed release, it will exit
+// fatally. If an asssociated security release branch does not exist, the function will
+// return without doing the check. It assumes that if the security branch doesn't exist,
+// it's because it was already merged everywhere and deleted.
+func (w *Work) mustIncludeSecurityBranch() {
+	securityReleaseBranch := fmt.Sprintf("%s-security", w.ReleaseBranch)
+
+	sha, ok := w.gitRemoteBranchCommit(privateGoRepoURL, securityReleaseBranch)
+	if !ok {
+		w.log.Printf("an associated security release branch %q does not exist; assuming it has been merged and deleted, so proceeding as usual", securityReleaseBranch)
+		return
+	}
+
+	if !w.gitCommitExistsInBranch(sha) {
+		log.Fatalf("release branch does not contain security release HEAD commit %q; aborting", sha)
+	}
+}