From f2c95eb3a7d33fff2816a5e6bd2b0cb462062e69 Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Sat, 2 Aug 2025 00:46:53 +0200 Subject: [PATCH 1/3] Defer tasks until named point 'exit'. --- call.go | 1 + executor.go | 7 +++++++ task.go | 22 ++++++++++++++++++++-- taskfile/ast/cmd.go | 2 ++ taskfile/ast/defer.go | 3 +++ website/docs/reference/schema.mdx | 4 +++- website/docs/usage.mdx | 15 +++++++++++++++ website/static/next-schema.json | 8 ++++++++ 8 files changed, 59 insertions(+), 3 deletions(-) diff --git a/call.go b/call.go index a0b357185c..0724865f70 100644 --- a/call.go +++ b/call.go @@ -8,4 +8,5 @@ type Call struct { Vars *ast.Vars Silent bool Indirect bool // True if the task was called by another task + When string } diff --git a/executor.go b/executor.go index 8f9233ef88..7b6012d493 100644 --- a/executor.go +++ b/executor.go @@ -71,11 +71,17 @@ type ( executionHashes map[string]context.Context executionHashesMutex sync.Mutex watchedDirs *xsync.MapOf[string, bool] + whenTasks map[string][]WhenTaskCall } TempDir struct { Remote string Fingerprint string } + WhenTaskCall struct { + Task *ast.Task + Call *Call + Index int + } ) // NewExecutor creates a new [Executor] and applies the given functional options @@ -98,6 +104,7 @@ func NewExecutor(opts ...ExecutorOption) *Executor { mkdirMutexMap: map[string]*sync.Mutex{}, executionHashes: map[string]context.Context{}, executionHashesMutex: sync.Mutex{}, + whenTasks: map[string][]WhenTaskCall{}, } e.Options(opts...) return e diff --git a/task.go b/task.go index fc3f17662d..33913c51f5 100644 --- a/task.go +++ b/task.go @@ -97,6 +97,15 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error { return e.watchTasks(watchCalls...) } + if whenList, ok := e.whenTasks["exit"]; ok { + for _, w := range whenList { + if err := e.runCommand(ctx, w.Task, w.Call, w.Index); err != nil { + e.Logger.Errf(logger.Red, "task: error running when[%v] task call", "exit", err) + } + } + delete(e.whenTasks, "exit") + } + return nil } @@ -211,10 +220,19 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { for i := range t.Cmds { if t.Cmds[i].Defer { - defer e.runDeferred(t, call, i, &deferredExitCode) + if len(call.When) > 0 { + if _, ok := e.whenTasks[call.When]; !ok { + e.whenTasks[call.When] = []WhenTaskCall{} + } + e.whenTasks[call.When] = append(e.whenTasks[call.When], WhenTaskCall{Task: t, Call: call, Index: i}) + } else { + defer e.runDeferred(t, call, i, &deferredExitCode) + } continue } + // TODO push to exit defer list + if err := e.runCommand(ctx, t, call, i); err != nil { if err2 := e.statusOnError(t); err2 != nil { e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) @@ -313,7 +331,7 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in reacquire := e.releaseConcurrencyLimit() defer reacquire() - err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) + err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true, When: cmd.When}) if err != nil { return err } diff --git a/taskfile/ast/cmd.go b/taskfile/ast/cmd.go index 3dab193c55..c8a4a7b2f0 100644 --- a/taskfile/ast/cmd.go +++ b/taskfile/ast/cmd.go @@ -19,6 +19,7 @@ type Cmd struct { IgnoreError bool Defer bool Platforms []*Platform + When string } func (c *Cmd) DeepCopy() *Cmd { @@ -73,6 +74,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { c.Defer = true c.Cmd = cmdStruct.Defer.Cmd c.Silent = cmdStruct.Silent + c.When = cmdStruct.Defer.When return nil } diff --git a/taskfile/ast/defer.go b/taskfile/ast/defer.go index 5705de445d..15eb7d54ed 100644 --- a/taskfile/ast/defer.go +++ b/taskfile/ast/defer.go @@ -11,6 +11,7 @@ type Defer struct { Task string Vars *Vars Silent bool + When string } func (d *Defer) UnmarshalYAML(node *yaml.Node) error { @@ -30,6 +31,7 @@ func (d *Defer) UnmarshalYAML(node *yaml.Node) error { Task string Vars *Vars Silent bool + When string } if err := node.Decode(&deferStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) @@ -38,6 +40,7 @@ func (d *Defer) UnmarshalYAML(node *yaml.Node) error { d.Task = deferStruct.Task d.Vars = deferStruct.Vars d.Silent = deferStruct.Silent + d.When = deferStruct.When return nil } diff --git a/website/docs/reference/schema.mdx b/website/docs/reference/schema.mdx index 8339f5100e..800a673270 100644 --- a/website/docs/reference/schema.mdx +++ b/website/docs/reference/schema.mdx @@ -141,7 +141,7 @@ tasks: | `silent` | `bool` | `false` | Skips some output for this command. Note that STDOUT and STDERR of the commands will still be redirected. | | `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. | -| `defer` | [`Defer`](#defer) | | Alternative to `cmd`, but schedules the command or a task to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. | +| `defer` | [`Defer`](#defer) | | Alternative to `cmd`, but schedules the command or a task to be executed at the end of this task, or a named point (e.g. `exit`), instead of immediately. This cannot be used together with `cmd`. | | `platforms` | `[]string` | All platforms | Specifies which platforms the command should be run on. [Valid GOOS and GOARCH values allowed](https://github.com/golang/go/blob/master/src/internal/syslist/syslist.go). Command will be skipped otherwise. | | `set` | `[]string` | | Specify options for the [`set` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html). | | `shopt` | `[]string` | | Specify option for the [`shopt` builtin](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html). | @@ -184,6 +184,7 @@ tasks: ### Defer The `defer` parameter defines a shell command to run, or a task to trigger, at the end of the current task instead of immediately. +A task may also be deffered to run at a later named point. If defined as a string this is a shell command, otherwise it is a map defining a task to call: | Attribute | Type | Default | Description | @@ -191,6 +192,7 @@ If defined as a string this is a shell command, otherwise it is a map defining a | `task` | `string` | | The deferred task to trigger. | | `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the deferred task. | | `silent` | `bool` | `false` | Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`. | +| `when` | `string` | | Defer the task to a later named point (e.g. `exit`). | ### For diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 9590cf4f25..951c122a4a 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -1804,6 +1804,21 @@ tasks: cleanup: rm -rf tmpdir/ ``` +Additionally the cleanup task can be deferred until task exits: + +```yaml +version: '3' + +tasks: + default: + cmds: + - mkdir -p tmpdir/ + - defer: { task: cleanup, when: exit } + - echo 'Do work on tmpdir/' + + cleanup: rm -rf tmpdir/ +``` + :::info Due to the nature of how the diff --git a/website/static/next-schema.json b/website/static/next-schema.json index 0a229bedc9..2b15d5dacd 100644 --- a/website/static/next-schema.json +++ b/website/static/next-schema.json @@ -317,6 +317,10 @@ "silent": { "description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.", "type": "boolean" + }, + "when": { + "description": "Defer the task or command to a later named point (e.g. `exit`).", + "type": "string" } }, "additionalProperties": false, @@ -384,6 +388,10 @@ "silent": { "description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.", "type": "boolean" + }, + "when": { + "description": "Defer the task or command to a later named point (e.g. `exit`).", + "type": "string" } }, "additionalProperties": false, From 453f3239701bb3af6e39ab44bdc0353eff79b3ec Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:55:20 +0200 Subject: [PATCH 2/3] Remove development note. --- task.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/task.go b/task.go index 33913c51f5..bc0dcdae0f 100644 --- a/task.go +++ b/task.go @@ -231,8 +231,6 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { continue } - // TODO push to exit defer list - if err := e.runCommand(ctx, t, call, i); err != nil { if err2 := e.statusOnError(t); err2 != nil { e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v\n", err2) From ee6f74a74d318f69ddfcba2ceb5461f803dc76ca Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:44:28 +0200 Subject: [PATCH 3/3] Check for flaky test. No code change. --- task.go | 1 - 1 file changed, 1 deletion(-) diff --git a/task.go b/task.go index bc0dcdae0f..6c381d1d76 100644 --- a/task.go +++ b/task.go @@ -105,7 +105,6 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error { } delete(e.whenTasks, "exit") } - return nil }