mirror of
				https://github.com/go-task/task.git
				synced 2025-10-30 23:58:01 +02:00 
			
		
		
		
	feat(defer): expose EXIT_CODE special variable to defer: (#1762)
				
					
				
			Co-authored-by: Dor Sahar <dorsahar@icloud.com>
This commit is contained in:
		| @@ -8,6 +8,8 @@ | ||||
| - Added a CI lint job to ensure that the docs are updated correctly (#1719 by | ||||
|   @vmaerten). | ||||
| - Updated minimum required Go version to 1.22 (#1758 by @pd93). | ||||
| - Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes | ||||
|   with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering). | ||||
|  | ||||
| ## v3.38.0 - 2024-06-30 | ||||
|  | ||||
|   | ||||
| @@ -90,14 +90,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error { | ||||
| 	return r.Run(ctx, p) | ||||
| } | ||||
|  | ||||
| // IsExitError returns true the given error is an exis status error | ||||
| func IsExitError(err error) bool { | ||||
| 	if _, ok := interp.IsExitStatus(err); ok { | ||||
| 		return true | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // Expand is a helper to mvdan.cc/shell.Fields that returns the first field | ||||
| // if available. | ||||
| func Expand(s string) (string, error) { | ||||
|   | ||||
							
								
								
									
										31
									
								
								task.go
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								task.go
									
									
									
									
									
								
							| @@ -11,6 +11,8 @@ import ( | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
|  | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
|  | ||||
| 	"github.com/go-task/task/v3/errors" | ||||
| 	"github.com/go-task/task/v3/internal/compiler" | ||||
| 	"github.com/go-task/task/v3/internal/env" | ||||
| @@ -247,9 +249,11 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error { | ||||
| 			e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err) | ||||
| 		} | ||||
|  | ||||
| 		var deferredExitCode uint8 | ||||
|  | ||||
| 		for i := range t.Cmds { | ||||
| 			if t.Cmds[i].Defer { | ||||
| 				defer e.runDeferred(t, call, i) | ||||
| 				defer e.runDeferred(t, call, i, &deferredExitCode) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| @@ -258,9 +262,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error { | ||||
| 					e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) | ||||
| 				} | ||||
|  | ||||
| 				if execext.IsExitError(err) && t.IgnoreError { | ||||
| 					e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) | ||||
| 					continue | ||||
| 				exitCode, isExitError := interp.IsExitStatus(err) | ||||
| 				if isExitError { | ||||
| 					if t.IgnoreError { | ||||
| 						e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err) | ||||
| 						continue | ||||
| 					} | ||||
| 					deferredExitCode = exitCode | ||||
| 				} | ||||
|  | ||||
| 				if call.Indirect { | ||||
| @@ -312,10 +320,21 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error { | ||||
| 	return g.Wait() | ||||
| } | ||||
|  | ||||
| func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) { | ||||
| func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) { | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	cmd := t.Cmds[i] | ||||
| 	vars, _ := e.Compiler.FastGetVariables(t, call) | ||||
| 	cache := &templater.Cache{Vars: vars} | ||||
| 	extra := map[string]any{} | ||||
|  | ||||
| 	if deferredExitCode != nil && *deferredExitCode > 0 { | ||||
| 		extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode) | ||||
| 	} | ||||
|  | ||||
| 	cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra) | ||||
|  | ||||
| 	if err := e.runCommand(ctx, t, call, i); err != nil { | ||||
| 		e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error()) | ||||
| 	} | ||||
| @@ -372,7 +391,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call, | ||||
| 		if closeErr := close(err); closeErr != nil { | ||||
| 			e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr) | ||||
| 		} | ||||
| 		if execext.IsExitError(err) && cmd.IgnoreError { | ||||
| 		if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError { | ||||
| 			e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err) | ||||
| 			return nil | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										28
									
								
								task_test.go
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								task_test.go
									
									
									
									
									
								
							| @@ -1738,6 +1738,34 @@ task-1 ran successfully | ||||
| 	assert.Contains(t, buff.String(), expectedOutputOrder) | ||||
| } | ||||
|  | ||||
| func TestExitCodeZero(t *testing.T) { | ||||
| 	const dir = "testdata/exit_code" | ||||
| 	var buff bytes.Buffer | ||||
| 	e := task.Executor{ | ||||
| 		Dir:    dir, | ||||
| 		Stdout: &buff, | ||||
| 		Stderr: &buff, | ||||
| 	} | ||||
| 	require.NoError(t, e.Setup()) | ||||
|  | ||||
| 	require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "exit-zero"})) | ||||
| 	assert.Equal(t, "EXIT_CODE=", strings.TrimSpace(buff.String())) | ||||
| } | ||||
|  | ||||
| func TestExitCodeOne(t *testing.T) { | ||||
| 	const dir = "testdata/exit_code" | ||||
| 	var buff bytes.Buffer | ||||
| 	e := task.Executor{ | ||||
| 		Dir:    dir, | ||||
| 		Stdout: &buff, | ||||
| 		Stderr: &buff, | ||||
| 	} | ||||
| 	require.NoError(t, e.Setup()) | ||||
|  | ||||
| 	require.Error(t, e.Run(context.Background(), &ast.Call{Task: "exit-one"})) | ||||
| 	assert.Equal(t, "EXIT_CODE=1", strings.TrimSpace(buff.String())) | ||||
| } | ||||
|  | ||||
| func TestIgnoreNilElements(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name string | ||||
|   | ||||
							
								
								
									
										17
									
								
								testdata/exit_code/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								testdata/exit_code/Taskfile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| version: '3' | ||||
|  | ||||
| silent: true | ||||
|  | ||||
| vars: | ||||
|   PREFIX: EXIT_CODE= | ||||
|  | ||||
| tasks: | ||||
|   exit-zero: | ||||
|     cmds: | ||||
|       - defer: echo {{.PREFIX}}{{.EXIT_CODE}} | ||||
|       - exit 0 | ||||
|  | ||||
|   exit-one: | ||||
|     cmds: | ||||
|       - defer: echo {{.PREFIX}}{{.EXIT_CODE}} | ||||
|       - exit 1 | ||||
| @@ -161,6 +161,12 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, | ||||
| 				} | ||||
| 				continue | ||||
| 			} | ||||
| 			// Defer commands are replaced in a lazy manner because | ||||
| 			// we need to include EXIT_CODE. | ||||
| 			if cmd.Defer { | ||||
| 				new.Cmds = append(new.Cmds, cmd.DeepCopy()) | ||||
| 				continue | ||||
| 			} | ||||
| 			newCmd := cmd.DeepCopy() | ||||
| 			newCmd.Cmd = templater.Replace(cmd.Cmd, cache) | ||||
| 			newCmd.Task = templater.Replace(cmd.Task, cache) | ||||
|   | ||||
| @@ -117,6 +117,7 @@ special variable will be overridden. | ||||
| | `TIMESTAMP`        | The date object of the greatest timestamp of the files listed in `sources`. Only available within the `status` prop and if method is set to `timestamp`. | | ||||
| | `TASK_VERSION`     | The current version of task.                                                                                                                             | | ||||
| | `ITEM`             | The value of the current iteration when using the `for` property. Can be changed to a different variable name using `as:`.                               | | ||||
| | `EXIT_CODE`        | Available exclusively inside the `defer:` command. Contains the failed command exit code. Only set when non-zero.                                        | | ||||
|  | ||||
| ## Functions | ||||
|  | ||||
|   | ||||
| @@ -1520,6 +1520,20 @@ commands are executed in the reverse order if you schedule multiple of them. | ||||
|  | ||||
| ::: | ||||
|  | ||||
| A special variable `.EXIT_CODE` is exposed when a command exited with a non-zero | ||||
| exit code. You can check its presence to know if the task completed successfully | ||||
| or not: | ||||
|  | ||||
| ```yaml | ||||
| version: '3' | ||||
|  | ||||
| tasks: | ||||
|   default: | ||||
|     cmds: | ||||
|       - defer: echo '{{if .EXIT_CODE}}Failed with {{.EXIT_CODE}}!{{else}}Success!{{end}}' | ||||
|       - exit 1 | ||||
| ``` | ||||
|  | ||||
| ## Help | ||||
|  | ||||
| Running `task --list` (or `task -l`) lists all tasks with a description. The | ||||
|   | ||||
		Reference in New Issue
	
	Block a user