-
Notifications
You must be signed in to change notification settings - Fork 2
/
steps.go
171 lines (142 loc) · 4.54 KB
/
steps.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package pipeline
import (
"errors"
"fmt"
"github.com/buildkite/go-pipeline/ordered"
"github.com/buildkite/go-pipeline/warning"
)
// Sentinel errors that can appear when falling back to UnknownStep.
var (
ErrStepTypeInference = errors.New("cannot infer step type")
ErrUnknownStepType = errors.New("unknown step type")
)
// Compile-time check that *Steps is an ordered.Unmarshaler.
var _ ordered.Unmarshaler = (*Steps)(nil)
// Steps contains multiple steps. It is useful for unmarshaling step sequences,
// since it has custom logic for determining the correct step type.
type Steps []Step
// UnmarshalOrdered unmarshals a slice ([]any) into a slice of steps.
func (s *Steps) UnmarshalOrdered(o any) error {
if o == nil {
if *s == nil {
// `steps: null` is normalised to an empty slice.
*s = Steps{}
}
return nil
}
sl, ok := o.([]any)
if !ok {
return fmt.Errorf("unmarshaling steps: got %T, want a slice ([]any)", sl)
}
// Preallocate slice if not already allocated
if *s == nil {
*s = make(Steps, 0, len(sl))
}
var warns []error
for i, st := range sl {
step, err := unmarshalStep(st)
if w := warning.As(err); w != nil {
warns = append(warns, w.Wrapf("while unmarshaling step %d of %d", i+1, len(sl)))
} else if err != nil {
return err
}
*s = append(*s, step)
}
return warning.Wrap(warns...)
}
func (s Steps) interpolate(tf stringTransformer) error {
return interpolateSlice(tf, s)
}
// unmarshalStep unmarshals into the right kind of Step.
func unmarshalStep(o any) (Step, error) {
switch o := o.(type) {
case string:
return NewScalarStep(o)
case *ordered.MapSA:
return stepFromMap(o)
default:
return nil, fmt.Errorf("unmarshaling step: unsupported type %T", o)
}
}
// stepFromMap parses a step (that was originally a YAML mapping).
func stepFromMap(o *ordered.MapSA) (Step, error) {
sType, hasType := o.Get("type")
var warns []error
var step Step
var err error
if hasType {
sTypeStr, ok := sType.(string)
if !ok {
return nil, fmt.Errorf("unmarshaling step: step's `type` key was %T (value %v), want string", sType, sType)
}
step, err = stepByType(sTypeStr)
} else {
step, err = stepByKeyInference(o)
}
if err != nil {
step = new(UnknownStep)
warns = append(warns, err)
}
// Decode the step (into the right step type).
err = ordered.Unmarshal(o, step)
if w := warning.As(err); w != nil {
warns = append(warns, w)
} else if err != nil {
// Hmm, maybe we picked the wrong kind of step?
// Downgrade this error to a warning.
step = &UnknownStep{Contents: o}
warns = append(warns, warning.Wrapf(err, "fell back using unknown type of step due to an unmarshaling error"))
}
return step, warning.Wrap(warns...)
}
// stepByType returns a new empty step with a type corresponding to the "type"
// field. Unrecognised type values result in an UnknownStep containing an
// error wrapping ErrUnknownStepType.
func stepByType(sType string) (Step, error) {
switch sType {
case "command", "script":
return new(CommandStep), nil
case "wait", "waiter":
return &WaitStep{Contents: map[string]any{}}, nil
case "block", "input", "manual":
return &InputStep{Contents: map[string]any{}}, nil
case "trigger":
return new(TriggerStep), nil
case "group": // as far as i know this doesn't happen, but it's here for completeness
return new(GroupStep), nil
default:
return nil, fmt.Errorf("%w %q", ErrUnknownStepType, sType)
}
}
// stepByKeyInference returns a new empty step with a type based on some heuristic rules
// (first rule wins):
//
// - command, commands, plugins -> CommandStep
// - wait, waiter -> WaitStep
// - block, input, manual -> InputStep
// - trigger: TriggerStep
// - group: GroupStep.
//
// Failure to infer a step type results in an UnknownStep containing an
// error wrapping ErrStepTypeInference.
func stepByKeyInference(o *ordered.MapSA) (Step, error) {
switch {
case o.Contains("command") || o.Contains("commands") || o.Contains("plugins"):
// NB: Some "command" step are commandless containers that exist
// just to run plugins!
return new(CommandStep), nil
case o.Contains("wait") || o.Contains("waiter"):
return new(WaitStep), nil
case o.Contains("block") || o.Contains("input") || o.Contains("manual"):
return new(InputStep), nil
case o.Contains("trigger"):
return new(TriggerStep), nil
case o.Contains("group"):
return new(GroupStep), nil
default:
inferrableKeys := []string{
"command", "commands", "plugins", "wait", "waiter", "block", "input", "manual", "trigger", "group",
}
return nil, fmt.Errorf("%w: need one of %v", ErrStepTypeInference, inferrableKeys)
}
}