Repository: go-task/task
Pull request #755 (2022-06-02 13:01:32)

Add --exit-code (-x) flag to enable carrying error codes from task cmds
Hello,

I recently found the need to get the return code that was returned from a failed command from a task's cmds. After investigating quickly it appeared to me that the task tool would always return with status code 1 when an error occurred during a task.

This PR proposes an additional command flag (--carry), false by default, that will carry the error code, if any, over to the calling shell.


Patch:
diff --git a/cmd/task/task.go b/cmd/task/task.go
index 20e4d612..3c0fc609 100644
--- a/cmd/task/task.go
+++ b/cmd/task/task.go
@@ -68,6 +68,7 @@ func main() {
 		silent      bool
 		dry         bool
 		summary     bool
+		exitCode    bool
 		parallel    bool
 		concurrency int
 		dir         string
@@ -89,6 +90,7 @@ func main() {
 	pflag.BoolVarP(&parallel, "parallel", "p", false, "executes tasks provided on command line in parallel")
 	pflag.BoolVar(&dry, "dry", false, "compiles and prints tasks in the order that they would be run, without executing them")
 	pflag.BoolVar(&summary, "summary", false, "show summary about a task")
+	pflag.BoolVarP(&exitCode, "exit-code", "x", false, "pass-through the exit code of the task command")
 	pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
 	pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
 	pflag.StringVarP(&output.Name, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
@@ -216,7 +218,13 @@ func main() {
 
 	if err := e.Run(ctx, calls...); err != nil {
 		e.Logger.Errf(logger.Red, "%v", err)
-		os.Exit(1)
+		code := 1
+		if exitCode {
+			if tre, ok := err.(*task.TaskRunError); ok {
+				code = tre.ExitCode()
+			}
+		}
+		os.Exit(code)
 	}
 }
 
diff --git a/errors.go b/errors.go
index a12df253..e2aa16b0 100644
--- a/errors.go
+++ b/errors.go
@@ -3,6 +3,7 @@ package task
 import (
 	"errors"
 	"fmt"
+	"mvdan.cc/sh/v3/interp"
 )
 
 var (
@@ -18,15 +19,23 @@ func (err *taskNotFoundError) Error() string {
 	return fmt.Sprintf(`task: Task "%s" not found`, err.taskName)
 }
 
-type taskRunError struct {
+type TaskRunError struct {
 	taskName string
 	err      error
 }
 
-func (err *taskRunError) Error() string {
+func (err *TaskRunError) Error() string {
 	return fmt.Sprintf(`task: Failed to run task "%s": %v`, err.taskName, err.err)
 }
 
+func (err *TaskRunError) ExitCode() int {
+	if c, ok := interp.IsExitStatus(err.err); ok {
+		return int(c)
+	}
+
+	return 1
+}
+
 // MaximumTaskCallExceededError is returned when a task is called too
 // many times. In this case you probably have a cyclic dependendy or
 // infinite loop
diff --git a/task.go b/task.go
index ec224260..6714d8e3 100644
--- a/task.go
+++ b/task.go
@@ -363,7 +363,7 @@ func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
 					continue
 				}
 
-				return &taskRunError{t.Task, err}
+				return &TaskRunError{t.Task, err}
 			}
 		}
 		e.Logger.VerboseErrf(logger.Magenta, `task: "%s" finished`, call.Task)
diff --git a/watch.go b/watch.go
index 78ca06d9..b93e2d30 100644
--- a/watch.go
+++ b/watch.go
@@ -88,7 +88,7 @@ func (e *Executor) watchTasks(calls ...taskfile.Call) error {
 }
 
 func isContextError(err error) bool {
-	if taskRunErr, ok := err.(*taskRunError); ok {
+	if taskRunErr, ok := err.(*TaskRunError); ok {
 		err = taskRunErr.err
 	}
 


Test patch:
diff --git a/task_test.go b/task_test.go
index a6a47b76..1094ea65 100644
--- a/task_test.go
+++ b/task_test.go
@@ -1275,3 +1275,22 @@ VAR_2 is included-default-var2
 	t.Log(buff.String())
 	assert.Equal(t, strings.TrimSpace(buff.String()), expectedOutputOrder)
 }
+
+func TestErrorCode(t *testing.T) {
+	const dir = "testdata/error_code"
+
+	var buff bytes.Buffer
+	e := &task.Executor{
+		Dir:    dir,
+		Stdout: &buff,
+		Stderr: &buff,
+		Silent: true,
+	}
+	assert.NoError(t, e.Setup())
+
+	err := e.Run(context.Background(), taskfile.Call{Task: "test-exit-code"})
+	assert.Error(t, err)
+	casted, ok := err.(*task.TaskRunError)
+	assert.True(t, ok, "cannot cast returned error to *task.TaskRunError")
+	assert.Equal(t, 42, casted.ExitCode(), "unexpected exit code from task")
+}
diff --git a/testdata/error_code/Taskfile.yml b/testdata/error_code/Taskfile.yml
new file mode 100644
index 00000000..b2c83ff8
--- /dev/null
+++ b/testdata/error_code/Taskfile.yml
@@ -0,0 +1,6 @@
+version: '3'
+
+tasks:
+  test-exit-code:
+    cmds:
+      - exit 42