forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
at master 5.1 kB view raw
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}