···1818 "github.com/docker/docker/api/types/network"
1919 "github.com/docker/docker/client"
2020 "github.com/docker/docker/pkg/stdcopy"
2121- "gopkg.in/yaml.v3"
2221 "tangled.org/core/api/tangled"
2322 "tangled.org/core/log"
2423 "tangled.org/core/spindle/config"
2524 "tangled.org/core/spindle/engine"
2625 "tangled.org/core/spindle/models"
2726 "tangled.org/core/spindle/secrets"
2727+ "tangled.org/core/spindle/workflow"
2828)
29293030const (
···4343 cleanup map[string][]cleanupFunc
4444}
45454646-type Step struct {
4747- name string
4848- kind models.StepKind
4949- command string
5050- environment map[string]string
5151-}
5252-5353-func (s Step) Name() string {
5454- return s.name
5555-}
5656-5757-func (s Step) Command() string {
5858- return s.command
5959-}
6060-6161-func (s Step) Kind() models.StepKind {
6262- return s.kind
6363-}
6464-6546// setupSteps get added to start of Steps
6647type setupSteps []models.Step
6748···7657 env map[string]string
7758}
78597979-func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
8080- swf := &models.Workflow{}
8181- addl := addlFields{}
6060+// nixeryWorkflowSchema defines the nixery-specific YAML fields
6161+// Common fields are embedded inline to avoid duplicate unmarshaling
6262+type nixeryWorkflowSchema struct {
6363+ workflow.CommonWorkflowFields `yaml:",inline"`
6464+ Dependencies map[string][]string `yaml:"dependencies"`
6565+}
82668383- dwf := &struct {
8484- Steps []struct {
8585- Command string `yaml:"command"`
8686- Name string `yaml:"name"`
8787- Environment map[string]string `yaml:"environment"`
8888- } `yaml:"steps"`
8989- Dependencies map[string][]string `yaml:"dependencies"`
9090- Environment map[string]string `yaml:"environment"`
9191- }{}
9292- err := yaml.Unmarshal([]byte(twf.Raw), &dwf)
9393- if err != nil {
9494- return nil, err
6767+// GetCommonFields returns a pointer to the embedded common fields
6868+func (s *nixeryWorkflowSchema) GetCommonFields() *workflow.CommonWorkflowFields {
6969+ return &s.CommonWorkflowFields
7070+}
7171+7272+// NewWorkflowSchema returns the schema struct for unmarshaling nixery workflow YAMLs
7373+func (e *Engine) NewWorkflowSchema() interface{} {
7474+ return &nixeryWorkflowSchema{}
7575+}
7676+7777+func (e *Engine) InitWorkflow(wf *models.Workflow, tpl tangled.Pipeline) error {
7878+ // Extract the workflow data wrapper
7979+ wfData, ok := wf.Data.(*workflow.WorkflowData)
8080+ if !ok {
8181+ return fmt.Errorf("invalid workflow data type for nixery engine")
9582 }
96839797- for _, dstep := range dwf.Steps {
9898- sstep := Step{}
9999- sstep.environment = dstep.Environment
100100- sstep.command = dstep.Command
101101- sstep.name = dstep.Name
102102- sstep.kind = models.StepKindUser
103103- swf.Steps = append(swf.Steps, sstep)
8484+ // Extract the parsed schema
8585+ schema, ok := wfData.Schema.(*nixeryWorkflowSchema)
8686+ if !ok {
8787+ return fmt.Errorf("invalid workflow schema type for nixery engine")
10488 }
105105- swf.Name = twf.Name
106106- addl.env = dwf.Environment
107107- addl.image = workflowImage(dwf.Dependencies, e.cfg.NixeryPipelines.Nixery)
10889109109- setup := &setupSteps{}
9090+ // Steps are already created by the parser with environment support - no need to replace!
110919292+ // Build the setup steps that run before user steps
9393+ setup := &setupSteps{}
11194 setup.addStep(nixConfStep())
112112- setup.addStep(cloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))
113113- // this step could be empty
114114- if s := dependencyStep(dwf.Dependencies); s != nil {
115115- setup.addStep(*s)
9595+9696+ // Use the original workflow for clone step (it has the Clone field)
9797+ setup.addStep(cloneStep(wfData.OriginalWorkflow, *tpl.TriggerMetadata, e.cfg.Server.Dev))
9898+9999+ // Add dependency installation step if dependencies are specified
100100+ if s := dependencyStep(schema.Dependencies); s != nil {
101101+ setup.addStep(s)
116102 }
117103118118- // append setup steps in order to the start of workflow steps
119119- swf.Steps = append(*setup, swf.Steps...)
120120- swf.Data = addl
104104+ // Prepend setup steps to the beginning of the workflow
105105+ wf.Steps = append(*setup, wf.Steps...)
121106122122- return swf, nil
107107+ // Store engine-specific runtime metadata
108108+ wf.Data = addlFields{
109109+ image: workflowImage(schema.Dependencies, e.cfg.NixeryPipelines.Nixery),
110110+ env: wfData.CommonEnv, // Use common environment from parser
111111+ // container will be set later in SetupWorkflow
112112+ }
113113+114114+ return nil
123115}
124116125117func (e *Engine) WorkflowTimeout() time.Duration {
···295287 workflowEnvs.AddEnv(s.Key, s.Value)
296288 }
297289298298- step := w.Steps[idx].(Step)
290290+ step := w.Steps[idx]
299291300292 select {
301293 case <-ctx.Done():
···304296 }
305297306298 envs := append(EnvVars(nil), workflowEnvs...)
307307- for k, v := range step.environment {
299299+ for k, v := range step.Environment() {
308300 envs.AddEnv(k, v)
309301 }
310302 envs.AddEnv("HOME", homeDir)
311303312304 mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{
313313- Cmd: []string{"bash", "-c", step.command},
305305+ Cmd: []string{"bash", "-c", step.Command()},
314306 AttachStdout: true,
315307 AttachStderr: true,
316308 Env: envs,
···333325 // Docker doesn't provide an API to kill an exec run
334326 // (sure, we could grab the PID and kill it ourselves,
335327 // but that's wasted effort)
336336- e.l.Warn("step timed out", "step", step.Name)
328328+ e.l.Warn("step timed out", "step", step.Name())
337329338330 <-tailDone
339331
+18-17
spindle/engines/nixery/setup_steps.go
···6677 "tangled.org/core/api/tangled"
88 "tangled.org/core/spindle/steps"
99+ "tangled.org/core/spindle/models"
910)
10111111-func nixConfStep() Step {
1212+func nixConfStep() models.Step {
1213 setupCmd := `mkdir -p /etc/nix
1314echo 'extra-experimental-features = nix-command flakes' >> /etc/nix/nix.conf
1415echo 'build-users-group = ' >> /etc/nix/nix.conf`
1515- return Step{
1616- command: setupCmd,
1717- name: "Configure Nix",
1616+ return models.DefaultStep{
1717+ StepCommand: setupCmd,
1818+ StepName: "Configure Nix",
1819 }
1920}
2021···2526// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
2627// - git checkout FETCH_HEAD
2728// And supports all trigger types (push, PR, manual) and clone options.
2828-func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step {
2929+func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) models.Step {
2930 // Use shared clone command builder
3031 cmds, err := steps.BuildCloneCommands(steps.CloneConfig{
3132 Workflow: twf,
···3536 })
3637 if err != nil {
3738 // Return error step - this will cause the workflow to fail with a clear message
3838- return Step{
3939- command: fmt.Sprintf("echo 'Failed to build clone commands: %s' && exit 1", err.Error()),
4040- name: "Clone repository into workspace (error)",
3939+ return models.DefaultStep{
4040+ StepCommand: fmt.Sprintf("echo 'Failed to build clone commands: %s' && exit 1", err.Error()),
4141+ StepName: "Clone repository into workspace (error)",
4142 }
4243 }
43444445 // Check if cloning should be skipped
4546 if cmds.Skip {
4646- return Step{}
4747+ return models.DefaultStep{}
4748 }
48494950 // Nixery-specific: join commands with newlines for shell execution
5050- return Step{
5151- command: strings.Join(cmds.All, "\n"),
5252- name: "Clone repository into workspace",
5151+ return models.DefaultStep{
5252+ StepCommand: strings.Join(cmds.All, "\n"),
5353+ StepName: "Clone repository into workspace",
5354 }
5455}
5556···5758// For dependencies using a custom registry (i.e. not nixpkgs), it collects
5859// all packages and adds a single 'nix profile install' step to the
5960// beginning of the workflow's step list.
6060-func dependencyStep(deps map[string][]string) *Step {
6161+func dependencyStep(deps map[string][]string) models.Step {
6162 var customPackages []string
62636364 for registry, packages := range deps {
···7778 if len(customPackages) > 0 {
7879 installCmd := "nix --extra-experimental-features nix-command --extra-experimental-features flakes profile install"
7980 cmd := fmt.Sprintf("%s %s", installCmd, strings.Join(customPackages, " "))
8080- installStep := Step{
8181- command: cmd,
8282- name: "Install custom dependencies",
8383- environment: map[string]string{
8181+ installStep := models.DefaultStep{
8282+ StepCommand: cmd,
8383+ StepName: "Install custom dependencies",
8484+ StepEnvironment: map[string]string{
8485 "NIX_NO_COLOR": "1",
8586 "NIX_SHOW_DOWNLOAD_PROGRESS": "0",
8687 },
+13-1
spindle/models/engine.go
···99)
10101111type Engine interface {
1212- InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*Workflow, error)
1212+ // NewWorkflowSchema returns an empty schema struct that will be used to unmarshal
1313+ // the workflow YAML. This allows each engine to define its own YAML structure
1414+ // with engine-specific fields (e.g., dependencies for nixery, image/architecture for k8s).
1515+ NewWorkflowSchema() interface{}
1616+1717+ // InitWorkflow is called after the workflow has been parsed and initial steps created.
1818+ // Engines should use this to:
1919+ // - Extract their schema from wf.Data
2020+ // - Prepend any setup steps (e.g., clone, dependency installation)
2121+ // - Transform or replace steps if needed
2222+ // - Store final metadata in wf.Data for use in SetupWorkflow
2323+ InitWorkflow(wf *Workflow, tpl tangled.Pipeline) error
2424+1325 SetupWorkflow(ctx context.Context, wid WorkflowId, wf *Workflow) error
1426 WorkflowTimeout() time.Duration
1527 DestroyWorkflow(ctx context.Context, wid WorkflowId) error