internal/workflow: always use JSON-marshaled task results

We only realized JSON marshaling of the release tasks was broken when
we retried them because in normal operation we used the in-memory
versions. Change to always use the marshaled results.

For golang/go#51797.

Change-Id: I9dbca62520732a7b00d64dc2481ab35b221fecf0
Reviewed-on: https://go-review.googlesource.com/c/build/+/411894
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go
index b6d11bf..3ef4da7 100644
--- a/internal/workflow/workflow.go
+++ b/internal/workflow/workflow.go
@@ -518,11 +518,11 @@
 			serializedResult: tState.SerializedResult,
 		}
 		if state.serializedResult != nil {
-			ptr := reflect.New(reflect.ValueOf(taskDef.f).Type().Out(0))
-			if err := json.Unmarshal(tState.SerializedResult, ptr.Interface()); err != nil {
+			result, err := unmarshalNew(reflect.ValueOf(taskDef.f).Type().Out(0), tState.SerializedResult)
+			if err != nil {
 				return nil, fmt.Errorf("failed to unmarshal result of %v: %v", taskDef.name, err)
 			}
-			state.result = ptr.Elem().Interface()
+			state.result = result
 		}
 		if tState.Error != "" {
 			state.err = fmt.Errorf("serialized error: %v", tState.Error) // untyped, but hopefully that doesn't matter.
@@ -532,6 +532,14 @@
 	return w, nil
 }
 
+func unmarshalNew(t reflect.Type, data []byte) (interface{}, error) {
+	ptr := reflect.New(t)
+	if err := json.Unmarshal(data, ptr.Interface()); err != nil {
+		return nil, err
+	}
+	return ptr.Elem().Interface(), nil
+}
+
 // Run runs a workflow to completion or quiescence and returns its outputs.
 // listener.TaskStateChanged will be called immediately, when each task starts,
 // and when they finish. It should be used only for monitoring and persistence
@@ -612,17 +620,21 @@
 		WorkflowID: w.ID,
 	}
 	in := append([]reflect.Value{reflect.ValueOf(tctx)}, args...)
-	out := reflect.ValueOf(state.def.f).Call(in)
+	fv := reflect.ValueOf(state.def.f)
+	out := fv.Call(in)
 
 	if errIdx := len(out) - 1; !out[errIdx].IsNil() {
 		state.err = out[errIdx].Interface().(error)
 	}
 	state.finished = true
-	if len(out) == 2 {
-		state.result = out[0].Interface()
-	}
-	if state.err == nil {
-		state.serializedResult, state.err = json.Marshal(state.result)
+	if len(out) == 2 && state.err == nil {
+		state.serializedResult, state.err = json.Marshal(out[0].Interface())
+		if state.err == nil {
+			state.result, state.err = unmarshalNew(fv.Type().Out(0), state.serializedResult)
+		}
+		if state.err == nil && !reflect.DeepEqual(out[0].Interface(), state.result) {
+			state.err = fmt.Errorf("JSON marshaling changed result from %#v to %#v", out[0].Interface(), state.result)
+		}
 	}
 	return state
 }
diff --git a/internal/workflow/workflow_test.go b/internal/workflow/workflow_test.go
index b0cc255..7630476 100644
--- a/internal/workflow/workflow_test.go
+++ b/internal/workflow/workflow_test.go
@@ -342,6 +342,23 @@
 	})
 }
 
+type badResult struct {
+	unexported string
+}
+
+func TestBadMarshaling(t *testing.T) {
+	greet := func(_ context.Context) (badResult, error) {
+		return badResult{"hi"}, nil
+	}
+
+	wd := workflow.New()
+	wd.Output("greeting", wd.Task("greet", greet))
+	w := startWorkflow(t, wd, nil)
+	if _, err := w.Run(context.Background(), &verboseListener{t}); err == nil {
+		t.Errorf("running a workflow with bad JSON should give an error, got none")
+	}
+}
+
 type mapListener struct {
 	workflow.Listener
 	states map[uuid.UUID]map[string]*workflow.TaskState