package workflow import ( "fmt" "gopkg.in/yaml.v3" "tangled.org/core/api/tangled" "tangled.org/core/spindle/models" ) // WorkflowData wraps the parsed engine schema and original workflow // for engines that need access to both during initialization type WorkflowData struct { Schema interface{} OriginalWorkflow tangled.Pipeline_Workflow CommonEnv map[string]string // Workflow-level environment variables (common to all engines) } // CommonWorkflowFields represents the standard fields present in all workflow YAMLs. // Engines should embed this in their schema structs using `yaml:",inline"` to avoid // duplicate YAML unmarshaling. type CommonWorkflowFields struct { Engine string `yaml:"engine"` Environment map[string]string `yaml:"environment"` // Workflow-level environment variables Steps []struct { Name string `yaml:"name"` Command string `yaml:"command"` Environment map[string]string `yaml:"environment"` // Per-step environment (overrides/extends workflow-level) } `yaml:"steps"` } // ParseWorkflow parses a workflow YAML using the specified engine's schema // and returns a fully initialized workflow ready for execution. func ParseWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline, engine models.Engine) (*models.Workflow, error) { // 1. Get engine-specific schema struct (should embed CommonWorkflowFields) schema := engine.NewWorkflowSchema() // 2. Unmarshal the YAML into the engine's schema ONCE // This captures both common fields (via inline embedding) and engine-specific fields if err := yaml.Unmarshal([]byte(twf.Raw), schema); err != nil { return nil, fmt.Errorf("failed to parse workflow into engine schema: %w", err) } // 3. Extract common fields from the schema (via type assertion to interface with embedded fields) commonFields, ok := schema.(interface { GetCommonFields() *CommonWorkflowFields }) if !ok { return nil, fmt.Errorf("engine schema must implement GetCommonFields() method") } common := commonFields.GetCommonFields() // 4. Convert common steps to models.Step steps := make([]models.Step, len(common.Steps)) for i, s := range common.Steps { steps[i] = models.DefaultStep{ StepName: s.Name, StepCommand: s.Command, StepKind: models.StepKindUser, StepEnvironment: s.Environment, } } // 5. Create the workflow with parsed data // Store both the schema and original workflow for engines that need access to // fields like Clone options workflowData := &WorkflowData{ Schema: schema, OriginalWorkflow: twf, CommonEnv: common.Environment, } wf := &models.Workflow{ Steps: steps, Name: twf.Name, Data: workflowData, } // 6. Let the engine initialize the workflow // This is where engines can prepend setup steps, transform data, etc. if err := engine.InitWorkflow(wf, tpl); err != nil { return nil, fmt.Errorf("engine initialization failed: %w", err) } return wf, nil }