cmd/relui: add post-submit bypass to "tag single x/ repo" workflow

Requires adding a checkbox parameter type. This should allow the release
coordinator (or whoever is running this workflow) to tag a single x/
repo without needing to wait for a green post-submit commit, which is
necessary when doing a security release.

Change-Id: Id9e6743bc5d86730bb109cda91164190c34187c7
Reviewed-on: https://go-review.googlesource.com/c/build/+/517235
Reviewed-by: Heschi Kreinick <heschi@google.com>
Auto-Submit: Roland Shoemaker <roland@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Roland Shoemaker <roland@golang.org>
diff --git a/internal/relui/templates/new_workflow.html b/internal/relui/templates/new_workflow.html
index 956d6d8..9cbd4cc 100644
--- a/internal/relui/templates/new_workflow.html
+++ b/internal/relui/templates/new_workflow.html
@@ -109,6 +109,15 @@
                 </button>
               </div>
             </div>
+            {{else if eq $p.Type.String "bool"}}
+            <div class="NewWorkflow-parameter NewWorkflow-parameter--bool">
+              <label for="workflow.params.{{$p.Name}}" title="{{$p.Doc}}">{{$p.Name}}</label>
+              <input
+                id="workflow.params.{{$p.Name}}"
+                name="workflow.params.{{$p.Name}}"
+                {{- with $p.HTMLInputType}}type="{{.}}"{{end}}
+                {{- if $p.RequireNonZero}}required{{end}} />
+            </div>
           {{else}}
             <div class="NewWorkflow-parameter">
               <label title="{{$p.Doc}}">{{$p.Name}}</label>
diff --git a/internal/relui/web.go b/internal/relui/web.go
index 791a3f7..73b76e8 100644
--- a/internal/relui/web.go
+++ b/internal/relui/web.go
@@ -371,6 +371,23 @@
 				return
 			}
 			params[p.Name()] = v
+		case "bool":
+			vStr := r.FormValue(fmt.Sprintf("workflow.params.%s", p.Name()))
+			var v bool
+			switch vStr {
+			case "on":
+				v = true
+			case "":
+				v = false
+			default:
+				http.Error(w, fmt.Sprintf("parameter %q has an unexpected value %q", p.Name(), vStr), http.StatusBadRequest)
+				return
+			}
+			if err := p.Valid(v); err != nil {
+				http.Error(w, err.Error(), http.StatusBadRequest)
+				return
+			}
+			params[p.Name()] = v
 		default:
 			http.Error(w, fmt.Sprintf("parameter %q has an unsupported type %q", p.Name(), p.Type()), http.StatusInternalServerError)
 			return
diff --git a/internal/task/tagx.go b/internal/task/tagx.go
index 2a8b2cb..7fb02dd 100644
--- a/internal/task/tagx.go
+++ b/internal/task/tagx.go
@@ -54,7 +54,10 @@
 	reviewers := wf.Param(wd, reviewersParam)
 	repos := wf.Task0(wd, "Load all repositories", x.SelectRepos)
 	name := wf.Param(wd, wf.ParamDef[string]{Name: "Repository name", Example: "tools"})
-	wf.Expand3(wd, "Create single-repo plan", x.BuildSingleRepoPlan, repos, name, reviewers)
+	// TODO: optional is required to avoid the "required" check, but since it's a checkbox
+	// it's obviously yes/no, should probably be exempted from that check.
+	skipPostSubmit := wf.Param(wd, wf.ParamDef[bool]{Name: "Skip post submit result (optional)", ParamType: wf.Bool})
+	wf.Expand4(wd, "Create single-repo plan", x.BuildSingleRepoPlan, repos, name, skipPostSubmit, reviewers)
 	return wd
 }
 
@@ -247,7 +250,7 @@
 			if _, ok := updated[repo.ModPath]; ok {
 				continue
 			}
-			dep, ok := x.planRepo(wd, repo, updated, reviewers)
+			dep, ok := x.planRepo(wd, repo, updated, reviewers, false)
 			if !ok {
 				continue
 			}
@@ -274,7 +277,7 @@
 	return nil
 }
 
-func (x *TagXReposTasks) BuildSingleRepoPlan(wd *wf.Definition, repoSlice []TagRepo, name string, reviewers []string) error {
+func (x *TagXReposTasks) BuildSingleRepoPlan(wd *wf.Definition, repoSlice []TagRepo, name string, skipPostSubmit bool, reviewers []string) error {
 	repos := map[string]TagRepo{}
 	updatedRepos := map[string]wf.Value[TagRepo]{}
 	for _, r := range repoSlice {
@@ -288,7 +291,7 @@
 	if !ok {
 		return fmt.Errorf("no repository %q", name)
 	}
-	tagged, ok := x.planRepo(wd, repo, updatedRepos, reviewers)
+	tagged, ok := x.planRepo(wd, repo, updatedRepos, reviewers, skipPostSubmit)
 	if !ok {
 		return fmt.Errorf("%q doesn't have all of its dependencies (%q)", repo.Name, repo.Deps)
 	}
@@ -299,7 +302,7 @@
 // planRepo adds tasks to wf to update and tag repo. It returns a Value
 // containing the tagged repository's information, or nil, false if its
 // dependencies haven't been planned yet.
-func (x *TagXReposTasks) planRepo(wd *wf.Definition, repo TagRepo, updated map[string]wf.Value[TagRepo], reviewers []string) (_ wf.Value[TagRepo], ready bool) {
+func (x *TagXReposTasks) planRepo(wd *wf.Definition, repo TagRepo, updated map[string]wf.Value[TagRepo], reviewers []string, skipPostSubmit bool) (_ wf.Value[TagRepo], ready bool) {
 	var deps []wf.Value[TagRepo]
 	for _, repoDeps := range repo.Deps {
 		if dep, ok := updated[repoDeps]; ok {
@@ -319,8 +322,10 @@
 		cl := wf.Task3(wd, "mail updated go.mod", x.MailGoMod, repoName, gomod, wf.Const(reviewers))
 		tagCommit = wf.Task3(wd, "wait for submit", x.AwaitGoMod, cl, repoName, branch)
 	}
-	greenCommit := wf.Task2(wd, "wait for green post-submit", x.AwaitGreen, wf.Const(repo), tagCommit)
-	tagged := wf.Task2(wd, "tag if appropriate", x.MaybeTag, wf.Const(repo), greenCommit)
+	if !skipPostSubmit {
+		tagCommit = wf.Task2(wd, "wait for green post-submit", x.AwaitGreen, wf.Const(repo), tagCommit)
+	}
+	tagged := wf.Task2(wd, "tag if appropriate", x.MaybeTag, wf.Const(repo), tagCommit)
 	return tagged, true
 }
 
diff --git a/internal/task/tagx_test.go b/internal/task/tagx_test.go
index 05ac67e..cda45e0 100644
--- a/internal/task/tagx_test.go
+++ b/internal/task/tagx_test.go
@@ -344,7 +344,7 @@
 	tagXTasks *TagXReposTasks
 }
 
-func newTagXTestDeps(t *testing.T, repos ...*FakeRepo) *tagXTestDeps {
+func newTagXTestDeps(t *testing.T, dashboardStatus string, repos ...*FakeRepo) *tagXTestDeps {
 	if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
 		t.Skip("Requires bash shell scripting support.")
 	}
@@ -367,10 +367,11 @@
 
 	fakeBuildlets := NewFakeBuildlets(t, "", nil)
 	fakeGerrit := NewFakeGerrit(t, repos...)
-	var builders, allOK []string
+	var builders, dashboardStatuses []string
 	for _, b := range dashboard.Builders {
 		builders = append(builders, b.Name)
-		allOK = append(allOK, "ok")
+		// allOK = append(allOK, "ok")
+		dashboardStatuses = append(dashboardStatuses, dashboardStatus)
 	}
 	fakeDash := func(repo string) *types.BuildStatus {
 		if repo == "" {
@@ -397,7 +398,7 @@
 					Date:       time.Now().Format(time.RFC3339),
 					Branch:     "master",
 					GoBranch:   "master",
-					Results:    allOK,
+					Results:    dashboardStatuses,
 				})
 			}
 			return st
@@ -447,7 +448,7 @@
 	})
 	tools.Tag("v1.1.5", tools1)
 
-	deps := newTagXTestDeps(t, sys, mod, tools)
+	deps := newTagXTestDeps(t, "ok", sys, mod, tools)
 
 	wd := deps.tagXTasks.NewDefinition()
 	w, err := workflow.Start(wd, map[string]interface{}{
@@ -502,7 +503,7 @@
 	}
 }
 
-func TestTagSingleRepo(t *testing.T) {
+func testTagSingleRepo(t *testing.T, dashboardStatus string, skipPostSubmit bool) {
 	mod := NewFakeRepo(t, "mod")
 	mod1 := mod.Commit(map[string]string{
 		"go.mod": "module golang.org/x/mod\n",
@@ -519,14 +520,20 @@
 		"main.go": "package main",
 	})
 
-	deps := newTagXTestDeps(t, mod, foo)
+	deps := newTagXTestDeps(t, dashboardStatus, mod, foo)
 
 	wd := deps.tagXTasks.NewSingleDefinition()
 	ctx, cancel := context.WithTimeout(deps.ctx, time.Minute)
-	w, err := workflow.Start(wd, map[string]interface{}{
+	args := map[string]interface{}{
 		"Repository name":   "foo",
 		reviewersParam.Name: []string(nil),
-	})
+	}
+	if skipPostSubmit {
+		args["Skip post submit result (optional)"] = true
+	} else {
+		args["Skip post submit result (optional)"] = false
+	}
+	w, err := workflow.Start(wd, args)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -549,6 +556,12 @@
 	}
 }
 
+func TestTagSingleRepo(t *testing.T) {
+	t.Run("with post-submit check", func(t *testing.T) { testTagSingleRepo(t, "ok", false) })
+	// If skipPostSubmit is false, AwaitGreen should sit an spin for a minute before failing
+	t.Run("without post-submit check", func(t *testing.T) { testTagSingleRepo(t, "bad", true) })
+}
+
 type verboseListener struct {
 	t              *testing.T
 	outputListener func(string, interface{})
diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go
index 680fd47..f9fd858 100644
--- a/internal/workflow/workflow.go
+++ b/internal/workflow/workflow.go
@@ -223,6 +223,12 @@
 	SliceLong = ParamType[[]string]{
 		HTMLElement: "textarea",
 	}
+
+	// Checkbox bool parameter
+	Bool = ParamType[bool]{
+		HTMLElement:   "input",
+		HTMLInputType: "checkbox",
+	}
 )
 
 // Param registers a new parameter p that is filled in at