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 Engine string `yaml:"engine"`
29 When []Constraint `yaml:"when"`
30 CloneOpts CloneOpts `yaml:"clone"`
31 Raw string `yaml:"-"`
32 }
33
34 Constraint struct {
35 Event StringList `yaml:"event"`
36 Branch StringList `yaml:"branch"` // this is optional, and only applied on "push" events
37 }
38
39 CloneOpts struct {
40 Skip bool `yaml:"skip"`
41 Depth int `yaml:"depth"`
42 IncludeSubmodules bool `yaml:"submodules"`
43 }
44
45 StringList []string
46
47 TriggerKind string
48)
49
50const (
51 WorkflowDir = ".tangled/workflows"
52
53 TriggerKindPush TriggerKind = "push"
54 TriggerKindPullRequest TriggerKind = "pull_request"
55 TriggerKindManual TriggerKind = "manual"
56)
57
58func (t TriggerKind) String() string {
59 return strings.ReplaceAll(string(t), "_", " ")
60}
61
62func FromFile(name string, contents []byte) (Workflow, error) {
63 var wf Workflow
64
65 err := yaml.Unmarshal(contents, &wf)
66 if err != nil {
67 return wf, err
68 }
69
70 wf.Name = name
71 wf.Raw = string(contents)
72
73 return wf, nil
74}
75
76// if any of the constraints on a workflow is true, return true
77func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
78 // manual triggers always run the workflow
79 if trigger.Manual != nil {
80 return true
81 }
82
83 // if not manual, run through the constraint list and see if any one matches
84 for _, c := range w.When {
85 if c.Match(trigger) {
86 return true
87 }
88 }
89
90 // no constraints, always run this workflow
91 if len(w.When) == 0 {
92 return true
93 }
94
95 return false
96}
97
98func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
99 match := true
100
101 // manual triggers always pass this constraint
102 if trigger.Manual != nil {
103 return true
104 }
105
106 // apply event constraints
107 match = match && c.MatchEvent(trigger.Kind)
108
109 // apply branch constraints for PRs
110 if trigger.PullRequest != nil {
111 match = match && c.MatchBranch(trigger.PullRequest.TargetBranch)
112 }
113
114 // apply ref constraints for pushes
115 if trigger.Push != nil {
116 match = match && c.MatchRef(trigger.Push.Ref)
117 }
118
119 return match
120}
121
122func (c *Constraint) MatchBranch(branch string) bool {
123 return slices.Contains(c.Branch, branch)
124}
125
126func (c *Constraint) MatchRef(ref string) bool {
127 refName := plumbing.ReferenceName(ref)
128 if refName.IsBranch() {
129 return slices.Contains(c.Branch, refName.Short())
130 }
131 return false
132}
133
134func (c *Constraint) MatchEvent(event string) bool {
135 return slices.Contains(c.Event, event)
136}
137
138// Custom unmarshaller for StringList
139func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error {
140 var stringType string
141 if err := unmarshal(&stringType); err == nil {
142 *s = []string{stringType}
143 return nil
144 }
145
146 var sliceType []any
147 if err := unmarshal(&sliceType); err == nil {
148
149 if sliceType == nil {
150 *s = nil
151 return nil
152 }
153
154 parts := make([]string, len(sliceType))
155 for k, v := range sliceType {
156 if sv, ok := v.(string); ok {
157 parts[k] = sv
158 } else {
159 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v)
160 }
161 }
162
163 *s = parts
164 return nil
165 }
166
167 return errors.New("failed to unmarshal StringOrSlice")
168}
169
170func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts {
171 return tangled.Pipeline_CloneOpts{
172 Depth: int64(c.Depth),
173 Skip: c.Skip,
174 Submodules: c.IncludeSubmodules,
175 }
176}