forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package workflow
2
3import (
4 "errors"
5 "fmt"
6 "slices"
7 "strings"
8
9 "tangled.org/core/api/tangled"
10
11 "github.com/bmatcuk/doublestar/v4"
12 "github.com/go-git/go-git/v5/plumbing"
13 "gopkg.in/yaml.v3"
14)
15
16// - when a repo is modified, it results in the trigger of a "Pipeline"
17// - a repo could consist of several workflow files
18// * .tangled/workflows/test.yml
19// * .tangled/workflows/lint.yml
20// - therefore a pipeline consists of several workflows, these execute in parallel
21// - each workflow consists of some execution steps, these execute serially
22
23type (
24 Pipeline []Workflow
25
26 // this is simply a structural representation of the workflow file
27 Workflow struct {
28 Name string `yaml:"-"` // name of the workflow file
29 Engine string `yaml:"engine"`
30 When []Constraint `yaml:"when"`
31 CloneOpts CloneOpts `yaml:"clone"`
32 Raw string `yaml:"-"`
33 }
34
35 Constraint struct {
36 Event StringList `yaml:"event"`
37 Branch StringList `yaml:"branch"` // required for pull_request; for push, either branch or tag must be specified
38 Tag StringList `yaml:"tag"` // optional; only applies to push events
39 }
40
41 CloneOpts struct {
42 Skip bool `yaml:"skip"`
43 Depth int `yaml:"depth"`
44 IncludeSubmodules bool `yaml:"submodules"`
45 }
46
47 StringList []string
48
49 TriggerKind string
50)
51
52const (
53 WorkflowDir = ".tangled/workflows"
54
55 TriggerKindPush TriggerKind = "push"
56 TriggerKindPullRequest TriggerKind = "pull_request"
57 TriggerKindManual TriggerKind = "manual"
58)
59
60func (t TriggerKind) String() string {
61 return strings.ReplaceAll(string(t), "_", " ")
62}
63
64// matchesPattern checks if a name matches any of the given patterns.
65// Patterns can be exact matches or glob patterns using * and **.
66// * matches any sequence of non-separator characters
67// ** matches any sequence of characters including separators
68func matchesPattern(name string, patterns []string) (bool, error) {
69 for _, pattern := range patterns {
70 matched, err := doublestar.Match(pattern, name)
71 if err != nil {
72 return false, err
73 }
74 if matched {
75 return true, nil
76 }
77 }
78 return false, nil
79}
80
81func FromFile(name string, contents []byte) (Workflow, error) {
82 var wf Workflow
83
84 err := yaml.Unmarshal(contents, &wf)
85 if err != nil {
86 return wf, err
87 }
88
89 wf.Name = name
90 wf.Raw = string(contents)
91
92 return wf, nil
93}
94
95// if any of the constraints on a workflow is true, return true
96func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) (bool, error) {
97 // manual triggers always run the workflow
98 if trigger.Manual != nil {
99 return true, nil
100 }
101
102 // if not manual, run through the constraint list and see if any one matches
103 for _, c := range w.When {
104 matched, err := c.Match(trigger)
105 if err != nil {
106 return false, err
107 }
108 if matched {
109 return true, nil
110 }
111 }
112
113 // no constraints, always run this workflow
114 if len(w.When) == 0 {
115 return true, nil
116 }
117
118 return false, nil
119}
120
121func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) (bool, error) {
122 match := true
123
124 // manual triggers always pass this constraint
125 if trigger.Manual != nil {
126 return true, nil
127 }
128
129 // apply event constraints
130 match = match && c.MatchEvent(trigger.Kind)
131
132 // apply branch constraints for PRs
133 if trigger.PullRequest != nil {
134 matched, err := c.MatchBranch(trigger.PullRequest.TargetBranch)
135 if err != nil {
136 return false, err
137 }
138 match = match && matched
139 }
140
141 // apply ref constraints for pushes
142 if trigger.Push != nil {
143 matched, err := c.MatchRef(trigger.Push.Ref)
144 if err != nil {
145 return false, err
146 }
147 match = match && matched
148 }
149
150 return match, nil
151}
152
153func (c *Constraint) MatchRef(ref string) (bool, error) {
154 refName := plumbing.ReferenceName(ref)
155 shortName := refName.Short()
156
157 if refName.IsBranch() {
158 return c.MatchBranch(shortName)
159 }
160
161 if refName.IsTag() {
162 return c.MatchTag(shortName)
163 }
164
165 return false, nil
166}
167
168func (c *Constraint) MatchBranch(branch string) (bool, error) {
169 return matchesPattern(branch, c.Branch)
170}
171
172func (c *Constraint) MatchTag(tag string) (bool, error) {
173 return matchesPattern(tag, c.Tag)
174}
175
176func (c *Constraint) MatchEvent(event string) bool {
177 return slices.Contains(c.Event, event)
178}
179
180// Custom unmarshaller for StringList
181func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error {
182 var stringType string
183 if err := unmarshal(&stringType); err == nil {
184 *s = []string{stringType}
185 return nil
186 }
187
188 var sliceType []any
189 if err := unmarshal(&sliceType); err == nil {
190
191 if sliceType == nil {
192 *s = nil
193 return nil
194 }
195
196 parts := make([]string, len(sliceType))
197 for k, v := range sliceType {
198 if sv, ok := v.(string); ok {
199 parts[k] = sv
200 } else {
201 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v)
202 }
203 }
204
205 *s = parts
206 return nil
207 }
208
209 return errors.New("failed to unmarshal StringOrSlice")
210}
211
212func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts {
213 return tangled.Pipeline_CloneOpts{
214 Depth: int64(c.Depth),
215 Skip: c.Skip,
216 Submodules: c.IncludeSubmodules,
217 }
218}