1package workflow
2
3import (
4 "errors"
5 "fmt"
6 "slices"
7
8 "tangled.sh/tangled.sh/core/api/tangled"
9
10 "github.com/go-git/go-git/v5/plumbing"
11 "gopkg.in/yaml.v3"
12)
13
14// - when a repo is modified, it results in the trigger of a "Pipeline"
15// - a repo could consist of several workflow files
16// * .tangled/workflows/test.yml
17// * .tangled/workflows/lint.yml
18// - therefore a pipeline consists of several workflows, these execute in parallel
19// - each workflow consists of some execution steps, these execute serially
20
21type (
22 Pipeline []Workflow
23
24 // this is simply a structural representation of the workflow file
25 Workflow struct {
26 Name string `yaml:"-"` // name of the workflow file
27 When []Constraint `yaml:"when"`
28 Dependencies Dependencies `yaml:"dependencies"`
29 Steps []Step `yaml:"steps"`
30 Environment map[string]string `yaml:"environment"`
31 CloneOpts CloneOpts `yaml:"clone"`
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 Dependencies map[string][]string
40
41 CloneOpts struct {
42 Skip bool `yaml:"skip"`
43 Depth int `yaml:"depth"`
44 IncludeSubmodules bool `yaml:"submodules"`
45 }
46
47 Step struct {
48 Name string `yaml:"name"`
49 Command string `yaml:"command"`
50 }
51
52 StringList []string
53)
54
55const (
56 TriggerKindPush string = "push"
57 TriggerKindPullRequest string = "pull_request"
58 TriggerKindManual string = "manual"
59)
60
61func FromFile(name string, contents []byte) (Workflow, error) {
62 var wf Workflow
63
64 err := yaml.Unmarshal(contents, &wf)
65 if err != nil {
66 return wf, err
67 }
68
69 wf.Name = name
70
71 return wf, nil
72}
73
74// if any of the constraints on a workflow is true, return true
75func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
76 // manual triggers always run the workflow
77 if trigger.Manual != nil {
78 return true
79 }
80
81 // if not manual, run through the constraint list and see if any one matches
82 for _, c := range w.When {
83 if c.Match(trigger) {
84 return true
85 }
86 }
87
88 // no constraints, always run this workflow
89 if len(w.When) == 0 {
90 return true
91 }
92
93 return false
94}
95
96func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
97 match := true
98
99 // manual triggers always pass this constraint
100 if trigger.Manual != nil {
101 return true
102 }
103
104 // apply event constraints
105 match = match && c.MatchEvent(trigger.Kind)
106
107 // apply branch constraints for PRs
108 if trigger.PullRequest != nil {
109 match = match && c.MatchBranch(trigger.PullRequest.TargetBranch)
110 }
111
112 // apply ref constraints for pushes
113 if trigger.Push != nil {
114 match = match && c.MatchRef(trigger.Push.Ref)
115 }
116
117 return match
118}
119
120func (c *Constraint) MatchBranch(branch string) bool {
121 return slices.Contains(c.Branch, branch)
122}
123
124func (c *Constraint) MatchRef(ref string) bool {
125 refName := plumbing.ReferenceName(ref)
126 if refName.IsBranch() {
127 return slices.Contains(c.Branch, refName.Short())
128 }
129 fmt.Println("no", 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
170// conversion utilities to atproto records
171func (d Dependencies) AsRecord() []tangled.Pipeline_Dependencies_Elem {
172 var deps []tangled.Pipeline_Dependencies_Elem
173 for registry, packages := range d {
174 deps = append(deps, tangled.Pipeline_Dependencies_Elem{
175 Registry: registry,
176 Packages: packages,
177 })
178 }
179 return deps
180}
181
182func (s Step) AsRecord() tangled.Pipeline_Step {
183 return tangled.Pipeline_Step{
184 Command: s.Command,
185 Name: s.Name,
186 }
187}
188
189func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts {
190 return tangled.Pipeline_CloneOpts{
191 Depth: int64(c.Depth),
192 Skip: c.Skip,
193 Submodules: c.IncludeSubmodules,
194 }
195}