diff --git a/workflow/compile.go b/workflow/compile.go new file mode 100644 index 0000000..df53784 --- /dev/null +++ b/workflow/compile.go @@ -0,0 +1,132 @@ +package workflow + +import ( + "fmt" + + "tangled.sh/tangled.sh/core/api/tangled" +) + +type Compiler struct { + Trigger tangled.Pipeline_TriggerMetadata + Diagnostics Diagnostics +} + +type Diagnostics struct { + Errors []error + Warnings []Warning +} + +func (d *Diagnostics) Combine(o Diagnostics) { + d.Errors = append(d.Errors, o.Errors...) + d.Warnings = append(d.Warnings, o.Warnings...) +} + +func (d *Diagnostics) AddWarning(path string, kind WarningKind, reason string) { + d.Warnings = append(d.Warnings, Warning{path, kind, reason}) +} + +func (d *Diagnostics) AddError(err error) { + d.Errors = append(d.Errors, err) +} + +func (d Diagnostics) IsErr() bool { + return len(d.Errors) != 0 +} + +type Warning struct { + Path string + Type WarningKind + Reason string +} + +type WarningKind string + +var ( + WorkflowSkipped WarningKind = "workflow skipped" + InvalidConfiguration WarningKind = "invalid configuration" +) + +// convert a repositories' workflow files into a fully compiled pipeline that runners accept +func (compiler *Compiler) Compile(p Pipeline) tangled.Pipeline { + cp := tangled.Pipeline{ + TriggerMetadata: &compiler.Trigger, + } + + for _, w := range p { + cw := compiler.compileWorkflow(w) + + // empty workflows are not added to the pipeline + if len(cw.Steps) == 0 { + continue + } + + cp.Workflows = append(cp.Workflows, &cw) + } + + return cp +} + +func (compiler *Compiler) compileWorkflow(w Workflow) tangled.Pipeline_Workflow { + cw := tangled.Pipeline_Workflow{} + + if !w.Match(compiler.Trigger) { + compiler.Diagnostics.AddWarning( + w.Name, + WorkflowSkipped, + fmt.Sprintf("did not match trigger %s", compiler.Trigger.Kind), + ) + return cw + } + + if len(w.Steps) == 0 { + compiler.Diagnostics.AddWarning( + w.Name, + WorkflowSkipped, + "empty workflow", + ) + return cw + } + + // validate clone options + compiler.analyzeCloneOptions(w) + + cw.Name = w.Name + cw.Dependencies = w.Dependencies.AsRecord() + for _, s := range w.Steps { + step := tangled.Pipeline_Step{ + Command: s.Command, + Name: s.Name, + } + cw.Steps = append(cw.Steps, &step) + } + for k, v := range w.Environment { + e := &tangled.Pipeline_Workflow_Environment_Elem{ + Key: k, + Value: v, + } + cw.Environment = append(cw.Environment, e) + } + + o := w.CloneOpts.AsRecord() + cw.Clone = &o + + return cw +} + +func (compiler *Compiler) analyzeCloneOptions(w Workflow) { + if w.CloneOpts.Skip && w.CloneOpts.IncludeSubmodules { + compiler.Diagnostics.AddWarning( + w.Name, + InvalidConfiguration, + "cannot apply `clone.skip` and `clone.submodules`", + ) + } + + if w.CloneOpts.Skip && w.CloneOpts.Depth > 0 { + compiler.Diagnostics.AddWarning( + w.Name, + InvalidConfiguration, + "cannot apply `clone.skip` and `clone.depth`", + ) + } +} diff --git a/workflow/compile_test.go b/workflow/compile_test.go new file mode 100644 index 0000000..dbefee4 --- /dev/null +++ b/workflow/compile_test.go @@ -0,0 +1,103 @@ +package workflow + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "tangled.sh/tangled.sh/core/api/tangled" +) + +var trigger = tangled.Pipeline_TriggerMetadata{ + Kind: TriggerKindPush, + Push: &tangled.Pipeline_PushTriggerData{ + Ref: "refs/heads/main", + OldSha: strings.Repeat("0", 40), + NewSha: strings.Repeat("f", 40), + }, +} + +var when = []Constraint{ + { + Event: []string{"push"}, + Branch: []string{"main"}, + }, +} + +func TestCompileWorkflow_MatchingWorkflowWithSteps(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/test.yml", + When: when, + Steps: []Step{ + {Name: "Test", Command: "go test ./..."}, + }, + CloneOpts: CloneOpts{}, // default true + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 1) + assert.Equal(t, wf.Name, cp.Workflows[0].Name) + assert.False(t, cp.Workflows[0].Clone.Skip) + assert.False(t, c.Diagnostics.IsErr()) +} + +func TestCompileWorkflow_EmptySteps(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/empty.yml", + When: when, + Steps: []Step{}, // no steps + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 0) + assert.Len(t, c.Diagnostics.Warnings, 1) + assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type) +} + +func TestCompileWorkflow_TriggerMismatch(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/mismatch.yml", + When: []Constraint{ + { + Event: []string{"push"}, + Branch: []string{"master"}, // different branch + }, + }, + Steps: []Step{ + {Name: "Lint", Command: "golint ./..."}, + }, + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 0) + assert.Len(t, c.Diagnostics.Warnings, 1) + assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type) +} + +func TestCompileWorkflow_CloneFalseWithShallowTrue(t *testing.T) { + wf := Workflow{ + Name: ".tangled/workflows/clone_skip.yml", + When: when, + Steps: []Step{ + {Name: "Skip", Command: "echo skip"}, + }, + CloneOpts: CloneOpts{ + Skip: true, + Depth: 1, + }, // false + } + + c := Compiler{Trigger: trigger} + cp := c.Compile([]Workflow{wf}) + + assert.Len(t, cp.Workflows, 1) + assert.True(t, cp.Workflows[0].Clone.Skip) + assert.Len(t, c.Diagnostics.Warnings, 1) + assert.Equal(t, InvalidConfiguration, c.Diagnostics.Warnings[0].Type) +}