From 539eeae0da795b3edbf74e8b5a0ef3a6935e6afa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:22:29 +0000 Subject: [PATCH 1/4] Initial plan From 224e1671b3661087eb7369235691660eaf265a3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:30:13 +0000 Subject: [PATCH 2/4] Add per-task lifecycle hooks (onSuccess, onFailure) Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> Agent-Logs-Url: https://github.com/kitproj/kit/sessions/553f2363-3908-4c91-ba67-90c952a74bf0 --- internal/run.go | 26 ++++++++++++++ internal/types/lifecycle.go | 31 ++++++++++++++++ internal/types/lifecycle_test.go | 62 ++++++++++++++++++++++++++++++++ internal/types/task.go | 18 ++++++++++ 4 files changed, 137 insertions(+) create mode 100644 internal/types/lifecycle.go create mode 100644 internal/types/lifecycle_test.go diff --git a/internal/run.go b/internal/run.go index 44d7e5a..07bbf78 100644 --- a/internal/run.go +++ b/internal/run.go @@ -7,6 +7,7 @@ import ( "io" "log" "os" + "os/exec" "path/filepath" "strings" "sync" @@ -507,6 +508,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB if err != nil { setNodeStatus(node, "failed", fmt.Sprint(err)) + runLifecycleHook(ctx, t, t.GetOnFailureHook(), out, logger) if t.GetRestartPolicy() != "Never" { restart() } @@ -514,6 +516,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } setNodeStatus(node, "succeeded", "") + runLifecycleHook(ctx, t, t.GetOnSuccessHook(), out, logger) if t.GetRestartPolicy() == "Always" { restart() } @@ -526,3 +529,26 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } } } + +// runLifecycleHook runs the given lifecycle hook command, logging any errors. +// It is a best-effort operation: if the hook command fails, the error is logged +// but does not affect the task's outcome. +func runLifecycleHook(ctx context.Context, t types.Task, hook *types.LifecycleHook, out io.Writer, logger *log.Logger) { + cmd := hook.GetCommand() + if len(cmd) == 0 { + return + } + environ, err := types.Environ(types.Spec{}, t) + if err != nil { + logger.Printf("lifecycle hook: failed to get environment: %v", err) + return + } + c := exec.CommandContext(ctx, cmd[0], cmd[1:]...) + c.Dir = t.WorkingDir + c.Stdout = out + c.Stderr = out + c.Env = append(environ, os.Environ()...) + if err := c.Run(); err != nil { + logger.Printf("lifecycle hook failed: %v", err) + } +} diff --git a/internal/types/lifecycle.go b/internal/types/lifecycle.go new file mode 100644 index 0000000..a6499a6 --- /dev/null +++ b/internal/types/lifecycle.go @@ -0,0 +1,31 @@ +package types + +// LifecycleHook defines a command to run at a specific point in the task lifecycle. +type LifecycleHook struct { + // The command to run. + Command Strings `json:"command,omitempty"` + // The shell script to run, instead of command. + Sh string `json:"sh,omitempty"` +} + +// GetCommand returns the command to run, handling both command and sh forms. +func (h *LifecycleHook) GetCommand() Strings { + if h == nil { + return nil + } + if len(h.Command) > 0 { + return h.Command + } + if h.Sh != "" { + return []string{"sh", "-c", h.Sh} + } + return nil +} + +// Lifecycle describes actions that the system should take in response to lifecycle events. +type Lifecycle struct { + // OnSuccess is the hook to run after the task succeeds. + OnSuccess *LifecycleHook `json:"onSuccess,omitempty"` + // OnFailure is the hook to run after the task fails. + OnFailure *LifecycleHook `json:"onFailure,omitempty"` +} diff --git a/internal/types/lifecycle_test.go b/internal/types/lifecycle_test.go new file mode 100644 index 0000000..9ddbbc9 --- /dev/null +++ b/internal/types/lifecycle_test.go @@ -0,0 +1,62 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLifecycleHook_GetCommand(t *testing.T) { + t.Run("Nil", func(t *testing.T) { + var h *LifecycleHook + assert.Nil(t, h.GetCommand()) + }) + t.Run("Empty", func(t *testing.T) { + h := &LifecycleHook{} + assert.Nil(t, h.GetCommand()) + }) + t.Run("Command", func(t *testing.T) { + h := &LifecycleHook{Command: Strings{"echo", "hello"}} + assert.Equal(t, Strings{"echo", "hello"}, h.GetCommand()) + }) + t.Run("Sh", func(t *testing.T) { + h := &LifecycleHook{Sh: "echo hello"} + assert.Equal(t, Strings{"sh", "-c", "echo hello"}, h.GetCommand()) + }) + t.Run("CommandPreferredOverSh", func(t *testing.T) { + h := &LifecycleHook{Command: Strings{"echo", "hi"}, Sh: "echo hello"} + assert.Equal(t, Strings{"echo", "hi"}, h.GetCommand()) + }) +} + +func TestTask_GetOnSuccessHook(t *testing.T) { + t.Run("NoLifecycle", func(t *testing.T) { + task := &Task{} + assert.Nil(t, task.GetOnSuccessHook()) + }) + t.Run("NoOnSuccess", func(t *testing.T) { + task := &Task{Lifecycle: &Lifecycle{}} + assert.Nil(t, task.GetOnSuccessHook()) + }) + t.Run("WithOnSuccess", func(t *testing.T) { + hook := &LifecycleHook{Sh: "echo success"} + task := &Task{Lifecycle: &Lifecycle{OnSuccess: hook}} + assert.Equal(t, hook, task.GetOnSuccessHook()) + }) +} + +func TestTask_GetOnFailureHook(t *testing.T) { + t.Run("NoLifecycle", func(t *testing.T) { + task := &Task{} + assert.Nil(t, task.GetOnFailureHook()) + }) + t.Run("NoOnFailure", func(t *testing.T) { + task := &Task{Lifecycle: &Lifecycle{}} + assert.Nil(t, task.GetOnFailureHook()) + }) + t.Run("WithOnFailure", func(t *testing.T) { + hook := &LifecycleHook{Sh: "echo failed"} + task := &Task{Lifecycle: &Lifecycle{OnFailure: hook}} + assert.Equal(t, hook, task.GetOnFailureHook()) + }) +} diff --git a/internal/types/task.go b/internal/types/task.go index b9f5703..0484b36 100644 --- a/internal/types/task.go +++ b/internal/types/task.go @@ -90,6 +90,8 @@ type Task struct { Group string `json:"group,omitempty"` // Whether this is the default task to run if no task is specified. Default bool `json:"default,omitempty"` + // Lifecycle describes actions that the system should take in response to task lifecycle events. + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` } func (t *Task) GetHostPorts() []uint16 { @@ -216,3 +218,19 @@ func (t *Task) GetStalledTimeout() time.Duration { } return 30 * time.Second } + +// GetOnSuccessHook returns the lifecycle hook to run when the task succeeds, or nil if none. +func (t *Task) GetOnSuccessHook() *LifecycleHook { + if t.Lifecycle == nil { + return nil + } + return t.Lifecycle.OnSuccess +} + +// GetOnFailureHook returns the lifecycle hook to run when the task fails, or nil if none. +func (t *Task) GetOnFailureHook() *LifecycleHook { + if t.Lifecycle == nil { + return nil + } + return t.Lifecycle.OnFailure +} From 5fea8d00600f4c5ed840877294d976154836f167 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:35:51 +0000 Subject: [PATCH 3/4] Add graph-level lifecycle hooks and address review comments Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> Agent-Logs-Url: https://github.com/kitproj/kit/sessions/553f2363-3908-4c91-ba67-90c952a74bf0 --- internal/run.go | 9 +++++++ internal/types/lifecycle.go | 16 ++++++++++++ internal/types/lifecycle_test.go | 40 ++++++++++++++++++++++------ internal/types/spec.go | 2 ++ internal/types/task.go | 4 +-- schema/workflow.schema.json | 45 ++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 10 deletions(-) diff --git a/internal/run.go b/internal/run.go index 07bbf78..65aa166 100644 --- a/internal/run.go +++ b/internal/run.go @@ -202,6 +202,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } allRunning := false + graphCompleted := false for { select { @@ -231,9 +232,13 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } if len(failures) > 0 { + runLifecycleHook(context.Background(), types.Task{}, wf.Lifecycle.GetOnFailureHook(), os.Stdout, logger) return fmt.Errorf("failed tasks: %v", failures) } + if graphCompleted { + runLifecycleHook(context.Background(), types.Task{}, wf.Lifecycle.GetOnSuccessHook(), os.Stdout, logger) + } return nil case event := <-events: switch x := event.(type) { @@ -276,6 +281,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB if len(pendingTasks) == 0 { logger.Println("✅ exiting because all requested tasks completed and none should be restarted") + graphCompleted = true cancel() } else if len(remainingTasks) == 0 { if !allRunning { @@ -534,6 +540,9 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB // It is a best-effort operation: if the hook command fails, the error is logged // but does not affect the task's outcome. func runLifecycleHook(ctx context.Context, t types.Task, hook *types.LifecycleHook, out io.Writer, logger *log.Logger) { + if hook == nil { + return + } cmd := hook.GetCommand() if len(cmd) == 0 { return diff --git a/internal/types/lifecycle.go b/internal/types/lifecycle.go index a6499a6..2c2b5fc 100644 --- a/internal/types/lifecycle.go +++ b/internal/types/lifecycle.go @@ -29,3 +29,19 @@ type Lifecycle struct { // OnFailure is the hook to run after the task fails. OnFailure *LifecycleHook `json:"onFailure,omitempty"` } + +// GetOnSuccessHook returns the OnSuccess hook, or nil if the Lifecycle is nil. +func (l *Lifecycle) GetOnSuccessHook() *LifecycleHook { + if l == nil { + return nil + } + return l.OnSuccess +} + +// GetOnFailureHook returns the OnFailure hook, or nil if the Lifecycle is nil. +func (l *Lifecycle) GetOnFailureHook() *LifecycleHook { + if l == nil { + return nil + } + return l.OnFailure +} diff --git a/internal/types/lifecycle_test.go b/internal/types/lifecycle_test.go index 9ddbbc9..896da6b 100644 --- a/internal/types/lifecycle_test.go +++ b/internal/types/lifecycle_test.go @@ -29,15 +29,43 @@ func TestLifecycleHook_GetCommand(t *testing.T) { }) } +func TestLifecycle_GetOnSuccessHook(t *testing.T) { + t.Run("NilLifecycle", func(t *testing.T) { + var l *Lifecycle + assert.Nil(t, l.GetOnSuccessHook()) + }) + t.Run("NoOnSuccess", func(t *testing.T) { + l := &Lifecycle{} + assert.Nil(t, l.GetOnSuccessHook()) + }) + t.Run("WithOnSuccess", func(t *testing.T) { + hook := &LifecycleHook{Sh: "echo success"} + l := &Lifecycle{OnSuccess: hook} + assert.Equal(t, hook, l.GetOnSuccessHook()) + }) +} + +func TestLifecycle_GetOnFailureHook(t *testing.T) { + t.Run("NilLifecycle", func(t *testing.T) { + var l *Lifecycle + assert.Nil(t, l.GetOnFailureHook()) + }) + t.Run("NoOnFailure", func(t *testing.T) { + l := &Lifecycle{} + assert.Nil(t, l.GetOnFailureHook()) + }) + t.Run("WithOnFailure", func(t *testing.T) { + hook := &LifecycleHook{Sh: "echo failed"} + l := &Lifecycle{OnFailure: hook} + assert.Equal(t, hook, l.GetOnFailureHook()) + }) +} + func TestTask_GetOnSuccessHook(t *testing.T) { t.Run("NoLifecycle", func(t *testing.T) { task := &Task{} assert.Nil(t, task.GetOnSuccessHook()) }) - t.Run("NoOnSuccess", func(t *testing.T) { - task := &Task{Lifecycle: &Lifecycle{}} - assert.Nil(t, task.GetOnSuccessHook()) - }) t.Run("WithOnSuccess", func(t *testing.T) { hook := &LifecycleHook{Sh: "echo success"} task := &Task{Lifecycle: &Lifecycle{OnSuccess: hook}} @@ -50,10 +78,6 @@ func TestTask_GetOnFailureHook(t *testing.T) { task := &Task{} assert.Nil(t, task.GetOnFailureHook()) }) - t.Run("NoOnFailure", func(t *testing.T) { - task := &Task{Lifecycle: &Lifecycle{}} - assert.Nil(t, task.GetOnFailureHook()) - }) t.Run("WithOnFailure", func(t *testing.T) { hook := &LifecycleHook{Sh: "echo failed"} task := &Task{Lifecycle: &Lifecycle{OnFailure: hook}} diff --git a/internal/types/spec.go b/internal/types/spec.go index 4b069e8..86f3077 100644 --- a/internal/types/spec.go +++ b/internal/types/spec.go @@ -18,6 +18,8 @@ type Spec struct { Env EnvVars `json:"env,omitempty"` // Environment file (e.g. .env) to use Envfile Envfile `json:"envfile,omitempty"` + // Lifecycle describes actions that the system should take in response to graph-level lifecycle events. + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` } func (s *Spec) GetTerminationGracePeriod() time.Duration { diff --git a/internal/types/task.go b/internal/types/task.go index 0484b36..dd34630 100644 --- a/internal/types/task.go +++ b/internal/types/task.go @@ -224,7 +224,7 @@ func (t *Task) GetOnSuccessHook() *LifecycleHook { if t.Lifecycle == nil { return nil } - return t.Lifecycle.OnSuccess + return t.Lifecycle.GetOnSuccessHook() } // GetOnFailureHook returns the lifecycle hook to run when the task fails, or nil if none. @@ -232,5 +232,5 @@ func (t *Task) GetOnFailureHook() *LifecycleHook { if t.Lifecycle == nil { return nil } - return t.Lifecycle.OnFailure + return t.Lifecycle.GetOnFailureHook() } diff --git a/schema/workflow.schema.json b/schema/workflow.schema.json index 04808ff..0b85cb6 100755 --- a/schema/workflow.schema.json +++ b/schema/workflow.schema.json @@ -72,6 +72,42 @@ ], "title": "HostPath" }, + "Lifecycle": { + "properties": { + "onSuccess": { + "$ref": "#/$defs/LifecycleHook", + "title": "onSuccess", + "description": "OnSuccess is the hook to run after the task succeeds." + }, + "onFailure": { + "$ref": "#/$defs/LifecycleHook", + "title": "onFailure", + "description": "OnFailure is the hook to run after the task fails." + } + }, + "additionalProperties": false, + "type": "object", + "title": "Lifecycle", + "description": "Lifecycle describes actions that the system should take in response to lifecycle events." + }, + "LifecycleHook": { + "properties": { + "command": { + "$ref": "#/$defs/Strings", + "title": "command", + "description": "The command to run." + }, + "sh": { + "type": "string", + "title": "sh", + "description": "The shell script to run, instead of command." + } + }, + "additionalProperties": false, + "type": "object", + "title": "LifecycleHook", + "description": "LifecycleHook defines a command to run at a specific point in the task lifecycle." + }, "Port": { "properties": { "containerPort": { @@ -298,6 +334,11 @@ "type": "boolean", "title": "default", "description": "Whether this is the default task to run if no task is specified." + }, + "lifecycle": { + "$ref": "#/$defs/Lifecycle", + "title": "lifecycle", + "description": "Lifecycle describes actions that the system should take in response to task lifecycle events." } }, "additionalProperties": false, @@ -394,6 +435,10 @@ "envfile": { "$ref": "#/$defs/Envfile", "title": "envfile" + }, + "lifecycle": { + "$ref": "#/$defs/Lifecycle", + "title": "lifecycle" } }, "additionalProperties": false, From 496f88e1b674ec2694d8d5fd5919ca74b3379b70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:46:02 +0000 Subject: [PATCH 4/4] refactor: lifecycle hooks are task name references instead of inline commands --- internal/run.go | 37 +++++++------------ internal/types/lifecycle.go | 42 +++++---------------- internal/types/lifecycle_test.go | 63 +++++++++----------------------- internal/types/task.go | 18 +++------ schema/workflow.schema.json | 26 ++----------- 5 files changed, 52 insertions(+), 134 deletions(-) diff --git a/internal/run.go b/internal/run.go index 65aa166..5acc7d1 100644 --- a/internal/run.go +++ b/internal/run.go @@ -7,7 +7,6 @@ import ( "io" "log" "os" - "os/exec" "path/filepath" "strings" "sync" @@ -232,12 +231,12 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } if len(failures) > 0 { - runLifecycleHook(context.Background(), types.Task{}, wf.Lifecycle.GetOnFailureHook(), os.Stdout, logger) + runLifecycleHook(context.Background(), wf.Lifecycle.GetOnFailure(), wf, os.Stdout, logger) return fmt.Errorf("failed tasks: %v", failures) } if graphCompleted { - runLifecycleHook(context.Background(), types.Task{}, wf.Lifecycle.GetOnSuccessHook(), os.Stdout, logger) + runLifecycleHook(context.Background(), wf.Lifecycle.GetOnSuccess(), wf, os.Stdout, logger) } return nil case event := <-events: @@ -514,7 +513,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB if err != nil { setNodeStatus(node, "failed", fmt.Sprint(err)) - runLifecycleHook(ctx, t, t.GetOnFailureHook(), out, logger) + runLifecycleHook(ctx, t.GetOnFailure(), wf, out, logger) if t.GetRestartPolicy() != "Never" { restart() } @@ -522,7 +521,7 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } setNodeStatus(node, "succeeded", "") - runLifecycleHook(ctx, t, t.GetOnSuccessHook(), out, logger) + runLifecycleHook(ctx, t.GetOnSuccess(), wf, out, logger) if t.GetRestartPolicy() == "Always" { restart() } @@ -536,28 +535,20 @@ func RunSubgraph(ctx context.Context, cancel context.CancelFunc, port int, openB } } -// runLifecycleHook runs the given lifecycle hook command, logging any errors. -// It is a best-effort operation: if the hook command fails, the error is logged -// but does not affect the task's outcome. -func runLifecycleHook(ctx context.Context, t types.Task, hook *types.LifecycleHook, out io.Writer, logger *log.Logger) { - if hook == nil { +// runLifecycleHook runs the named task as a lifecycle hook, logging any errors. +// It is a best-effort operation: if the hook task fails, the error is logged +// but does not affect the triggering task's outcome. +func runLifecycleHook(ctx context.Context, taskName string, wf *types.Workflow, out io.Writer, logger *log.Logger) { + if taskName == "" { return } - cmd := hook.GetCommand() - if len(cmd) == 0 { + t, ok := wf.Tasks[taskName] + if !ok { + logger.Printf("lifecycle hook: task %q not found", taskName) return } - environ, err := types.Environ(types.Spec{}, t) - if err != nil { - logger.Printf("lifecycle hook: failed to get environment: %v", err) - return - } - c := exec.CommandContext(ctx, cmd[0], cmd[1:]...) - c.Dir = t.WorkingDir - c.Stdout = out - c.Stderr = out - c.Env = append(environ, os.Environ()...) - if err := c.Run(); err != nil { + p := proc.New(taskName, t, logger, types.Spec(*wf)) + if err := p.Run(ctx, out, out); err != nil { logger.Printf("lifecycle hook failed: %v", err) } } diff --git a/internal/types/lifecycle.go b/internal/types/lifecycle.go index 2c2b5fc..b1ca898 100644 --- a/internal/types/lifecycle.go +++ b/internal/types/lifecycle.go @@ -1,47 +1,25 @@ package types -// LifecycleHook defines a command to run at a specific point in the task lifecycle. -type LifecycleHook struct { - // The command to run. - Command Strings `json:"command,omitempty"` - // The shell script to run, instead of command. - Sh string `json:"sh,omitempty"` -} - -// GetCommand returns the command to run, handling both command and sh forms. -func (h *LifecycleHook) GetCommand() Strings { - if h == nil { - return nil - } - if len(h.Command) > 0 { - return h.Command - } - if h.Sh != "" { - return []string{"sh", "-c", h.Sh} - } - return nil -} - // Lifecycle describes actions that the system should take in response to lifecycle events. type Lifecycle struct { - // OnSuccess is the hook to run after the task succeeds. - OnSuccess *LifecycleHook `json:"onSuccess,omitempty"` - // OnFailure is the hook to run after the task fails. - OnFailure *LifecycleHook `json:"onFailure,omitempty"` + // OnSuccess is the name of the task to run after the task/graph succeeds. + OnSuccess string `json:"onSuccess,omitempty"` + // OnFailure is the name of the task to run after the task/graph fails. + OnFailure string `json:"onFailure,omitempty"` } -// GetOnSuccessHook returns the OnSuccess hook, or nil if the Lifecycle is nil. -func (l *Lifecycle) GetOnSuccessHook() *LifecycleHook { +// GetOnSuccess returns the OnSuccess task name, or empty string if the Lifecycle is nil. +func (l *Lifecycle) GetOnSuccess() string { if l == nil { - return nil + return "" } return l.OnSuccess } -// GetOnFailureHook returns the OnFailure hook, or nil if the Lifecycle is nil. -func (l *Lifecycle) GetOnFailureHook() *LifecycleHook { +// GetOnFailure returns the OnFailure task name, or empty string if the Lifecycle is nil. +func (l *Lifecycle) GetOnFailure() string { if l == nil { - return nil + return "" } return l.OnFailure } diff --git a/internal/types/lifecycle_test.go b/internal/types/lifecycle_test.go index 896da6b..3eda09a 100644 --- a/internal/types/lifecycle_test.go +++ b/internal/types/lifecycle_test.go @@ -6,81 +6,54 @@ import ( "github.com/stretchr/testify/assert" ) -func TestLifecycleHook_GetCommand(t *testing.T) { - t.Run("Nil", func(t *testing.T) { - var h *LifecycleHook - assert.Nil(t, h.GetCommand()) - }) - t.Run("Empty", func(t *testing.T) { - h := &LifecycleHook{} - assert.Nil(t, h.GetCommand()) - }) - t.Run("Command", func(t *testing.T) { - h := &LifecycleHook{Command: Strings{"echo", "hello"}} - assert.Equal(t, Strings{"echo", "hello"}, h.GetCommand()) - }) - t.Run("Sh", func(t *testing.T) { - h := &LifecycleHook{Sh: "echo hello"} - assert.Equal(t, Strings{"sh", "-c", "echo hello"}, h.GetCommand()) - }) - t.Run("CommandPreferredOverSh", func(t *testing.T) { - h := &LifecycleHook{Command: Strings{"echo", "hi"}, Sh: "echo hello"} - assert.Equal(t, Strings{"echo", "hi"}, h.GetCommand()) - }) -} - -func TestLifecycle_GetOnSuccessHook(t *testing.T) { +func TestLifecycle_GetOnSuccess(t *testing.T) { t.Run("NilLifecycle", func(t *testing.T) { var l *Lifecycle - assert.Nil(t, l.GetOnSuccessHook()) + assert.Equal(t, "", l.GetOnSuccess()) }) t.Run("NoOnSuccess", func(t *testing.T) { l := &Lifecycle{} - assert.Nil(t, l.GetOnSuccessHook()) + assert.Equal(t, "", l.GetOnSuccess()) }) t.Run("WithOnSuccess", func(t *testing.T) { - hook := &LifecycleHook{Sh: "echo success"} - l := &Lifecycle{OnSuccess: hook} - assert.Equal(t, hook, l.GetOnSuccessHook()) + l := &Lifecycle{OnSuccess: "notify"} + assert.Equal(t, "notify", l.GetOnSuccess()) }) } -func TestLifecycle_GetOnFailureHook(t *testing.T) { +func TestLifecycle_GetOnFailure(t *testing.T) { t.Run("NilLifecycle", func(t *testing.T) { var l *Lifecycle - assert.Nil(t, l.GetOnFailureHook()) + assert.Equal(t, "", l.GetOnFailure()) }) t.Run("NoOnFailure", func(t *testing.T) { l := &Lifecycle{} - assert.Nil(t, l.GetOnFailureHook()) + assert.Equal(t, "", l.GetOnFailure()) }) t.Run("WithOnFailure", func(t *testing.T) { - hook := &LifecycleHook{Sh: "echo failed"} - l := &Lifecycle{OnFailure: hook} - assert.Equal(t, hook, l.GetOnFailureHook()) + l := &Lifecycle{OnFailure: "alert"} + assert.Equal(t, "alert", l.GetOnFailure()) }) } -func TestTask_GetOnSuccessHook(t *testing.T) { +func TestTask_GetOnSuccess(t *testing.T) { t.Run("NoLifecycle", func(t *testing.T) { task := &Task{} - assert.Nil(t, task.GetOnSuccessHook()) + assert.Equal(t, "", task.GetOnSuccess()) }) t.Run("WithOnSuccess", func(t *testing.T) { - hook := &LifecycleHook{Sh: "echo success"} - task := &Task{Lifecycle: &Lifecycle{OnSuccess: hook}} - assert.Equal(t, hook, task.GetOnSuccessHook()) + task := &Task{Lifecycle: &Lifecycle{OnSuccess: "notify"}} + assert.Equal(t, "notify", task.GetOnSuccess()) }) } -func TestTask_GetOnFailureHook(t *testing.T) { +func TestTask_GetOnFailure(t *testing.T) { t.Run("NoLifecycle", func(t *testing.T) { task := &Task{} - assert.Nil(t, task.GetOnFailureHook()) + assert.Equal(t, "", task.GetOnFailure()) }) t.Run("WithOnFailure", func(t *testing.T) { - hook := &LifecycleHook{Sh: "echo failed"} - task := &Task{Lifecycle: &Lifecycle{OnFailure: hook}} - assert.Equal(t, hook, task.GetOnFailureHook()) + task := &Task{Lifecycle: &Lifecycle{OnFailure: "alert"}} + assert.Equal(t, "alert", task.GetOnFailure()) }) } diff --git a/internal/types/task.go b/internal/types/task.go index dd34630..5065328 100644 --- a/internal/types/task.go +++ b/internal/types/task.go @@ -219,18 +219,12 @@ func (t *Task) GetStalledTimeout() time.Duration { return 30 * time.Second } -// GetOnSuccessHook returns the lifecycle hook to run when the task succeeds, or nil if none. -func (t *Task) GetOnSuccessHook() *LifecycleHook { - if t.Lifecycle == nil { - return nil - } - return t.Lifecycle.GetOnSuccessHook() +// GetOnSuccess returns the name of the task to run when this task succeeds, or empty string if none. +func (t *Task) GetOnSuccess() string { + return t.Lifecycle.GetOnSuccess() } -// GetOnFailureHook returns the lifecycle hook to run when the task fails, or nil if none. -func (t *Task) GetOnFailureHook() *LifecycleHook { - if t.Lifecycle == nil { - return nil - } - return t.Lifecycle.GetOnFailureHook() +// GetOnFailure returns the name of the task to run when this task fails, or empty string if none. +func (t *Task) GetOnFailure() string { + return t.Lifecycle.GetOnFailure() } diff --git a/schema/workflow.schema.json b/schema/workflow.schema.json index 0b85cb6..e540a7a 100755 --- a/schema/workflow.schema.json +++ b/schema/workflow.schema.json @@ -75,14 +75,14 @@ "Lifecycle": { "properties": { "onSuccess": { - "$ref": "#/$defs/LifecycleHook", + "type": "string", "title": "onSuccess", - "description": "OnSuccess is the hook to run after the task succeeds." + "description": "OnSuccess is the name of the task to run after the task/graph succeeds." }, "onFailure": { - "$ref": "#/$defs/LifecycleHook", + "type": "string", "title": "onFailure", - "description": "OnFailure is the hook to run after the task fails." + "description": "OnFailure is the name of the task to run after the task/graph fails." } }, "additionalProperties": false, @@ -90,24 +90,6 @@ "title": "Lifecycle", "description": "Lifecycle describes actions that the system should take in response to lifecycle events." }, - "LifecycleHook": { - "properties": { - "command": { - "$ref": "#/$defs/Strings", - "title": "command", - "description": "The command to run." - }, - "sh": { - "type": "string", - "title": "sh", - "description": "The shell script to run, instead of command." - } - }, - "additionalProperties": false, - "type": "object", - "title": "LifecycleHook", - "description": "LifecycleHook defines a command to run at a specific point in the task lifecycle." - }, "Port": { "properties": { "containerPort": {