66 "io"
77 "log"
88 "os"
9+ "sync"
910 "text/template"
1011
1112 "gopkg.in/yaml.v3"
@@ -57,18 +58,60 @@ func parse(data []byte) (*WorkflowConfig, error) {
5758 return & cfg , nil
5859}
5960
61+ // WorkflowState represents the current execution status.
62+ type WorkflowState string
63+
64+ const (
65+ StateIdle WorkflowState = "idle"
66+ StateRunning WorkflowState = "running"
67+ StateComplete WorkflowState = "complete"
68+ StateFailed WorkflowState = "failed"
69+ )
70+
71+ // Status is a snapshot of the engine's current execution state,
72+ // safe to serialize as JSON for the /api/v1/status endpoint.
73+ type Status struct {
74+ State WorkflowState `json:"state"`
75+ CurrentStep int `json:"currentStep"`
76+ TotalSteps int `json:"totalSteps"`
77+ StepName string `json:"stepName,omitempty"`
78+ Error string `json:"error,omitempty"`
79+ Outputs map [string ]any `json:"outputs"`
80+ }
81+
6082// Engine executes a workflow using its registry and collected variables.
6183type Engine struct {
6284 Registry * Registry
6385 Vars map [string ]any
86+
87+ mu sync.Mutex
88+ status Status
6489}
6590
6691// NewEngine creates an Engine with the given registry and initial variables.
6792func NewEngine (reg * Registry , vars map [string ]any ) * Engine {
6893 if vars == nil {
6994 vars = make (map [string ]any )
7095 }
71- return & Engine {Registry : reg , Vars : vars }
96+ return & Engine {
97+ Registry : reg ,
98+ Vars : vars ,
99+ status : Status {State : StateIdle , Outputs : make (map [string ]any )},
100+ }
101+ }
102+
103+ // Status returns a snapshot of the current workflow execution state.
104+ func (e * Engine ) Status () Status {
105+ e .mu .Lock ()
106+ defer e .mu .Unlock ()
107+ // Return a copy of outputs so the caller can't mutate engine state.
108+ out := make (map [string ]any , len (e .status .Outputs ))
109+ for k , v := range e .status .Outputs {
110+ out [k ] = v
111+ }
112+ s := e .status
113+ s .Outputs = out
114+ return s
72115}
73116
74117// Run executes every step in the workflow sequentially.
@@ -77,41 +120,73 @@ func NewEngine(reg *Registry, vars map[string]any) *Engine {
77120func (e * Engine ) Run (wf * WorkflowConfig ) error {
78121 log .Printf ("workflow: starting %q (%d steps)" , wf .Name , len (wf .Steps ))
79122
123+ e .mu .Lock ()
124+ e .status = Status {State : StateRunning , TotalSteps : len (wf .Steps ), Outputs : make (map [string ]any )}
125+ e .mu .Unlock ()
126+
80127 for i , step := range wf .Steps {
81128 log .Printf ("workflow: [%d/%d] %s (action=%s)" , i + 1 , len (wf .Steps ), step .Name , step .Action )
82129
130+ e .mu .Lock ()
131+ e .status .CurrentStep = i + 1
132+ e .status .StepName = step .Name
133+ e .mu .Unlock ()
134+
83135 fn := e .Registry .Get (step .Action )
84136 if fn == nil {
85- return fmt .Errorf ("workflow step %d: unknown action %q" , i + 1 , step .Action )
137+ err := fmt .Errorf ("workflow step %d: unknown action %q" , i + 1 , step .Action )
138+ e .mu .Lock ()
139+ e .status .State = StateFailed
140+ e .status .Error = err .Error ()
141+ e .mu .Unlock ()
142+ return err
86143 }
87144
88145 // Resolve {{.var}} templates in string params.
89146 resolved , err := e .resolveParams (step .Params )
90147 if err != nil {
91- return fmt .Errorf ("workflow step %d (%s): resolving params: %w" , i + 1 , step .Name , err )
148+ err = fmt .Errorf ("workflow step %d (%s): resolving params: %w" , i + 1 , step .Name , err )
149+ e .mu .Lock ()
150+ e .status .State = StateFailed
151+ e .status .Error = err .Error ()
152+ e .mu .Unlock ()
153+ return err
92154 }
93155
94156 result , err := fn (resolved )
95157 if err != nil {
96- return fmt .Errorf ("workflow step %d (%s): %w" , i + 1 , step .Name , err )
158+ err = fmt .Errorf ("workflow step %d (%s): %w" , i + 1 , step .Name , err )
159+ e .mu .Lock ()
160+ e .status .State = StateFailed
161+ e .status .Error = err .Error ()
162+ e .mu .Unlock ()
163+ return err
97164 }
98165
99166 // Map action outputs to workflow variables.
100167 if result != nil && step .Outputs != nil {
168+ e .mu .Lock ()
101169 for field , varName := range step .Outputs {
102170 val , ok := (* result )[field ]
103171 if ! ok {
104172 log .Printf ("workflow: warning: step %d output field %q not found in result" , i + 1 , field )
105173 continue
106174 }
107175 e .Vars [varName ] = val
176+ e .status .Outputs [varName ] = val
108177 log .Printf ("workflow: captured %s = %v" , varName , val )
109178 }
179+ e .mu .Unlock ()
110180 }
111181
112182 log .Printf ("workflow: [%d/%d] %s completed" , i + 1 , len (wf .Steps ), step .Name )
113183 }
114184
185+ e .mu .Lock ()
186+ e .status .State = StateComplete
187+ e .status .StepName = ""
188+ e .mu .Unlock ()
189+
115190 log .Printf ("workflow: %q finished successfully" , wf .Name )
116191 return nil
117192}
0 commit comments