internal/relui: record and display workflow errors

This changes the postgres workflow listener to record workflow errors on
workflow completion. It adds the workflow state, output, and errors to
the UI.

Updates golang/go#53207

Change-Id: Idac3eace1b06b05de89dc634e9b58df66157cf22
Reviewed-on: https://go-review.googlesource.com/c/build/+/410237
Reviewed-by: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alex Rakoczy <alex@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/internal/relui/db/workflows.sql.go b/internal/relui/db/workflows.sql.go
index 98ee991..19e9a8f 100644
--- a/internal/relui/db/workflows.sql.go
+++ b/internal/relui/db/workflows.sql.go
@@ -363,7 +363,8 @@
 UPDATE workflows
 SET finished   = $2,
     output     = $3,
-    updated_at = $4
+    error      = $4,
+    updated_at = $5
 WHERE workflows.id = $1
 RETURNING id, params, name, created_at, updated_at, finished, output, error
 `
@@ -372,6 +373,7 @@
 	ID        uuid.UUID
 	Finished  bool
 	Output    string
+	Error     string
 	UpdatedAt time.Time
 }
 
@@ -380,6 +382,7 @@
 		arg.ID,
 		arg.Finished,
 		arg.Output,
+		arg.Error,
 		arg.UpdatedAt,
 	)
 	var i Workflow
diff --git a/internal/relui/listener.go b/internal/relui/listener.go
index d57ddd3..8cae750 100644
--- a/internal/relui/listener.go
+++ b/internal/relui/listener.go
@@ -79,19 +79,23 @@
 
 // WorkflowFinished saves the final state of a workflow after its run
 // has completed.
-func (l *PGListener) WorkflowFinished(ctx context.Context, workflowID uuid.UUID, outputs map[string]interface{}, err error) error {
-	log.Printf("WorkflowCompleted(%q, %v, %q)", workflowID, outputs, err)
+func (l *PGListener) WorkflowFinished(ctx context.Context, workflowID uuid.UUID, outputs map[string]interface{}, workflowErr error) error {
+	log.Printf("WorkflowCompleted(%q, %v, %q)", workflowID, outputs, workflowErr)
 	q := db.New(l.db)
 	m, err := json.Marshal(outputs)
 	if err != nil {
 		return err
 	}
-	_, err = q.WorkflowFinished(ctx, db.WorkflowFinishedParams{
+	wp := db.WorkflowFinishedParams{
 		ID:        workflowID,
 		Finished:  true,
 		Output:    string(m),
 		UpdatedAt: time.Now(),
-	})
+	}
+	if workflowErr != nil {
+		wp.Error = workflowErr.Error()
+	}
+	_, err = q.WorkflowFinished(ctx, wp)
 	return err
 }
 
diff --git a/internal/relui/queries/workflows.sql b/internal/relui/queries/workflows.sql
index bb59c19..de78bea 100644
--- a/internal/relui/queries/workflows.sql
+++ b/internal/relui/queries/workflows.sql
@@ -71,7 +71,8 @@
 UPDATE workflows
 SET finished   = $2,
     output     = $3,
-    updated_at = $4
+    error      = $4,
+    updated_at = $5
 WHERE workflows.id = $1
 RETURNING *;
 
diff --git a/internal/relui/static/styles.css b/internal/relui/static/styles.css
index 7dce7f7..50aca0e 100644
--- a/internal/relui/static/styles.css
+++ b/internal/relui/static/styles.css
@@ -81,6 +81,22 @@
   border: none;
   border-spacing: 0;
 }
+.WorkflowList-workflowStateIcon {
+  background-size: contain;
+  display: inline-block;
+  height: 1.25rem;
+  margin: -0.25rem 0;
+  width: 1.25rem;
+}
+.Workflowlist-workflowStateIcon--error {
+  background-image: url("images/error_red_24dp.svg");
+}
+.Workflowlist-workflowStateIcon--success {
+  background-image: url("images/check_circle_green_24dp.svg");
+}
+.Workflowlist-workflowStateIcon--pending {
+  background-image: url("images/pending_yellow_24dp.svg");
+}
 .WorkflowList-titleTime {
   font-size: 1rem;
 }
diff --git a/internal/relui/templates/home.html b/internal/relui/templates/home.html
index a7156aa..3ba985b 100644
--- a/internal/relui/templates/home.html
+++ b/internal/relui/templates/home.html
@@ -20,9 +20,32 @@
           </h3>
           <table class="WorkflowList-params">
             <tbody>
+              <tr>
+                <td>State:</td>
+                <td class="WorkflowList-paramData">
+                  {{if $workflow.Error}}
+                    Error
+                    <div class="WorkflowList-workflowStateIcon Workflowlist-workflowStateIcon--error"></div>
+                  {{else if $workflow.Finished}}
+                    Success
+                    <div class="WorkflowList-workflowStateIcon Workflowlist-workflowStateIcon--success"></div>
+                  {{else}}
+                    Pending
+                    <div class="WorkflowList-workflowStateIcon Workflowlist-workflowStateIcon--pending"></div>
+                  {{end}}
+                </td>
+              </tr>
+              <tr>
+                <td>Output:</td>
+                <td class="WorkflowList-paramData">{{$workflow.Output}}</td>
+              </tr>
+              <tr>
+                <td>Error:</td>
+                <td class="WorkflowList-paramData">{{$workflow.Error}}</td>
+              </tr>
               {{range $name, $value := $.WorkflowParams $workflow}}
                 <tr>
-                  <td class="WorkflowList-paramData">{{$name}}:</td>
+                  <td>{{$name}}:</td>
                   <td class="WorkflowList-paramData">{{$value}}</td>
                 </tr>
               {{end}}