1package workflow
2
3import (
4 "fmt"
5
6 "gopkg.in/yaml.v3"
7 "tangled.org/core/api/tangled"
8 "tangled.org/core/spindle/models"
9)
10
11// WorkflowData wraps the parsed engine schema and original workflow
12// for engines that need access to both during initialization
13type WorkflowData struct {
14 Schema interface{}
15 OriginalWorkflow tangled.Pipeline_Workflow
16 CommonEnv map[string]string // Workflow-level environment variables (common to all engines)
17}
18
19// CommonWorkflowFields represents the standard fields present in all workflow YAMLs.
20// Engines should embed this in their schema structs using `yaml:",inline"` to avoid
21// duplicate YAML unmarshaling.
22type CommonWorkflowFields struct {
23 Engine string `yaml:"engine"`
24 Environment map[string]string `yaml:"environment"` // Workflow-level environment variables
25 Steps []struct {
26 Name string `yaml:"name"`
27 Command string `yaml:"command"`
28 Environment map[string]string `yaml:"environment"` // Per-step environment (overrides/extends workflow-level)
29 } `yaml:"steps"`
30}
31
32// ParseWorkflow parses a workflow YAML using the specified engine's schema
33// and returns a fully initialized workflow ready for execution.
34func ParseWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline, engine models.Engine) (*models.Workflow, error) {
35 // 1. Get engine-specific schema struct (should embed CommonWorkflowFields)
36 schema := engine.NewWorkflowSchema()
37
38 // 2. Unmarshal the YAML into the engine's schema ONCE
39 // This captures both common fields (via inline embedding) and engine-specific fields
40 if err := yaml.Unmarshal([]byte(twf.Raw), schema); err != nil {
41 return nil, fmt.Errorf("failed to parse workflow into engine schema: %w", err)
42 }
43
44 // 3. Extract common fields from the schema (via type assertion to interface with embedded fields)
45 commonFields, ok := schema.(interface {
46 GetCommonFields() *CommonWorkflowFields
47 })
48 if !ok {
49 return nil, fmt.Errorf("engine schema must implement GetCommonFields() method")
50 }
51 common := commonFields.GetCommonFields()
52
53 // 4. Convert common steps to models.Step
54 steps := make([]models.Step, len(common.Steps))
55 for i, s := range common.Steps {
56 steps[i] = models.DefaultStep{
57 StepName: s.Name,
58 StepCommand: s.Command,
59 StepKind: models.StepKindUser,
60 StepEnvironment: s.Environment,
61 }
62 }
63
64 // 5. Create the workflow with parsed data
65 // Store both the schema and original workflow for engines that need access to
66 // fields like Clone options
67 workflowData := &WorkflowData{
68 Schema: schema,
69 OriginalWorkflow: twf,
70 CommonEnv: common.Environment,
71 }
72
73 wf := &models.Workflow{
74 Steps: steps,
75 Name: twf.Name,
76 Data: workflowData,
77 }
78
79 // 6. Let the engine initialize the workflow
80 // This is where engines can prepend setup steps, transform data, etc.
81 if err := engine.InitWorkflow(wf, tpl); err != nil {
82 return nil, fmt.Errorf("engine initialization failed: %w", err)
83 }
84
85 return wf, nil
86}