1package workflow
2
3import (
4 "fmt"
5
6 "tangled.sh/tangled.sh/core/api/tangled"
7)
8
9type RawWorkflow struct {
10 Name string
11 Contents []byte
12}
13
14type RawPipeline = []RawWorkflow
15
16type Compiler struct {
17 Trigger tangled.Pipeline_TriggerMetadata
18 Diagnostics Diagnostics
19}
20
21type Diagnostics struct {
22 Errors []Error
23 Warnings []Warning
24}
25
26func (d *Diagnostics) IsEmpty() bool {
27 return len(d.Errors) == 0 && len(d.Warnings) == 0
28}
29
30func (d *Diagnostics) Combine(o Diagnostics) {
31 d.Errors = append(d.Errors, o.Errors...)
32 d.Warnings = append(d.Warnings, o.Warnings...)
33}
34
35func (d *Diagnostics) AddWarning(path string, kind WarningKind, reason string) {
36 d.Warnings = append(d.Warnings, Warning{path, kind, reason})
37}
38
39func (d *Diagnostics) AddError(path string, err error) {
40 d.Errors = append(d.Errors, Error{path, err})
41}
42
43func (d Diagnostics) IsErr() bool {
44 return len(d.Errors) != 0
45}
46
47type Error struct {
48 Path string
49 Error error
50}
51
52func (e Error) String() string {
53 return fmt.Sprintf("error: %s: %s", e.Path, e.Error.Error())
54}
55
56type Warning struct {
57 Path string
58 Type WarningKind
59 Reason string
60}
61
62func (w Warning) String() string {
63 return fmt.Sprintf("warning: %s: %s: %s", w.Path, w.Type, w.Reason)
64}
65
66type WarningKind string
67
68var (
69 WorkflowSkipped WarningKind = "workflow skipped"
70 InvalidConfiguration WarningKind = "invalid configuration"
71)
72
73func (compiler *Compiler) Parse(p RawPipeline) Pipeline {
74 var pp Pipeline
75
76 for _, w := range p {
77 wf, err := FromFile(w.Name, w.Contents)
78 if err != nil {
79 compiler.Diagnostics.AddError(w.Name, err)
80 continue
81 }
82
83 pp = append(pp, wf)
84 }
85
86 return pp
87}
88
89// convert a repositories' workflow files into a fully compiled pipeline that runners accept
90func (compiler *Compiler) Compile(p Pipeline) tangled.Pipeline {
91 cp := tangled.Pipeline{
92 TriggerMetadata: &compiler.Trigger,
93 }
94
95 for _, wf := range p {
96 cw := compiler.compileWorkflow(wf)
97
98 // empty workflows are not added to the pipeline
99 if len(cw.Steps) == 0 {
100 continue
101 }
102
103 cp.Workflows = append(cp.Workflows, &cw)
104 }
105
106 return cp
107}
108
109func (compiler *Compiler) compileWorkflow(w Workflow) tangled.Pipeline_Workflow {
110 cw := tangled.Pipeline_Workflow{}
111
112 if !w.Match(compiler.Trigger) {
113 compiler.Diagnostics.AddWarning(
114 w.Name,
115 WorkflowSkipped,
116 fmt.Sprintf("did not match trigger %s", compiler.Trigger.Kind),
117 )
118 return cw
119 }
120
121 if len(w.Steps) == 0 {
122 compiler.Diagnostics.AddWarning(
123 w.Name,
124 WorkflowSkipped,
125 "empty workflow",
126 )
127 return cw
128 }
129
130 // validate clone options
131 compiler.analyzeCloneOptions(w)
132
133 cw.Name = w.Name
134 cw.Dependencies = w.Dependencies.AsRecord()
135 for _, s := range w.Steps {
136 step := tangled.Pipeline_Step{
137 Command: s.Command,
138 Name: s.Name,
139 }
140 for k, v := range s.Environment {
141 e := &tangled.Pipeline_Pair{
142 Key: k,
143 Value: v,
144 }
145 step.Environment = append(step.Environment, e)
146 }
147 cw.Steps = append(cw.Steps, &step)
148 }
149 for k, v := range w.Environment {
150 e := &tangled.Pipeline_Pair{
151 Key: k,
152 Value: v,
153 }
154 cw.Environment = append(cw.Environment, e)
155 }
156
157 o := w.CloneOpts.AsRecord()
158 cw.Clone = &o
159
160 return cw
161}
162
163func (compiler *Compiler) analyzeCloneOptions(w Workflow) {
164 if w.CloneOpts.Skip && w.CloneOpts.IncludeSubmodules {
165 compiler.Diagnostics.AddWarning(
166 w.Name,
167 InvalidConfiguration,
168 "cannot apply `clone.skip` and `clone.submodules`",
169 )
170 }
171
172 if w.CloneOpts.Skip && w.CloneOpts.Depth > 0 {
173 compiler.Diagnostics.AddWarning(
174 w.Name,
175 InvalidConfiguration,
176 "cannot apply `clone.skip` and `clone.depth`",
177 )
178 }
179}