Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

spindle/engines: add dummy engine that logs to stdout

Useful during local testing.

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

author
Anirudh Oppiliappan
committer
Tangled
date (Jun 11, 2026, 12:01 PM +0300) commit 5616e571 parent 4127eece change-id xmmrnxnl
+95
+1
.gitignore
··· 33 33 id_ed25519 34 34 *.pem 35 35 *.key 36 + .claude/
+92
spindle/engines/dummy/engine.go
··· 1 + package dummy 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log/slog" 7 + "time" 8 + 9 + "gopkg.in/yaml.v3" 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/spindle/models" 12 + "tangled.org/core/spindle/secrets" 13 + ) 14 + 15 + // DummyEngine is a no-op engine that logs all lifecycle events via slog and writes 16 + // step output to the workflow logger. Useful for testing pipeline plumbing 17 + // without a real execution backend. 18 + type DummyEngine struct { 19 + l *slog.Logger 20 + } 21 + 22 + func New(l *slog.Logger) *DummyEngine { 23 + return &DummyEngine{l: l.With("engine", "dummy")} 24 + } 25 + 26 + type Step struct { 27 + name string 28 + kind models.StepKind 29 + command string 30 + } 31 + 32 + func (s Step) Name() string { return s.name } 33 + func (s Step) Command() string { return s.command } 34 + func (s Step) Kind() models.StepKind { return s.kind } 35 + 36 + func (e *DummyEngine) InitWorkflow(twf tangled.Pipeline_Workflow, _ tangled.Pipeline) (*models.Workflow, error) { 37 + dwf := &struct { 38 + Steps []struct { 39 + Name string `yaml:"name"` 40 + Command string `yaml:"command"` 41 + } `yaml:"steps"` 42 + Environment map[string]string `yaml:"environment"` 43 + }{} 44 + 45 + if err := yaml.Unmarshal([]byte(twf.Raw), dwf); err != nil { 46 + return nil, err 47 + } 48 + 49 + wf := &models.Workflow{ 50 + Name: twf.Name, 51 + Environment: dwf.Environment, 52 + } 53 + for _, ds := range dwf.Steps { 54 + wf.Steps = append(wf.Steps, Step{ 55 + name: ds.Name, 56 + kind: models.StepKindUser, 57 + command: ds.Command, 58 + }) 59 + } 60 + 61 + e.l.Info("workflow initialised", "name", twf.Name, "steps", len(wf.Steps)) 62 + return wf, nil 63 + } 64 + 65 + func (e *DummyEngine) SetupWorkflow(_ context.Context, wid models.WorkflowId, wf *models.Workflow, wfLogger models.WorkflowLogger) error { 66 + e.l.Info("setting up workflow", "wid", wid) 67 + 68 + setupStep := Step{name: "dummy setup", kind: models.StepKindSystem} 69 + const setupIdx = -1 70 + 71 + wfLogger.ControlWriter(setupIdx, setupStep, models.StepStatusStart).Write([]byte{0}) 72 + defer wfLogger.ControlWriter(setupIdx, setupStep, models.StepStatusEnd).Write([]byte{0}) 73 + 74 + fmt.Fprintf(wfLogger.DataWriter(setupIdx, "stdout"), "dummy engine: workflow %q ready", wf.Name) 75 + return nil 76 + } 77 + 78 + func (e *DummyEngine) WorkflowTimeout() time.Duration { 79 + return 5 * time.Minute 80 + } 81 + 82 + func (e *DummyEngine) DestroyWorkflow(_ context.Context, wid models.WorkflowId) error { 83 + e.l.Info("destroying workflow", "wid", wid) 84 + return nil 85 + } 86 + 87 + func (e *DummyEngine) RunStep(_ context.Context, wid models.WorkflowId, w *models.Workflow, idx int, _ []secrets.UnlockedSecret, wfLogger models.WorkflowLogger) error { 88 + step := w.Steps[idx] 89 + e.l.Info("running step", "wid", wid, "step", step.Name(), "command", step.Command()) 90 + fmt.Fprintf(wfLogger.DataWriter(idx, "stdout"), "$ %s", step.Command()) 91 + return nil 92 + }
+2
spindle/server.go
··· 24 24 "tangled.org/core/spindle/config" 25 25 "tangled.org/core/spindle/db" 26 26 "tangled.org/core/spindle/engine" 27 + "tangled.org/core/spindle/engines/dummy" 27 28 "tangled.org/core/spindle/engines/nixery" 28 29 "tangled.org/core/spindle/models" 29 30 "tangled.org/core/spindle/queue" ··· 334 335 335 336 s, err := New(ctx, cfg, map[string]models.Engine{ 336 337 "nixery": nixeryEng, 338 + "dummy": dummy.New(log.FromContext(ctx)), 337 339 }) 338 340 if err != nil { 339 341 return err