forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}