forked from tangled.org/core
this repo has no description

workflow: introduce workflow compiler

takes a yaml based workflow and compiles it down to a
sh.tangled.pipeline object, after performing some basic analysis.

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 1842809f b361a7a5

verified
Changed files
+235
workflow
+132
workflow/compile.go
··· 1 + package workflow 2 + 3 + import ( 4 + "fmt" 5 + 6 + "tangled.sh/tangled.sh/core/api/tangled" 7 + ) 8 + 9 + type Compiler struct { 10 + Trigger tangled.Pipeline_TriggerMetadata 11 + Diagnostics Diagnostics 12 + } 13 + 14 + type Diagnostics struct { 15 + Errors []error 16 + Warnings []Warning 17 + } 18 + 19 + func (d *Diagnostics) Combine(o Diagnostics) { 20 + d.Errors = append(d.Errors, o.Errors...) 21 + d.Warnings = append(d.Warnings, o.Warnings...) 22 + } 23 + 24 + func (d *Diagnostics) AddWarning(path string, kind WarningKind, reason string) { 25 + d.Warnings = append(d.Warnings, Warning{path, kind, reason}) 26 + } 27 + 28 + func (d *Diagnostics) AddError(err error) { 29 + d.Errors = append(d.Errors, err) 30 + } 31 + 32 + func (d Diagnostics) IsErr() bool { 33 + return len(d.Errors) != 0 34 + } 35 + 36 + type Warning struct { 37 + Path string 38 + Type WarningKind 39 + Reason string 40 + } 41 + 42 + type WarningKind string 43 + 44 + var ( 45 + WorkflowSkipped WarningKind = "workflow skipped" 46 + InvalidConfiguration WarningKind = "invalid configuration" 47 + ) 48 + 49 + // convert a repositories' workflow files into a fully compiled pipeline that runners accept 50 + func (compiler *Compiler) Compile(p Pipeline) tangled.Pipeline { 51 + cp := tangled.Pipeline{ 52 + TriggerMetadata: &compiler.Trigger, 53 + } 54 + 55 + for _, w := range p { 56 + cw := compiler.compileWorkflow(w) 57 + 58 + // empty workflows are not added to the pipeline 59 + if len(cw.Steps) == 0 { 60 + continue 61 + } 62 + 63 + cp.Workflows = append(cp.Workflows, &cw) 64 + } 65 + 66 + return cp 67 + } 68 + 69 + func (compiler *Compiler) compileWorkflow(w Workflow) tangled.Pipeline_Workflow { 70 + cw := tangled.Pipeline_Workflow{} 71 + 72 + if !w.Match(compiler.Trigger) { 73 + compiler.Diagnostics.AddWarning( 74 + w.Name, 75 + WorkflowSkipped, 76 + fmt.Sprintf("did not match trigger %s", compiler.Trigger.Kind), 77 + ) 78 + return cw 79 + } 80 + 81 + if len(w.Steps) == 0 { 82 + compiler.Diagnostics.AddWarning( 83 + w.Name, 84 + WorkflowSkipped, 85 + "empty workflow", 86 + ) 87 + return cw 88 + } 89 + 90 + // validate clone options 91 + compiler.analyzeCloneOptions(w) 92 + 93 + cw.Name = w.Name 94 + cw.Dependencies = w.Dependencies.AsRecord() 95 + for _, s := range w.Steps { 96 + step := tangled.Pipeline_Step{ 97 + Command: s.Command, 98 + Name: s.Name, 99 + } 100 + cw.Steps = append(cw.Steps, &step) 101 + } 102 + for k, v := range w.Environment { 103 + e := &tangled.Pipeline_Workflow_Environment_Elem{ 104 + Key: k, 105 + Value: v, 106 + } 107 + cw.Environment = append(cw.Environment, e) 108 + } 109 + 110 + o := w.CloneOpts.AsRecord() 111 + cw.Clone = &o 112 + 113 + return cw 114 + } 115 + 116 + func (compiler *Compiler) analyzeCloneOptions(w Workflow) { 117 + if w.CloneOpts.Skip && w.CloneOpts.IncludeSubmodules { 118 + compiler.Diagnostics.AddWarning( 119 + w.Name, 120 + InvalidConfiguration, 121 + "cannot apply `clone.skip` and `clone.submodules`", 122 + ) 123 + } 124 + 125 + if w.CloneOpts.Skip && w.CloneOpts.Depth > 0 { 126 + compiler.Diagnostics.AddWarning( 127 + w.Name, 128 + InvalidConfiguration, 129 + "cannot apply `clone.skip` and `clone.depth`", 130 + ) 131 + } 132 + }
+103
workflow/compile_test.go
··· 1 + package workflow 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + 7 + "github.com/stretchr/testify/assert" 8 + "tangled.sh/tangled.sh/core/api/tangled" 9 + ) 10 + 11 + var trigger = tangled.Pipeline_TriggerMetadata{ 12 + Kind: TriggerKindPush, 13 + Push: &tangled.Pipeline_PushTriggerData{ 14 + Ref: "refs/heads/main", 15 + OldSha: strings.Repeat("0", 40), 16 + NewSha: strings.Repeat("f", 40), 17 + }, 18 + } 19 + 20 + var when = []Constraint{ 21 + { 22 + Event: []string{"push"}, 23 + Branch: []string{"main"}, 24 + }, 25 + } 26 + 27 + func TestCompileWorkflow_MatchingWorkflowWithSteps(t *testing.T) { 28 + wf := Workflow{ 29 + Name: ".tangled/workflows/test.yml", 30 + When: when, 31 + Steps: []Step{ 32 + {Name: "Test", Command: "go test ./..."}, 33 + }, 34 + CloneOpts: CloneOpts{}, // default true 35 + } 36 + 37 + c := Compiler{Trigger: trigger} 38 + cp := c.Compile([]Workflow{wf}) 39 + 40 + assert.Len(t, cp.Workflows, 1) 41 + assert.Equal(t, wf.Name, cp.Workflows[0].Name) 42 + assert.False(t, cp.Workflows[0].Clone.Skip) 43 + assert.False(t, c.Diagnostics.IsErr()) 44 + } 45 + 46 + func TestCompileWorkflow_EmptySteps(t *testing.T) { 47 + wf := Workflow{ 48 + Name: ".tangled/workflows/empty.yml", 49 + When: when, 50 + Steps: []Step{}, // no steps 51 + } 52 + 53 + c := Compiler{Trigger: trigger} 54 + cp := c.Compile([]Workflow{wf}) 55 + 56 + assert.Len(t, cp.Workflows, 0) 57 + assert.Len(t, c.Diagnostics.Warnings, 1) 58 + assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type) 59 + } 60 + 61 + func TestCompileWorkflow_TriggerMismatch(t *testing.T) { 62 + wf := Workflow{ 63 + Name: ".tangled/workflows/mismatch.yml", 64 + When: []Constraint{ 65 + { 66 + Event: []string{"push"}, 67 + Branch: []string{"master"}, // different branch 68 + }, 69 + }, 70 + Steps: []Step{ 71 + {Name: "Lint", Command: "golint ./..."}, 72 + }, 73 + } 74 + 75 + c := Compiler{Trigger: trigger} 76 + cp := c.Compile([]Workflow{wf}) 77 + 78 + assert.Len(t, cp.Workflows, 0) 79 + assert.Len(t, c.Diagnostics.Warnings, 1) 80 + assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type) 81 + } 82 + 83 + func TestCompileWorkflow_CloneFalseWithShallowTrue(t *testing.T) { 84 + wf := Workflow{ 85 + Name: ".tangled/workflows/clone_skip.yml", 86 + When: when, 87 + Steps: []Step{ 88 + {Name: "Skip", Command: "echo skip"}, 89 + }, 90 + CloneOpts: CloneOpts{ 91 + Skip: true, 92 + Depth: 1, 93 + }, // false 94 + } 95 + 96 + c := Compiler{Trigger: trigger} 97 + cp := c.Compile([]Workflow{wf}) 98 + 99 + assert.Len(t, cp.Workflows, 1) 100 + assert.True(t, cp.Workflows[0].Clone.Skip) 101 + assert.Len(t, c.Diagnostics.Warnings, 1) 102 + assert.Equal(t, InvalidConfiguration, c.Diagnostics.Warnings[0].Type) 103 + }