cmd/relui: add proto definition for workflows

Using configuration for our workflows will help separate concerns
between implementation and workflow configuration.

Eventually, similar tasks can be re-used in different workflows, such as
fetching from Git, updating the VERSION file, or publishing a tag.

The current configuration definition is mainly illustrative, and is
expected to change as we build out a prototype.

For golang/go#40279

Change-Id: I5c6f8a18571ab819de0b1d026c86050735efeed9
Reviewed-on: https://go-review.googlesource.com/c/build/+/243340
Run-TryBot: Alexander Rakoczy <alex@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/cmd/relui/main.go b/cmd/relui/main.go
index e20c6fb..9c1587f 100644
--- a/cmd/relui/main.go
+++ b/cmd/relui/main.go
@@ -5,13 +5,18 @@
 package main
 
 import (
+	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
+	"path/filepath"
+
+	"github.com/golang/protobuf/proto"
+	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
 func main() {
-	s := &server{store: &memoryStore{}}
+	s := &server{store: &memoryStore{}, configs: loadWorkflowConfig("./workflows")}
 	http.Handle("/workflows/create", http.HandlerFunc(s.createWorkflowHandler))
 	http.Handle("/workflows/new", http.HandlerFunc(s.newWorkflowHandler))
 	http.Handle("/", fileServerHandler(relativeFile("./static"), http.HandlerFunc(s.homeHandler)))
@@ -23,3 +28,28 @@
 	log.Printf("Listening on :" + port)
 	log.Fatal(http.ListenAndServe(":"+port, http.DefaultServeMux))
 }
+
+// loadWorkflowConfig loads Workflow configuration files from dir. It expects all files to be in textproto format.
+func loadWorkflowConfig(dir string) []*reluipb.Workflow {
+	fs, err := filepath.Glob(filepath.Join(relativeFile(dir), "*.textpb"))
+	if err != nil {
+		log.Fatalf("Error perusing %q for configuration", filepath.Join(dir, "*.textpb"))
+	}
+	if len(fs) == 0 {
+		log.Println("No workflow configuration found.")
+	}
+	var ws []*reluipb.Workflow
+	for _, f := range fs {
+		b, err := ioutil.ReadFile(f)
+		if err != nil {
+			log.Printf("ioutil.ReadFile(%q) = _, %v, wanted no error", f, err)
+		}
+		w := new(reluipb.Workflow)
+		if err = proto.UnmarshalText(string(b), w); err != nil {
+			log.Printf("Error unmarshalling Workflow from %q: %v", f, err)
+			continue
+		}
+		ws = append(ws, w)
+	}
+	return ws
+}
diff --git a/cmd/relui/protos/protos.go b/cmd/relui/protos/protos.go
new file mode 100644
index 0000000..9e4bded
--- /dev/null
+++ b/cmd/relui/protos/protos.go
@@ -0,0 +1,11 @@
+// Copyright 2020 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 protos
+
+// Run "go generate" in this directory to update. You need to have:
+//
+// - a protoc binary (see https://github.com/golang/protobuf#installation)
+// - go get -u github.com/golang/protobuf/protoc-gen-go
+
+//go:generate protoc --proto_path=$GOPATH/src:. --go_out=plugins=grpc:. relui.proto
diff --git a/cmd/relui/protos/relui.pb.go b/cmd/relui/protos/relui.pb.go
new file mode 100644
index 0000000..039aaa6
--- /dev/null
+++ b/cmd/relui/protos/relui.pb.go
@@ -0,0 +1,281 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: relui.proto
+
+package protos
+
+import (
+	fmt "fmt"
+	proto "github.com/golang/protobuf/proto"
+	math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+type TaskStatus int32
+
+const (
+	TaskStatus_TASK_STATUS_UNKNOWN TaskStatus = 0
+	TaskStatus_TASK_STATUS_CREATED TaskStatus = 1
+	TaskStatus_TASK_STATUS_STARTED TaskStatus = 2
+)
+
+var TaskStatus_name = map[int32]string{
+	0: "TASK_STATUS_UNKNOWN",
+	1: "TASK_STATUS_CREATED",
+	2: "TASK_STATUS_STARTED",
+}
+
+var TaskStatus_value = map[string]int32{
+	"TASK_STATUS_UNKNOWN": 0,
+	"TASK_STATUS_CREATED": 1,
+	"TASK_STATUS_STARTED": 2,
+}
+
+func (x TaskStatus) String() string {
+	return proto.EnumName(TaskStatus_name, int32(x))
+}
+
+func (TaskStatus) EnumDescriptor() ([]byte, []int) {
+	return fileDescriptor_6de8859f82adce0a, []int{0}
+}
+
+type Workflow struct {
+	// name is a unique name for a workflow, such as local_go_release. The name must be unique across
+	// all workflow configurations.
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// buildable_asks is a list of tasks to be performed by the workflow.
+	BuildableTasks []*BuildableTask `protobuf:"bytes,2,rep,name=buildable_tasks,json=buildableTasks,proto3" json:"buildable_tasks,omitempty"`
+	// params are parameters provided when creating a workflow.
+	Params               map[string]string `protobuf:"bytes,3,rep,name=params,proto3" json:"params,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
+	XXX_unrecognized     []byte            `json:"-"`
+	XXX_sizecache        int32             `json:"-"`
+}
+
+func (m *Workflow) Reset()         { *m = Workflow{} }
+func (m *Workflow) String() string { return proto.CompactTextString(m) }
+func (*Workflow) ProtoMessage()    {}
+func (*Workflow) Descriptor() ([]byte, []int) {
+	return fileDescriptor_6de8859f82adce0a, []int{0}
+}
+
+func (m *Workflow) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_Workflow.Unmarshal(m, b)
+}
+func (m *Workflow) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_Workflow.Marshal(b, m, deterministic)
+}
+func (m *Workflow) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_Workflow.Merge(m, src)
+}
+func (m *Workflow) XXX_Size() int {
+	return xxx_messageInfo_Workflow.Size(m)
+}
+func (m *Workflow) XXX_DiscardUnknown() {
+	xxx_messageInfo_Workflow.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_Workflow proto.InternalMessageInfo
+
+func (m *Workflow) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *Workflow) GetBuildableTasks() []*BuildableTask {
+	if m != nil {
+		return m.BuildableTasks
+	}
+	return nil
+}
+
+func (m *Workflow) GetParams() map[string]string {
+	if m != nil {
+		return m.Params
+	}
+	return nil
+}
+
+type BuildableTask struct {
+	// name is a unique name for a task, such as fetch_go_source. The name must be unique across
+	// all workflow configurations.
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// depends_on is the name of a task this task depends on. Artifacts from the depends_on task will be available
+	// to this task.
+	DependsOn string `protobuf:"bytes,2,opt,name=depends_on,json=dependsOn,proto3" json:"depends_on,omitempty"`
+	// task_status is the current status of a task.
+	Status TaskStatus `protobuf:"varint,3,opt,name=status,proto3,enum=protos.TaskStatus" json:"status,omitempty"`
+	// artifact_url is an optional URL to an artifact published by this task.
+	ArtifactUrl string `protobuf:"bytes,4,opt,name=artifact_url,json=artifactUrl,proto3" json:"artifact_url,omitempty"`
+	// git_source is an optional configuration for which git source to fetch.
+	GitSource *GitSource `protobuf:"bytes,5,opt,name=git_source,json=gitSource,proto3" json:"git_source,omitempty"`
+	// task_type is a unique type for a task, such as FetchGerritSource. Types are used by task runners to identify
+	// how to execute a task.
+	TaskType             string   `protobuf:"bytes,6,opt,name=task_type,json=taskType,proto3" json:"task_type,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *BuildableTask) Reset()         { *m = BuildableTask{} }
+func (m *BuildableTask) String() string { return proto.CompactTextString(m) }
+func (*BuildableTask) ProtoMessage()    {}
+func (*BuildableTask) Descriptor() ([]byte, []int) {
+	return fileDescriptor_6de8859f82adce0a, []int{1}
+}
+
+func (m *BuildableTask) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_BuildableTask.Unmarshal(m, b)
+}
+func (m *BuildableTask) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_BuildableTask.Marshal(b, m, deterministic)
+}
+func (m *BuildableTask) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_BuildableTask.Merge(m, src)
+}
+func (m *BuildableTask) XXX_Size() int {
+	return xxx_messageInfo_BuildableTask.Size(m)
+}
+func (m *BuildableTask) XXX_DiscardUnknown() {
+	xxx_messageInfo_BuildableTask.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_BuildableTask proto.InternalMessageInfo
+
+func (m *BuildableTask) GetName() string {
+	if m != nil {
+		return m.Name
+	}
+	return ""
+}
+
+func (m *BuildableTask) GetDependsOn() string {
+	if m != nil {
+		return m.DependsOn
+	}
+	return ""
+}
+
+func (m *BuildableTask) GetStatus() TaskStatus {
+	if m != nil {
+		return m.Status
+	}
+	return TaskStatus_TASK_STATUS_UNKNOWN
+}
+
+func (m *BuildableTask) GetArtifactUrl() string {
+	if m != nil {
+		return m.ArtifactUrl
+	}
+	return ""
+}
+
+func (m *BuildableTask) GetGitSource() *GitSource {
+	if m != nil {
+		return m.GitSource
+	}
+	return nil
+}
+
+func (m *BuildableTask) GetTaskType() string {
+	if m != nil {
+		return m.TaskType
+	}
+	return ""
+}
+
+type GitSource struct {
+	Url                  string   `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
+	Ref                  string   `protobuf:"bytes,2,opt,name=ref,proto3" json:"ref,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *GitSource) Reset()         { *m = GitSource{} }
+func (m *GitSource) String() string { return proto.CompactTextString(m) }
+func (*GitSource) ProtoMessage()    {}
+func (*GitSource) Descriptor() ([]byte, []int) {
+	return fileDescriptor_6de8859f82adce0a, []int{2}
+}
+
+func (m *GitSource) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_GitSource.Unmarshal(m, b)
+}
+func (m *GitSource) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_GitSource.Marshal(b, m, deterministic)
+}
+func (m *GitSource) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_GitSource.Merge(m, src)
+}
+func (m *GitSource) XXX_Size() int {
+	return xxx_messageInfo_GitSource.Size(m)
+}
+func (m *GitSource) XXX_DiscardUnknown() {
+	xxx_messageInfo_GitSource.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GitSource proto.InternalMessageInfo
+
+func (m *GitSource) GetUrl() string {
+	if m != nil {
+		return m.Url
+	}
+	return ""
+}
+
+func (m *GitSource) GetRef() string {
+	if m != nil {
+		return m.Ref
+	}
+	return ""
+}
+
+func init() {
+	proto.RegisterEnum("protos.TaskStatus", TaskStatus_name, TaskStatus_value)
+	proto.RegisterType((*Workflow)(nil), "protos.Workflow")
+	proto.RegisterMapType((map[string]string)(nil), "protos.Workflow.ParamsEntry")
+	proto.RegisterType((*BuildableTask)(nil), "protos.BuildableTask")
+	proto.RegisterType((*GitSource)(nil), "protos.GitSource")
+}
+
+func init() { proto.RegisterFile("relui.proto", fileDescriptor_6de8859f82adce0a) }
+
+var fileDescriptor_6de8859f82adce0a = []byte{
+	// 384 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x4d, 0x6f, 0xda, 0x40,
+	0x10, 0xed, 0x1a, 0xb0, 0xf0, 0xb8, 0xa5, 0x74, 0xdb, 0xaa, 0x56, 0x3f, 0x24, 0xca, 0xc9, 0xe2,
+	0x40, 0x2b, 0xda, 0x43, 0x9b, 0x43, 0x24, 0x27, 0x41, 0x39, 0x20, 0x41, 0x64, 0x1b, 0x71, 0xb4,
+	0xd6, 0xb0, 0x20, 0xcb, 0x8b, 0x6d, 0xed, 0xae, 0x13, 0xf9, 0x57, 0xe6, 0x2f, 0xe4, 0xa7, 0x44,
+	0xbb, 0xd8, 0x09, 0x41, 0x39, 0xf9, 0xed, 0x7b, 0x33, 0x6f, 0xde, 0x8c, 0x0c, 0x36, 0xa7, 0xac,
+	0x4c, 0xc6, 0x05, 0xcf, 0x65, 0x8e, 0x4d, 0xfd, 0x11, 0xc3, 0x7b, 0x04, 0xdd, 0x55, 0xce, 0xd3,
+	0x2d, 0xcb, 0xef, 0x30, 0x86, 0x76, 0x46, 0xf6, 0xd4, 0x41, 0x03, 0xe4, 0x5a, 0xbe, 0xc6, 0xf8,
+	0x1c, 0xde, 0xc7, 0x65, 0xc2, 0x36, 0x24, 0x66, 0x34, 0x92, 0x44, 0xa4, 0xc2, 0x31, 0x06, 0x2d,
+	0xd7, 0x9e, 0x7c, 0x3e, 0x38, 0x89, 0xf1, 0x45, 0x23, 0x87, 0x44, 0xa4, 0x7e, 0x2f, 0x3e, 0x7e,
+	0x0a, 0xfc, 0x17, 0xcc, 0x82, 0x70, 0xb2, 0x17, 0x4e, 0x4b, 0xb7, 0x7d, 0x6f, 0xda, 0x9a, 0xa9,
+	0xe3, 0x1b, 0x2d, 0x4f, 0x33, 0xc9, 0x2b, 0xbf, 0xae, 0xfd, 0xfa, 0x1f, 0xec, 0x23, 0x1a, 0xf7,
+	0xa1, 0x95, 0xd2, 0xaa, 0xce, 0xa5, 0x20, 0xfe, 0x04, 0x9d, 0x5b, 0xc2, 0x4a, 0xea, 0x18, 0x9a,
+	0x3b, 0x3c, 0xce, 0x8c, 0x7f, 0x68, 0xf8, 0x80, 0xe0, 0xdd, 0x8b, 0x48, 0xaf, 0xae, 0xf5, 0x03,
+	0x60, 0x43, 0x0b, 0x9a, 0x6d, 0x44, 0x94, 0x67, 0xb5, 0x89, 0x55, 0x33, 0x8b, 0x0c, 0x8f, 0xc0,
+	0x14, 0x92, 0xc8, 0x52, 0xa5, 0x46, 0x6e, 0x6f, 0x82, 0x9b, 0xd4, 0xca, 0x30, 0xd0, 0x8a, 0x5f,
+	0x57, 0xe0, 0x9f, 0xf0, 0x96, 0x70, 0x99, 0x6c, 0xc9, 0x5a, 0x46, 0x25, 0x67, 0x4e, 0x5b, 0x9b,
+	0xd9, 0x0d, 0xb7, 0xe4, 0x0c, 0xff, 0x06, 0xd8, 0x25, 0x32, 0x12, 0x79, 0xc9, 0xd7, 0xd4, 0xe9,
+	0x0c, 0x90, 0x6b, 0x4f, 0x3e, 0x34, 0x96, 0xd7, 0x89, 0x0c, 0xb4, 0xe0, 0x5b, 0xbb, 0x06, 0xe2,
+	0x6f, 0x60, 0xa9, 0x63, 0x47, 0xb2, 0x2a, 0xa8, 0x63, 0x6a, 0xc7, 0xae, 0x22, 0xc2, 0xaa, 0xa0,
+	0xc3, 0x5f, 0x60, 0x3d, 0x35, 0xa9, 0xdb, 0xa8, 0xa9, 0xf5, 0x6d, 0x4a, 0xce, 0x14, 0xc3, 0xe9,
+	0xb6, 0x5e, 0x4a, 0xc1, 0xd1, 0x0a, 0xe0, 0x39, 0x38, 0xfe, 0x02, 0x1f, 0x43, 0x2f, 0x98, 0x45,
+	0x41, 0xe8, 0x85, 0xcb, 0x20, 0x5a, 0xce, 0x67, 0xf3, 0xc5, 0x6a, 0xde, 0x7f, 0x73, 0x2a, 0x5c,
+	0xfa, 0x53, 0x2f, 0x9c, 0x5e, 0xf5, 0xd1, 0xa9, 0x10, 0x84, 0x9e, 0xaf, 0x04, 0x23, 0x3e, 0xfc,
+	0x46, 0x7f, 0x1e, 0x03, 0x00, 0x00, 0xff, 0xff, 0x73, 0x67, 0x81, 0x13, 0x5c, 0x02, 0x00, 0x00,
+}
diff --git a/cmd/relui/protos/relui.proto b/cmd/relui/protos/relui.proto
new file mode 100644
index 0000000..fa1be06
--- /dev/null
+++ b/cmd/relui/protos/relui.proto
@@ -0,0 +1,53 @@
+// Copyright 2020 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.
+
+syntax = "proto3";
+
+package protos;
+
+message Workflow {
+  // name is a unique name for a workflow, such as local_go_release. The name must be unique across
+  // all workflow configurations.
+  string name = 1;
+
+  // buildable_asks is a list of tasks to be performed by the workflow.
+  repeated BuildableTask buildable_tasks = 2;
+
+  // params are parameters provided when creating a workflow.
+  map<string, string> params = 3;
+}
+
+message BuildableTask {
+  // name is a unique name for a task, such as fetch_go_source. The name must be unique across
+  // all workflow configurations.
+  string name = 1;
+
+  // depends_on is the name of a task this task depends on. Artifacts from the depends_on task will be available
+  // to this task.
+  string depends_on = 2;
+
+  // task_status is the current status of a task.
+  TaskStatus status = 3;
+
+  // artifact_url is an optional URL to an artifact published by this task.
+  string artifact_url = 4;
+
+  // git_source is an optional configuration for which git source to fetch.
+  GitSource git_source = 5;
+
+  // task_type is a unique type for a task, such as FetchGerritSource. Types are used by task runners to identify
+  // how to execute a task.
+  string task_type = 6;
+}
+
+message GitSource {
+  string url = 1;
+  string ref = 2;
+}
+
+enum TaskStatus {
+  TASK_STATUS_UNKNOWN = 0;
+  TASK_STATUS_CREATED = 1;
+  TASK_STATUS_STARTED = 2;
+}
diff --git a/cmd/relui/store.go b/cmd/relui/store.go
new file mode 100644
index 0000000..09fd362
--- /dev/null
+++ b/cmd/relui/store.go
@@ -0,0 +1,42 @@
+// Copyright 2020 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 main
+
+import (
+	"sync"
+
+	reluipb "golang.org/x/build/cmd/relui/protos"
+)
+
+// store is a persistence adapter for saving data.
+type store interface {
+	GetWorkflows() []*reluipb.Workflow
+	AddWorkflow(workflow *reluipb.Workflow) error
+}
+
+var _ store = (*memoryStore)(nil)
+
+// memoryStore is a non-durable implementation of store that keeps everything in memory.
+type memoryStore struct {
+	mut       sync.Mutex
+	workflows []*reluipb.Workflow
+}
+
+// AddWorkflow adds a workflow to the store.
+func (m *memoryStore) AddWorkflow(w *reluipb.Workflow) error {
+	m.mut.Lock()
+	defer m.mut.Unlock()
+	m.workflows = append(m.workflows, w)
+	return nil
+}
+
+// GetWorkflows returns all workflows stored.
+//
+// TODO(golang.org/issue/40279) - clone workflows if they're ever mutated.
+func (m *memoryStore) GetWorkflows() []*reluipb.Workflow {
+	m.mut.Lock()
+	defer m.mut.Unlock()
+	return m.workflows
+}
diff --git a/cmd/relui/templates/home.html b/cmd/relui/templates/home.html
index 7cd9342..c868830 100644
--- a/cmd/relui/templates/home.html
+++ b/cmd/relui/templates/home.html
@@ -12,12 +12,12 @@
   <ul class="WorkflowList">
     {{range $workflow := .Workflows}}
     <li class="WorkflowList-item">
-      <h3>{{$workflow.Title}}</h3>
+      <h3>{{$workflow.Name}} - {{index $workflow.GetParams "GitObject"}}</h3>
       <h4 class="WorkflowList-sectionTitle">Tasks</h4>
       <ul class="TaskList">
-        {{range $task := $workflow.Tasks}}
+        {{range $task := $workflow.BuildableTasks}}
         <li class="TaskList-item">
-          <span class="TaskList-itemTitle">{{$task.Title}}</span>
+          <span class="TaskList-itemTitle">{{$task.Name}}</span>
           Status: {{$task.Status}}
         </li>
         {{end}}
diff --git a/cmd/relui/web.go b/cmd/relui/web.go
index ec24176..fddde4c 100644
--- a/cmd/relui/web.go
+++ b/cmd/relui/web.go
@@ -14,6 +14,9 @@
 	"os"
 	"path"
 	"path/filepath"
+
+	"github.com/golang/protobuf/proto"
+	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
 // fileServerHandler returns a http.Handler rooted at root. It will call the next handler provided for requests to "/".
@@ -47,11 +50,15 @@
 
 // server implements the http handlers for relui.
 type server struct {
+	// configs are all configured release workflows.
+	configs []*reluipb.Workflow
+
+	// store is for persisting application state.
 	store store
 }
 
 type homeResponse struct {
-	Workflows []workflow
+	Workflows []*reluipb.Workflow
 }
 
 // homeHandler renders the homepage.
@@ -88,7 +95,17 @@
 		http.Error(w, "workflow revision is required", http.StatusBadRequest)
 		return
 	}
-	if err := s.store.AddWorkflow(newLocalGoRelease(ref)); err != nil {
+	if len(s.configs) == 0 {
+		http.Error(w, "Unable to create workflow: no workflows configured", http.StatusInternalServerError)
+		return
+	}
+	// Always create the first workflow for now, until we have more.
+	wf := proto.Clone(s.configs[0]).(*reluipb.Workflow)
+	if wf.GetParams() == nil {
+		wf.Params = map[string]string{}
+	}
+	wf.Params["GitObject"] = ref
+	if err := s.store.AddWorkflow(wf); err != nil {
 		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
 		return
 	}
diff --git a/cmd/relui/web_test.go b/cmd/relui/web_test.go
index dfb739a..311a30b 100644
--- a/cmd/relui/web_test.go
+++ b/cmd/relui/web_test.go
@@ -13,6 +13,7 @@
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	reluipb "golang.org/x/build/cmd/relui/protos"
 )
 
 func TestFileServerHandler(t *testing.T) {
@@ -105,6 +106,7 @@
 }
 
 func TestServerCreateWorkflowHandler(t *testing.T) {
+	config := []*reluipb.Workflow{{Name: "test_workflow"}}
 	cases := []struct {
 		desc        string
 		params      url.Values
@@ -132,7 +134,7 @@
 			req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 			w := httptest.NewRecorder()
 
-			s := &server{store: &memoryStore{}}
+			s := &server{store: &memoryStore{}, configs: config}
 			s.createWorkflowHandler(w, req)
 			resp := w.Result()
 
@@ -152,7 +154,7 @@
 			if c.wantParams == nil {
 				return
 			}
-			if diff := cmp.Diff(c.wantParams, s.store.GetWorkflows()[0].Params()); diff != "" {
+			if diff := cmp.Diff(c.wantParams, s.store.GetWorkflows()[0].GetParams()); diff != "" {
 				t.Errorf("s.Store.GetWorkflows()[0].Params() mismatch (-want, +got):\n%s", diff)
 			}
 		})
diff --git a/cmd/relui/workflow.go b/cmd/relui/workflow.go
deleted file mode 100644
index bc96725..0000000
--- a/cmd/relui/workflow.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2020 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 main
-
-import (
-	"fmt"
-	"sync"
-)
-
-type workflow interface {
-	// Params are the list of parameters given when the workflow was created.
-	Params() map[string]string
-	// Title is a human-readable description of a task.
-	Title() string
-	// Tasks are a list of steps in a workflow.
-	Tasks() []task
-}
-
-type task interface {
-	// Title is a human-readable description of a task.
-	Title() string
-	// Status is the current status of the task.
-	Status() string
-}
-
-// newLocalGoRelease creates a localGoRelease workflow.
-func newLocalGoRelease(revision string) *localGoRelease {
-	return &localGoRelease{GitObject: revision, tasks: []task{&fetchGoSource{gitObject: revision}}}
-}
-
-type localGoRelease struct {
-	GitObject string
-	tasks     []task
-}
-
-func (l *localGoRelease) Params() map[string]string {
-	return map[string]string{"GitObject": l.GitObject}
-}
-
-func (l *localGoRelease) Title() string {
-	return fmt.Sprintf("Local Go release (%s)", l.GitObject)
-}
-
-func (l *localGoRelease) Tasks() []task {
-	return l.tasks
-}
-
-// fetchGoSource is a task for fetching the Go repository at a specific commit reference.
-type fetchGoSource struct {
-	gitObject string
-}
-
-func (f *fetchGoSource) Title() string {
-	return "Fetch Go source at " + f.gitObject
-}
-
-func (f *fetchGoSource) Status() string {
-	return "created"
-}
-
-// store is a persistence adapter for saving data. When running locally, this is implemented by memoryStore.
-type store interface {
-	GetWorkflows() []workflow
-	AddWorkflow(workflow) error
-}
-
-// memoryStore is a non-durable implementation of store that keeps everything in memory.
-type memoryStore struct {
-	sync.Mutex
-	Workflows []workflow
-}
-
-// AddWorkflow adds a workflow to the store.
-func (m *memoryStore) AddWorkflow(w workflow) error {
-	m.Lock()
-	defer m.Unlock()
-	m.Workflows = append(m.Workflows, w)
-	return nil
-}
-
-// GetWorkflows returns all workflows stored.
-func (m *memoryStore) GetWorkflows() []workflow {
-	m.Lock()
-	defer m.Unlock()
-	return m.Workflows
-}
diff --git a/cmd/relui/workflows/local_go_release.textpb b/cmd/relui/workflows/local_go_release.textpb
new file mode 100644
index 0000000..06e1b0f
--- /dev/null
+++ b/cmd/relui/workflows/local_go_release.textpb
@@ -0,0 +1,14 @@
+# proto-file: cmd/relui/protos/relui.proto
+# proto-message: Workflow
+
+name: "local_go_release"
+buildable_tasks: [
+  {
+    name: "fetch_go_source"
+    task_type: "FetchGitSource"
+    git_source: {
+      ref: "master"
+      url: "https://go.googlesource.com/go"
+    }
+  }
+]