Monorepo for Tangled tangled.org
2

Configure Feed

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

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