forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package workflow 2 3import ( 4 "errors" 5 "fmt" 6 "slices" 7 "strings" 8 9 "tangled.sh/tangled.sh/core/api/tangled" 10 11 "github.com/go-git/go-git/v5/plumbing" 12 "gopkg.in/yaml.v3" 13) 14 15// - when a repo is modified, it results in the trigger of a "Pipeline" 16// - a repo could consist of several workflow files 17// * .tangled/workflows/test.yml 18// * .tangled/workflows/lint.yml 19// - therefore a pipeline consists of several workflows, these execute in parallel 20// - each workflow consists of some execution steps, these execute serially 21 22type ( 23 Pipeline []Workflow 24 25 // this is simply a structural representation of the workflow file 26 Workflow struct { 27 Name string `yaml:"-"` // name of the workflow file 28 When []Constraint `yaml:"when"` 29 Dependencies Dependencies `yaml:"dependencies"` 30 Steps []Step `yaml:"steps"` 31 Environment map[string]string `yaml:"environment"` 32 CloneOpts CloneOpts `yaml:"clone"` 33 } 34 35 Constraint struct { 36 Event StringList `yaml:"event"` 37 Branch StringList `yaml:"branch"` // this is optional, and only applied on "push" events 38 } 39 40 Dependencies map[string][]string 41 42 CloneOpts struct { 43 Skip bool `yaml:"skip"` 44 Depth int `yaml:"depth"` 45 IncludeSubmodules bool `yaml:"submodules"` 46 } 47 48 Step struct { 49 Name string `yaml:"name"` 50 Command string `yaml:"command"` 51 Environment map[string]string `yaml:"environment"` 52 } 53 54 StringList []string 55 56 TriggerKind string 57) 58 59const ( 60 WorkflowDir = ".tangled/workflows" 61 62 TriggerKindPush TriggerKind = "push" 63 TriggerKindPullRequest TriggerKind = "pull_request" 64 TriggerKindManual TriggerKind = "manual" 65) 66 67func (t TriggerKind) String() string { 68 return strings.ReplaceAll(string(t), "_", " ") 69} 70 71func FromFile(name string, contents []byte) (Workflow, error) { 72 var wf Workflow 73 74 err := yaml.Unmarshal(contents, &wf) 75 if err != nil { 76 return wf, err 77 } 78 79 wf.Name = name 80 81 return wf, nil 82} 83 84// if any of the constraints on a workflow is true, return true 85func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) bool { 86 // manual triggers always run the workflow 87 if trigger.Manual != nil { 88 return true 89 } 90 91 // if not manual, run through the constraint list and see if any one matches 92 for _, c := range w.When { 93 if c.Match(trigger) { 94 return true 95 } 96 } 97 98 // no constraints, always run this workflow 99 if len(w.When) == 0 { 100 return true 101 } 102 103 return false 104} 105 106func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) bool { 107 match := true 108 109 // manual triggers always pass this constraint 110 if trigger.Manual != nil { 111 return true 112 } 113 114 // apply event constraints 115 match = match && c.MatchEvent(trigger.Kind) 116 117 // apply branch constraints for PRs 118 if trigger.PullRequest != nil { 119 match = match && c.MatchBranch(trigger.PullRequest.TargetBranch) 120 } 121 122 // apply ref constraints for pushes 123 if trigger.Push != nil { 124 match = match && c.MatchRef(trigger.Push.Ref) 125 } 126 127 return match 128} 129 130func (c *Constraint) MatchBranch(branch string) bool { 131 return slices.Contains(c.Branch, branch) 132} 133 134func (c *Constraint) MatchRef(ref string) bool { 135 refName := plumbing.ReferenceName(ref) 136 if refName.IsBranch() { 137 return slices.Contains(c.Branch, refName.Short()) 138 } 139 return false 140} 141 142func (c *Constraint) MatchEvent(event string) bool { 143 return slices.Contains(c.Event, event) 144} 145 146// Custom unmarshaller for StringList 147func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error { 148 var stringType string 149 if err := unmarshal(&stringType); err == nil { 150 *s = []string{stringType} 151 return nil 152 } 153 154 var sliceType []any 155 if err := unmarshal(&sliceType); err == nil { 156 157 if sliceType == nil { 158 *s = nil 159 return nil 160 } 161 162 parts := make([]string, len(sliceType)) 163 for k, v := range sliceType { 164 if sv, ok := v.(string); ok { 165 parts[k] = sv 166 } else { 167 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v) 168 } 169 } 170 171 *s = parts 172 return nil 173 } 174 175 return errors.New("failed to unmarshal StringOrSlice") 176} 177 178// conversion utilities to atproto records 179func (d Dependencies) AsRecord() []*tangled.Pipeline_Dependency { 180 var deps []*tangled.Pipeline_Dependency 181 for registry, packages := range d { 182 deps = append(deps, &tangled.Pipeline_Dependency{ 183 Registry: registry, 184 Packages: packages, 185 }) 186 } 187 return deps 188} 189 190func (s Step) AsRecord() tangled.Pipeline_Step { 191 return tangled.Pipeline_Step{ 192 Command: s.Command, 193 Name: s.Name, 194 } 195} 196 197func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts { 198 return tangled.Pipeline_CloneOpts{ 199 Depth: int64(c.Depth), 200 Skip: c.Skip, 201 Submodules: c.IncludeSubmodules, 202 } 203}