Skip to content

Commit

Permalink
feat(defer): expose EXIT_CODE special variable to defer: (#1762)
Browse files Browse the repository at this point in the history
Co-authored-by: Dor Sahar <[email protected]>
  • Loading branch information
andreynering and dorimon-1 authored Aug 15, 2024
1 parent 35119c1 commit b259ede
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 0 additions & 8 deletions internal/execext/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
31 changes: 25 additions & 6 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
}
Expand Down
28 changes: 28 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions testdata/exit_code/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions website/docs/reference/templating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions website/docs/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b259ede

Please sign in to comment.