This repository has no description
0

Configure Feed

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

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