mirror of
https://github.com/go-task/task.git
synced 2025-02-03 13:22:11 +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:
parent
35119c12ab
commit
b259edeb65
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user