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