From 2655775a8ff2e676b366ef41d09c98ef81914a72 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 23:34:33 +0000 Subject: [PATCH] Fix loop task finalisation --- executor.go | 39 +++++++++++++++++++++++++++++++++-- executor_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/executor.go b/executor.go index 7c8a939..96b1484 100644 --- a/executor.go +++ b/executor.go @@ -663,7 +663,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin // Handle loops, including legacy with_file, with_fileglob, and with_sequence syntax. if task.Loop != nil || task.WithFile != nil || task.WithFileGlob != nil || task.WithSequence != nil { - return e.runLoop(ctx, host, client, task, play) + return e.runLoop(ctx, host, client, task, play, start) } // Execute the task, honouring retries/until when configured. @@ -853,7 +853,7 @@ func shouldRetryTask(task *Task, host string, e *Executor, result *TaskResult) b } // runLoop handles task loops. -func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorClient, task *Task, play *Play) error { +func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorClient, task *Task, play *Play, start time.Time) error { var ( items []any err error @@ -991,9 +991,44 @@ func (e *Executor) runLoop(ctx context.Context, host string, client sshExecutorC combined.Failed = true } } + combined.Duration = time.Since(start) e.results[host][task.Register] = combined } + result := &TaskResult{ + Results: results, + Changed: false, + } + for _, r := range results { + if r.Changed { + result.Changed = true + } + if r.Failed { + result.Failed = true + } + } + result.Duration = time.Since(start) + + displayResult := result + if task.NoLog { + displayResult = redactTaskResult(result) + } + + if result.Changed && task.Notify != nil { + e.handleNotify(task.Notify) + } + + if e.OnTaskEnd != nil { + e.OnTaskEnd(host, task, displayResult) + } + + if result.Failed { + e.markBatchHostFailed(host) + if !task.IgnoreErrors { + return taskFailureError(task, result) + } + } + return nil } diff --git a/executor_test.go b/executor_test.go index b87b158..32a3ee6 100644 --- a/executor_test.go +++ b/executor_test.go @@ -919,6 +919,59 @@ func TestExecutor_RunTaskOnHost_Good_LoopFromWithNested(t *testing.T) { assert.Equal(t, "blue-large", result.Results[3].Msg) } +func TestExecutor_RunTaskOnHosts_Good_LoopNotifiesAndCallsCallback(t *testing.T) { + e := NewExecutor("/tmp") + e.clients["host1"] = NewMockSSHClient() + + var ended *TaskResult + task := &Task{ + Name: "Looped change", + Module: "set_fact", + Args: map[string]any{ + "changed_flag": true, + }, + Loop: []any{"one", "two"}, + Notify: "restart app", + } + play := &Play{ + Handlers: []Task{ + { + Name: "restart app", + Module: "debug", + Args: map[string]any{"msg": "handler"}, + }, + }, + } + + e.OnTaskEnd = func(_ string, _ *Task, result *TaskResult) { + ended = result + } + + err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, play) + require.NoError(t, err) + + require.NotNil(t, ended) + assert.True(t, ended.Changed) + assert.Len(t, ended.Results, 2) + assert.True(t, e.notified["restart app"]) +} + +func TestExecutor_RunTaskOnHosts_Bad_LoopFailurePropagates(t *testing.T) { + e := NewExecutor("/tmp") + e.clients["host1"] = NewMockSSHClient() + + task := &Task{ + Name: "Looped failure", + Module: "fail", + Args: map[string]any{"msg": "bad"}, + Loop: []any{"one", "two"}, + } + + err := e.runTaskOnHosts(context.Background(), []string{"host1"}, task, &Play{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "task failed") +} + func TestExecutor_RunTaskWithRetries_Good_UntilSuccess(t *testing.T) { e := NewExecutor("/tmp") attempts := 0