Skip to content

Commit b259ede

Browse files
feat(defer): expose EXIT_CODE special variable to defer: (#1762)
Co-authored-by: Dor Sahar <[email protected]>
1 parent 35119c1 commit b259ede

File tree

8 files changed

+93
-14
lines changed

8 files changed

+93
-14
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- Added a CI lint job to ensure that the docs are updated correctly (#1719 by
99
@vmaerten).
1010
- Updated minimum required Go version to 1.22 (#1758 by @pd93).
11+
- Expose a new `EXIT_CODE` special variable on `defer:` when a command finishes
12+
with a non-zero exit code (#1484, #1762 by @dorimon-1 and @andreynering).
1113

1214
## v3.38.0 - 2024-06-30
1315

internal/execext/exec.go

-8
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,6 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
9090
return r.Run(ctx, p)
9191
}
9292

93-
// IsExitError returns true the given error is an exis status error
94-
func IsExitError(err error) bool {
95-
if _, ok := interp.IsExitStatus(err); ok {
96-
return true
97-
}
98-
return false
99-
}
100-
10193
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
10294
// if available.
10395
func Expand(s string) (string, error) {

task.go

+25-6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"sync/atomic"
1212
"time"
1313

14+
"mvdan.cc/sh/v3/interp"
15+
1416
"github.com/go-task/task/v3/errors"
1517
"github.com/go-task/task/v3/internal/compiler"
1618
"github.com/go-task/task/v3/internal/env"
@@ -247,9 +249,11 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
247249
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v\n", t.Dir, err)
248250
}
249251

252+
var deferredExitCode uint8
253+
250254
for i := range t.Cmds {
251255
if t.Cmds[i].Defer {
252-
defer e.runDeferred(t, call, i)
256+
defer e.runDeferred(t, call, i, &deferredExitCode)
253257
continue
254258
}
255259

@@ -258,9 +262,13 @@ func (e *Executor) RunTask(ctx context.Context, call *ast.Call) error {
258262
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2)
259263
}
260264

261-
if execext.IsExitError(err) && t.IgnoreError {
262-
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
263-
continue
265+
exitCode, isExitError := interp.IsExitStatus(err)
266+
if isExitError {
267+
if t.IgnoreError {
268+
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v\n", err)
269+
continue
270+
}
271+
deferredExitCode = exitCode
264272
}
265273

266274
if call.Indirect {
@@ -312,10 +320,21 @@ func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error {
312320
return g.Wait()
313321
}
314322

315-
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int) {
323+
func (e *Executor) runDeferred(t *ast.Task, call *ast.Call, i int, deferredExitCode *uint8) {
316324
ctx, cancel := context.WithCancel(context.Background())
317325
defer cancel()
318326

327+
cmd := t.Cmds[i]
328+
vars, _ := e.Compiler.FastGetVariables(t, call)
329+
cache := &templater.Cache{Vars: vars}
330+
extra := map[string]any{}
331+
332+
if deferredExitCode != nil && *deferredExitCode > 0 {
333+
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
334+
}
335+
336+
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
337+
319338
if err := e.runCommand(ctx, t, call, i); err != nil {
320339
e.Logger.VerboseErrf(logger.Yellow, "task: ignored error in deferred cmd: %s\n", err.Error())
321340
}
@@ -372,7 +391,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *ast.Call,
372391
if closeErr := close(err); closeErr != nil {
373392
e.Logger.Errf(logger.Red, "task: unable to close writer: %v\n", closeErr)
374393
}
375-
if execext.IsExitError(err) && cmd.IgnoreError {
394+
if _, isExitError := interp.IsExitStatus(err); isExitError && cmd.IgnoreError {
376395
e.Logger.VerboseErrf(logger.Yellow, "task: [%s] command error ignored: %v\n", t.Name(), err)
377396
return nil
378397
}

task_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,34 @@ task-1 ran successfully
17381738
assert.Contains(t, buff.String(), expectedOutputOrder)
17391739
}
17401740

1741+
func TestExitCodeZero(t *testing.T) {
1742+
const dir = "testdata/exit_code"
1743+
var buff bytes.Buffer
1744+
e := task.Executor{
1745+
Dir: dir,
1746+
Stdout: &buff,
1747+
Stderr: &buff,
1748+
}
1749+
require.NoError(t, e.Setup())
1750+
1751+
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: "exit-zero"}))
1752+
assert.Equal(t, "EXIT_CODE=", strings.TrimSpace(buff.String()))
1753+
}
1754+
1755+
func TestExitCodeOne(t *testing.T) {
1756+
const dir = "testdata/exit_code"
1757+
var buff bytes.Buffer
1758+
e := task.Executor{
1759+
Dir: dir,
1760+
Stdout: &buff,
1761+
Stderr: &buff,
1762+
}
1763+
require.NoError(t, e.Setup())
1764+
1765+
require.Error(t, e.Run(context.Background(), &ast.Call{Task: "exit-one"}))
1766+
assert.Equal(t, "EXIT_CODE=1", strings.TrimSpace(buff.String()))
1767+
}
1768+
17411769
func TestIgnoreNilElements(t *testing.T) {
17421770
tests := []struct {
17431771
name string

testdata/exit_code/Taskfile.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
version: '3'
2+
3+
silent: true
4+
5+
vars:
6+
PREFIX: EXIT_CODE=
7+
8+
tasks:
9+
exit-zero:
10+
cmds:
11+
- defer: echo {{.PREFIX}}{{.EXIT_CODE}}
12+
- exit 0
13+
14+
exit-one:
15+
cmds:
16+
- defer: echo {{.PREFIX}}{{.EXIT_CODE}}
17+
- exit 1

variables.go

+6
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task,
161161
}
162162
continue
163163
}
164+
// Defer commands are replaced in a lazy manner because
165+
// we need to include EXIT_CODE.
166+
if cmd.Defer {
167+
new.Cmds = append(new.Cmds, cmd.DeepCopy())
168+
continue
169+
}
164170
newCmd := cmd.DeepCopy()
165171
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
166172
newCmd.Task = templater.Replace(cmd.Task, cache)

website/docs/reference/templating.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ special variable will be overridden.
117117
| `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`. |
118118
| `TASK_VERSION` | The current version of task. |
119119
| `ITEM` | The value of the current iteration when using the `for` property. Can be changed to a different variable name using `as:`. |
120+
| `EXIT_CODE` | Available exclusively inside the `defer:` command. Contains the failed command exit code. Only set when non-zero. |
120121

121122
## Functions
122123

website/docs/usage.mdx

+14
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,20 @@ commands are executed in the reverse order if you schedule multiple of them.
15201520

15211521
:::
15221522

1523+
A special variable `.EXIT_CODE` is exposed when a command exited with a non-zero
1524+
exit code. You can check its presence to know if the task completed successfully
1525+
or not:
1526+
1527+
```yaml
1528+
version: '3'
1529+
1530+
tasks:
1531+
default:
1532+
cmds:
1533+
- defer: echo '{{if .EXIT_CODE}}Failed with {{.EXIT_CODE}}!{{else}}Success!{{end}}'
1534+
- exit 1
1535+
```
1536+
15231537
## Help
15241538

15251539
Running `task --list` (or `task -l`) lists all tasks with a description. The

0 commit comments

Comments
 (0)