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 Environment map[string]string `yaml:"environment"`
51 }
52
53 StringList []string
54)
55
56const (
57 TriggerKindPush string = "push"
58 TriggerKindPullRequest string = "pull_request"
59 TriggerKindManual string = "manual"
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
72 return wf, nil
73}
74
75// if any of the constraints on a workflow is true, return true
76func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
77 // manual triggers always run the workflow
78 if trigger.Manual != nil {
79 return true
80 }
81
82 // if not manual, run through the constraint list and see if any one matches
83 for _, c := range w.When {
84 if c.Match(trigger) {
85 return true
86 }
87 }
88
89 // no constraints, always run this workflow
90 if len(w.When) == 0 {
91 return true
92 }
93
94 return false
95}
96
97func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) bool {
98 match := true
99
100 // manual triggers always pass this constraint
101 if trigger.Manual != nil {
102 return true
103 }
104
105 // apply event constraints
106 match = match && c.MatchEvent(trigger.Kind)
107
108 // apply branch constraints for PRs
109 if trigger.PullRequest != nil {
110 match = match && c.MatchBranch(trigger.PullRequest.TargetBranch)
111 }
112
113 // apply ref constraints for pushes
114 if trigger.Push != nil {
115 match = match && c.MatchRef(trigger.Push.Ref)
116 }
117
118 return match
119}
120
121func (c *Constraint) MatchBranch(branch string) bool {
122 return slices.Contains(c.Branch, branch)
123}
124
125func (c *Constraint) MatchRef(ref string) bool {
126 refName := plumbing.ReferenceName(ref)
127 if refName.IsBranch() {
128 return slices.Contains(c.Branch, refName.Short())
129 }
130 fmt.Println("no", c.Branch, refName.Short())
131
132 return false
133}
134
135func (c *Constraint) MatchEvent(event string) bool {
136 return slices.Contains(c.Event, event)
137}
138
139// Custom unmarshaller for StringList
140func (s *StringList) UnmarshalYAML(unmarshal func(any) error) error {
141 var stringType string
142 if err := unmarshal(&stringType); err == nil {
143 *s = []string{stringType}
144 return nil
145 }
146
147 var sliceType []any
148 if err := unmarshal(&sliceType); err == nil {
149
150 if sliceType == nil {
151 *s = nil
152 return nil
153 }
154
155 parts := make([]string, len(sliceType))
156 for k, v := range sliceType {
157 if sv, ok := v.(string); ok {
158 parts[k] = sv
159 } else {
160 return fmt.Errorf("cannot unmarshal '%v' of type %T into a string value", v, v)
161 }
162 }
163
164 *s = parts
165 return nil
166 }
167
168 return errors.New("failed to unmarshal StringOrSlice")
169}
170
171// conversion utilities to atproto records
172func (d Dependencies) AsRecord() []tangled.Pipeline_Dependencies_Elem {
173 var deps []tangled.Pipeline_Dependencies_Elem
174 for registry, packages := range d {
175 deps = append(deps, tangled.Pipeline_Dependencies_Elem{
176 Registry: registry,
177 Packages: packages,
178 })
179 }
180 return deps
181}
182
183func (s Step) AsRecord() tangled.Pipeline_Step {
184 return tangled.Pipeline_Step{
185 Command: s.Command,
186 Name: s.Name,
187 }
188}
189
190func (c CloneOpts) AsRecord() tangled.Pipeline_CloneOpts {
191 return tangled.Pipeline_CloneOpts{
192 Depth: int64(c.Depth),
193 Skip: c.Skip,
194 Submodules: c.IncludeSubmodules,
195 }
196}