+132
workflow/compile.go
+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
+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
+
}