cmd/coordinator: move git ancestor checking before buildlet creation, generalize

This also adds GoDeps to the builders config file and adds a
dependency for the ssacheck builder, which currently fails on older
release branches with:

> Failed on linux-amd64-ssacheck:
> https://storage.googleapis.com/go-build-log/1255edaf/linux-amd64-ssacheck_18f7e015.log
>
> ...
> compile: unknown debug key -d dclstack

Change-Id: I4ad0a298705feed56f467f3c8d9488f8be2e4af5
Reviewed-on: https://go-review.googlesource.com/43774
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/coordinator/coordinator.go b/cmd/coordinator/coordinator.go
index 2b469a7..9b5aed3 100644
--- a/cmd/coordinator/coordinator.go
+++ b/cmd/coordinator/coordinator.go
@@ -71,6 +71,12 @@
 	// Notably, it is NOT included for network/communication
 	// errors.
 	eventDone = "done"
+
+	// eventSkipBuildMissingDep is a build event name meaning
+	// the builder type is not applicable to the commit being
+	// tested because the commit lacks a necessary dependency
+	// in its git history.
+	eventSkipBuildMissingDep = "skipped_build_missing_dep"
 )
 
 var (
@@ -1048,7 +1054,7 @@
 			}
 		}
 
-		if bs.hasEvent(eventDone) {
+		if bs.hasEvent(eventDone) || bs.hasEvent(eventSkipBuildMissingDep) {
 			ts.noteBuildComplete(bconf, bs)
 			return
 		}
@@ -1358,18 +1364,22 @@
 }
 
 // start starts the build in a new goroutine.
-// The buildStatus's context is closed on when the build is complete,
+// The buildStatus's context is closed when the build is complete,
 // successfully or not.
 func (st *buildStatus) start() {
 	setStatus(st.builderRev, st)
 	go func() {
 		err := st.build()
-		if err != nil {
-			fmt.Fprintf(st, "\n\nError: %v\n", err)
-			log.Println(st.builderRev, "failed:", err)
+		if err == errSkipBuildDueToDeps {
+			st.setDone(true)
+		} else {
+			if err != nil {
+				fmt.Fprintf(st, "\n\nError: %v\n", err)
+				log.Println(st.builderRev, "failed:", err)
+			}
+			st.setDone(err == nil)
+			putBuildRecord(st.buildRecord())
 		}
-		st.setDone(err == nil)
-		putBuildRecord(st.buildRecord())
 		markDone(st.builderRev)
 	}()
 }
@@ -1490,7 +1500,57 @@
 	return nil
 }
 
+func (st *buildStatus) checkDep(ctx context.Context, dep string) (have bool, err error) {
+	span := st.createSpan("ask_maintner_has_ancestor")
+	defer func() { span.done(err) }()
+	tries := 0
+	for {
+		tries++
+		res, err := maintnerClient.HasAncestor(ctx, &apipb.HasAncestorRequest{
+			Commit:   st.rev,
+			Ancestor: dep,
+		})
+		if err != nil {
+			if tries == 3 {
+				span.done(err)
+				return false, err
+			}
+			time.Sleep(1 * time.Second)
+			continue
+		}
+		if res.UnknownCommit {
+			select {
+			case <-ctx.Done():
+				return false, ctx.Err()
+			case <-time.After(1 * time.Second):
+			}
+			continue
+		}
+		return res.HasAncestor, nil
+	}
+}
+
+var errSkipBuildDueToDeps = errors.New("build was skipped due to missing deps")
+
 func (st *buildStatus) build() error {
+	if deps := st.conf.GoDeps; len(deps) > 0 {
+		ctx, cancel := context.WithTimeout(st.ctx, 30*time.Second)
+		defer cancel()
+		for _, dep := range deps {
+			has, err := st.checkDep(ctx, dep)
+			if err != nil {
+				fmt.Fprintf(st, "Error checking whether commit %s includes ancestor %s: %v\n", st.rev, dep, err)
+				return err
+			}
+			if !has {
+				st.logEventTime(eventSkipBuildMissingDep)
+				fmt.Fprintf(st, "skipping build; commit %s lacks ancestor %s\n", st.rev, dep)
+				return errSkipBuildDueToDeps
+			}
+		}
+		cancel()
+	}
+
 	putBuildRecord(st.buildRecord())
 
 	sp := st.createSpan("checking_for_snapshot")
@@ -1853,39 +1913,6 @@
 // caller, runMake, per builder config).
 // The idea is that this might find data races in cmd/compile.
 func (st *buildStatus) runConcurrentGoBuildStdCmd(bc *buildlet.Client) (remoteErr, err error) {
-	// Only run this step if this rev has the cmd/compile -c flag.
-	// See Issue 20222.
-	qctx, cancel := context.WithTimeout(st.ctx, 30*time.Second)
-	defer cancel()
-	spanha := st.createSpan("ask_maintner_has_ancestor")
-	for {
-		const dashCRev = "22f1b56dab29d397d2bdbdd603d85e60fb678089"
-		res, err := maintnerClient.HasAncestor(qctx, &apipb.HasAncestorRequest{
-			Commit:   st.rev,
-			Ancestor: dashCRev,
-		})
-		if err != nil {
-			log.Printf("HasAncestor(%q, %q) error: %v", st.rev, dashCRev, err)
-			spanha.done(err)
-			return nil, err
-		}
-		if res.UnknownCommit {
-			select {
-			case <-qctx.Done():
-				return qctx.Err(), nil
-			case <-time.After(1 * time.Second):
-			}
-			continue
-		}
-		spanha.done(nil)
-		if !res.HasAncestor {
-			// Old commit (e.g. 1.8 branch). Don't test.
-			return nil, nil
-		}
-		// Recent commit. Break & test.
-		break
-	}
-
 	span := st.createSpan("go_build_c128_std_cmd")
 	remoteErr, err = bc.Exec("go/bin/go", buildlet.ExecOpts{
 		Output:   st,
diff --git a/dashboard/builders.go b/dashboard/builders.go
index 8ccf299..5c125b6 100644
--- a/dashboard/builders.go
+++ b/dashboard/builders.go
@@ -467,6 +467,11 @@
 	// "std".
 	InstallRacePackages []string
 
+	// GoDeps is a list of of git sha1 commits that must be in the
+	// commit to be tested's history. If absent, this builder is
+	// not run for that commit.
+	GoDeps []string
+
 	// numTestHelpers is the number of _additional_ buildlets
 	// past the first one to help out with sharded tests.
 	// For trybots, the numTryHelpers value is used, unless it's
@@ -850,6 +855,9 @@
 		CompileOnly: true,
 		Notes:       "SSA internal checks enabled",
 		env:         []string{"GO_GCFLAGS=-d=ssa/check/on,dclstack"},
+		GoDeps: []string{
+			"f65abf6ddc8d1f3d403a9195fd74eaffa022b07f", // adds dclstack
+		},
 	})
 	addBuilder(BuildConfig{
 		Name:                "linux-amd64-racecompile",
@@ -860,6 +868,9 @@
 		StopAfterMake:       true,
 		InstallRacePackages: []string{"cmd/compile"},
 		Notes:               "race-enabled cmd/compile",
+		GoDeps: []string{
+			"22f1b56dab29d397d2bdbdd603d85e60fb678089", // adds cmd/compile -c; Issue 20222
+		},
 	})
 	addBuilder(BuildConfig{
 		Name:              "linux-amd64-race",