cmd/relui,internal/task: add workflow for re-syncing go-private master

This is currently a manual process that requires a high-priv grant, a
workflow significantly reduces the likelihood of getting this wrong. It
also lets us use a cron-like schedule to automate these updates.

Updates golang/go#59717

Change-Id: Iff7ce7c37f2ecd9dfee79ee8e80cfb98810011e6
Reviewed-on: https://go-review.googlesource.com/c/build/+/486575
Run-TryBot: Roland Shoemaker <roland@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index 9de5a33..2051571 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -245,6 +245,12 @@
 	}
 	dh.RegisterDefinition("Tag a new version of x/telemetry/config (if necessary)", tagTelemetryTasks.NewDefinition())
 
+	privateSyncTask := &task.PrivateMasterSyncTask{
+		PrivateGerritURL: "https://team.googlesource.com/golang/go-private",
+		Ref:              "public",
+	}
+	dh.RegisterDefinition("Sync go-private master branch with public", privateSyncTask.NewDefinition())
+
 	var base *url.URL
 	if *baseURL != "" {
 		base, err = url.Parse(*baseURL)
diff --git a/internal/task/sync_private.go b/internal/task/sync_private.go
new file mode 100644
index 0000000..7a2a37e
--- /dev/null
+++ b/internal/task/sync_private.go
@@ -0,0 +1,47 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package task
+
+import (
+	"fmt"
+
+	wf "golang.org/x/build/internal/workflow"
+)
+
+type PrivateMasterSyncTask struct {
+	PrivateGerritURL string
+	Ref              string
+}
+
+func (t *PrivateMasterSyncTask) NewDefinition() *wf.Definition {
+	wd := wf.New()
+	// We use a Task, instead of an Action, even though we don't actually want
+	// to return any result, because nothing depends on the Action, and if we
+	// use an Action the definition will tell us we don't reference it anywhere
+	// and say it should be deleted.
+	synced := wf.Task0(wd, "Sync go-private master to public", func(ctx *wf.TaskContext) (string, error) {
+		git := &Git{}
+		repo, err := git.Clone(ctx, t.PrivateGerritURL)
+		if err != nil {
+			return "", err
+		}
+
+		// NOTE: we assume this is generally safe in the case of a race between
+		// submitting a patch and resetting the master branch due to the ordering
+		// of operations at Gerrit. If the submit wins, we reset the master
+		// branch, and the submitted commit is orphaned, which is the expected
+		// behavior anyway. If the reset wins, the submission will either be
+		// cherry-picked onto the new base, which should either succeed, or fail
+		// due to a merge conflict, or Gerrit will reject the submission because
+		// something changed underneath it. Either case seems fine.
+		if _, err := repo.RunCommand(ctx.Context, "push", "--force", "origin", fmt.Sprintf("origin/%s:refs/heads/master", t.Ref)); err != nil {
+			return "", err
+		}
+
+		return "finished", nil
+	})
+	wf.Output(wd, fmt.Sprintf("Reset master to %s", t.Ref), synced)
+	return wd
+}
diff --git a/internal/task/sync_private_test.go b/internal/task/sync_private_test.go
new file mode 100644
index 0000000..c4a4b10
--- /dev/null
+++ b/internal/task/sync_private_test.go
@@ -0,0 +1,46 @@
+package task
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	"golang.org/x/build/internal/workflow"
+)
+
+func TestSyncPrivate(t *testing.T) {
+	fakeRepo := NewFakeRepo(t, "fake")
+	masterCommit := fakeRepo.CommitOnBranch("master", map[string]string{
+		"hello": "there",
+	})
+	fakeRepo.Branch("public", masterCommit)
+	publicCommit := fakeRepo.CommitOnBranch("public", map[string]string{
+		"general": "kenobi",
+	})
+
+	sync := &PrivateMasterSyncTask{
+		PrivateGerritURL: fakeRepo.dir.dir, // kind of wild that this works
+		Ref:              "public",
+	}
+
+	wd := sync.NewDefinition()
+	w, err := workflow.Start(wd, map[string]any{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+	defer cancel()
+	_, err = w.Run(ctx, &verboseListener{t: t})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	fakeRepo.runGit("switch", "master")
+	newMasterCommit := strings.TrimSpace(string(fakeRepo.runGit("rev-parse", "HEAD")))
+	// newMasterCommit := fakeRepo.ReadBranchHead(context.Background(), )
+
+	if newMasterCommit != publicCommit {
+		t.Fatalf("unexpected master commit: got %q, want %q", newMasterCommit, publicCommit)
+	}
+}