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