Monorepo for Tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at icy/xmmrnx 242 lines 5.8 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 Paths StringList `yaml:"paths"` // optional; only run if any changed file matches a glob pattern 40 } 41 42 CloneOpts struct { 43 Skip bool `yaml:"skip"` 44 Depth int `yaml:"depth"` 45 IncludeSubmodules bool `yaml:"submodules"` 46 } 47 48 StringList []string 49 50 TriggerKind string 51) 52 53const ( 54 WorkflowDir = ".tangled/workflows" 55 56 TriggerKindPush TriggerKind = "push" 57 TriggerKindPullRequest TriggerKind = "pull_request" 58 TriggerKindManual TriggerKind = "manual" 59) 60 61func (t TriggerKind) String() string { 62 return strings.ReplaceAll(string(t), "_", " ") 63} 64 65// matchesPattern checks if a name matches any of the given patterns. 66// Patterns can be exact matches or glob patterns using * and **. 67// * matches any sequence of non-separator characters 68// ** matches any sequence of characters including separators 69func matchesPattern(name string, patterns []string) (bool, error) { 70 for _, pattern := range patterns { 71 matched, err := doublestar.Match(pattern, name) 72 if err != nil { 73 return false, err 74 } 75 if matched { 76 return true, nil 77 } 78 } 79 return false, nil 80} 81 82func FromFile(name string, contents []byte) (Workflow, error) { 83 var wf Workflow 84 85 err := yaml.Unmarshal(contents, &wf) 86 if err != nil { 87 return wf, err 88 } 89 90 wf.Name = name 91 wf.Raw = string(contents) 92 93 return wf, nil 94} 95 96// if any of the constraints on a workflow is true, return true 97func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) { 98 // manual triggers always run the workflow 99 if trigger.Manual != nil { 100 return true, nil 101 } 102 103 // if not manual, run through the constraint list and see if any one matches 104 for _, c := range w.When { 105 matched, err := c.Match(trigger, changedFiles) 106 if err != nil { 107 return false, err 108 } 109 if matched { 110 return true, nil 111 } 112 } 113 114 // no constraints, always run this workflow 115 if len(w.When) == 0 { 116 return true, nil 117 } 118 119 return false, nil 120} 121 122func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) { 123 match := true 124 125 // manual triggers always pass this constraint 126 if trigger.Manual != nil { 127 return true, nil 128 } 129 130 // apply event constraints 131 match = match && c.MatchEvent(trigger.Kind) 132 133 // apply branch constraints for PRs 134 if trigger.PullRequest != nil { 135 matched, err := c.MatchBranch(trigger.PullRequest.TargetBranch) 136 if err != nil { 137 return false, err 138 } 139 match = match && matched 140 } 141 142 // apply ref constraints for pushes 143 if trigger.Push != nil { 144 matched, err := c.MatchRef(trigger.Push.Ref) 145 if err != nil { 146 return false, err 147 } 148 match = match && matched 149 } 150 151 // apply paths filter: if specified, at least one changed file must match 152 if len(c.Paths) > 0 { 153 matched, err := matchesAnyFile(changedFiles, c.Paths) 154 if err != nil { 155 return false, err 156 } 157 match = match && matched 158 } 159 160 return match, nil 161} 162 163// matchesAnyFile returns true if any file in files matches any of the glob patterns. 164func matchesAnyFile(files []string, patterns []string) (bool, error) { 165 for _, f := range files { 166 matched, err := matchesPattern(f, patterns) 167 if err != nil { 168 return false, err 169 } 170 if matched { 171 return true, nil 172 } 173 } 174 return false, nil 175} 176 177func (c *Constraint) MatchRef(ref string) (bool, error) { 178 refName := plumbing.ReferenceName(ref) 179 shortName := refName.Short() 180 181 if refName.IsBranch() { 182 return c.MatchBranch(shortName) 183 } 184 185 if refName.IsTag() { 186 return c.MatchTag(shortName) 187 } 188 189 return false, nil 190} 191 192func (c *Constraint) MatchBranch(branch string) (bool, error) { 193 return matchesPattern(branch, c.Branch) 194} 195 196func (c *Constraint) MatchTag(tag string) (bool, error) { 197 return matchesPattern(tag, c.Tag) 198} 199 200func (c *Constraint) MatchEvent(event string) bool { 201 return slices.Contains(c.Event, event) 202} 203 204// Custom unmarshaller for StringList 205func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error { 206 var stringType string 207 if err := unmarshal(&stringType); err == nil { 208 *s = []string{stringType} 209 return nil 210 } 211 212 var sliceType []any 213 if err := unmarshal(&sliceType); err == nil { 214 215 if sliceType == nil { 216 *s = nil 217 return nil 218 } 219 220 parts := make([]string, len(sliceType)) 221 for k, v := range sliceType { 222 if sv, ok := v.(string); ok { 223 parts[k] = sv 224 } else { 225 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v) 226 } 227 } 228 229 *s = parts 230 return nil 231 } 232 233 return errors.New("failed to unmarshal StringOrSlice") 234} 235 236func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts { 237 return tangled.Pipeline_CloneOpts{ 238 Depth: int64(c.Depth), 239 Skip: c.Skip, 240 Submodules: c.IncludeSubmodules, 241 } 242}