Monorepo for Tangled tangled.org
0
fork

Configure Feed

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

at spindle-cron 238 lines 5.5 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-co-op/gocron/v2" 13 "github.com/go-git/go-git/v5/plumbing" 14 "gopkg.in/yaml.v3" 15) 16 17// - when a repo is modified, it results in the trigger of a "Pipeline" 18// - a repo could consist of several workflow files 19// * .tangled/workflows/test.yml 20// * .tangled/workflows/lint.yml 21// - therefore a pipeline consists of several workflows, these execute in parallel 22// - each workflow consists of some execution steps, these execute serially 23 24type ( 25 Pipeline []Workflow 26 27 // this is simply a structural representation of the workflow file 28 Workflow struct { 29 Name string `yaml:"-"` // name of the workflow file 30 Engine string `yaml:"engine"` 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 Cron StringList `yaml:"cron"` // required for schedule events 41 } 42 43 CloneOpts struct { 44 Skip bool `yaml:"skip"` 45 Depth int `yaml:"depth"` 46 IncludeSubmodules bool `yaml:"submodules"` 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 TriggerKindSchedule TriggerKind = "schedule" 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) (bool, error) { 100 // manual triggers always run the workflow 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) 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) (bool, error) { 125 match := true 126 127 // manual triggers always pass this constraint 128 if trigger.Manual != nil { 129 return true, nil 130 } 131 132 // apply event constraints 133 match = match && c.MatchEvent(trigger.Kind) 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 match = match && matched 142 } 143 144 // apply ref constraints for pushes 145 if trigger.Push != nil { 146 matched, err := c.MatchRef(trigger.Push.Ref) 147 if err != nil { 148 return false, err 149 } 150 match = match && matched 151 } 152 153 // apply cron constraints for schedules 154 if trigger.Schedule != nil { 155 s, _ := gocron.NewScheduler() 156 157 _, err := s.NewJob(gocron.CronJob( 158 *trigger.Schedule.CronExpression, 159 false, 160 ), 161 gocron.NewTask( 162 func() {}, 163 ), 164 ) 165 if err != nil { 166 return false, err 167 } 168 } 169 170 return match, nil 171} 172 173func (c *Constraint) MatchRef(ref string) (bool, error) { 174 refName := plumbing.ReferenceName(ref) 175 shortName := refName.Short() 176 177 if refName.IsBranch() { 178 return c.MatchBranch(shortName) 179 } 180 181 if refName.IsTag() { 182 return c.MatchTag(shortName) 183 } 184 185 return false, nil 186} 187 188func (c *Constraint) MatchBranch(branch string) (bool, error) { 189 return matchesPattern(branch, c.Branch) 190} 191 192func (c *Constraint) MatchTag(tag string) (bool, error) { 193 return matchesPattern(tag, c.Tag) 194} 195 196func (c *Constraint) MatchEvent(event string) bool { 197 return slices.Contains(c.Event, event) 198} 199 200// Custom unmarshaller for StringList 201func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error { 202 var stringType string 203 if err := unmarshal(&stringType); err == nil { 204 *s = []string{stringType} 205 return nil 206 } 207 208 var sliceType []any 209 if err := unmarshal(&sliceType); err == nil { 210 211 if sliceType == nil { 212 *s = nil 213 return nil 214 } 215 216 parts := make([]string, len(sliceType)) 217 for k, v := range sliceType { 218 if sv, ok := v.(string); ok { 219 parts[k] = sv 220 } else { 221 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v) 222 } 223 } 224 225 *s = parts 226 return nil 227 } 228 229 return errors.New("failed to unmarshal StringOrSlice") 230} 231 232func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts { 233 return tangled.Pipeline_CloneOpts{ 234 Depth: int64(c.Depth), 235 Skip: c.Skip, 236 Submodules: c.IncludeSubmodules, 237 } 238}