1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6 "strings"
7
8 "bnema.dev/ermite-actions/expr"
9 "bnema.dev/ermite-actions/template"
10 "bnema.dev/ermite-actions/workflow"
11)
12
13// convertSteps is the runner's PipelineTemplateConverter.ConvertToSteps
14// (Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs).
15// It is not the conversion of package workflow, which reads the steps of a
16// workflow file: this one throws at the first token of the wrong type, keeps
17// a step whose "uses" has the wrong form (and then fails on it later if it has
18// no id), accepts ids that start with "__", and names a "$/" action
19// "selfRepository". See the runner for each of those.
20func (l *loader) convertSteps(items *template.Sequence) []*workflow.Step {
21 names := newRefNames()
22 var result []*workflow.Step
23 var refs []*stepRef
24 for _, it := range items.Items {
25 s, ref := l.convertStep(it, names)
26 if s != nil {
27 result = append(result, s)
28 refs = append(refs, ref)
29 }
30 }
31 for i, s := range result {
32 if s.ID != "" {
33 continue
34 }
35 ref := refs[i]
36 if ref == nil {
37 // The runner dereferences the Reference of a step whose "uses" had
38 // the wrong form.
39 throwf(exNullRef, "Object reference not set to an instance of an object.")
40 }
41 name := ref.name
42 if name == "" {
43 name = "run"
44 }
45 names.appendSegment("__" + name)
46 s.ID = names.build()
47 }
48 return result
49}
50
51// stepRef is what the runner makes of a step's action reference that decides
52// the generated id: the image of a docker:// action, "owner/repo", "self" for
53// a local action, "selfRepository" for a $/ action, "" for a run step.
54type stepRef struct{ name string }
55
56func (l *loader) convertStep(t template.Token, names *refNames) (*workflow.Step, *stepRef) {
57 m := assertMapping(t, "steps item")
58 step := &workflow.Step{Pos: t.Pos()}
59 var run template.Token
60 var ifToken template.Token
61 var uses *template.String
62 for _, p := range m.Pairs {
63 key := assertString(p.Key, "steps item key")
64 v := p.Value
65 switch key.Value {
66 case "continue-on-error":
67 if !template.IsExpression(v) {
68 assertBoolean(v, "step continue-on-error")
69 }
70 step.ContinueOnError = assertScalar(v, "steps continue-on-error")
71 case "env":
72 if !template.IsExpression(v) {
73 for _, kv := range assertMapping(v, "environment").Pairs {
74 if template.IsExpression(kv.Key) {
75 continue
76 }
77 assertString(kv.Key, "environment key")
78 if template.IsExpression(kv.Value) {
79 continue
80 }
81 assertString(kv.Value, "environment value")
82 }
83 }
84 if em, ok := v.(*template.Mapping); ok {
85 step.Env = em
86 } else {
87 step.EnvExpression = v
88 }
89 case "id":
90 id := assertString(v, "steps item id")
91 if id.Value != "" {
92 if msg := names.tryAddKnownName(id.Value); msg != "" {
93 l.ctx.ErrorAt(id, msg)
94 }
95 }
96 step.ID, step.Explicit = id.Value, id.Value != ""
97 case "if":
98 ifToken = assertScalar(v, "steps item if")
99 case "name":
100 step.Name = assertScalar(v, "steps item name")
101 case "run":
102 run = assertScalar(v, "steps item run")
103 case "shell":
104 step.Shell = assertScalar(v, "steps item shell")
105 case "timeout-minutes":
106 if !template.IsExpression(v) {
107 if _, ok := v.(*template.Number); !ok {
108 assertFail(v, "step timeout-minutes", "NumberToken")
109 }
110 }
111 step.TimeoutMinutes = assertScalar(v, "steps item timeout-minutes")
112 case "uses":
113 uses = assertString(v, "steps item uses")
114 case "with":
115 if !template.IsExpression(v) {
116 for _, kv := range assertMapping(v, "inputs").Pairs {
117 if template.IsExpression(kv.Key) {
118 continue
119 }
120 assertString(kv.Key, "inputs key")
121 if template.IsExpression(kv.Value) {
122 continue
123 }
124 assertString(kv.Value, "inputs value")
125 }
126 }
127 if wm, ok := v.(*template.Mapping); ok {
128 step.With = wm
129 } else {
130 step.WithExpression = v
131 }
132 case "working-directory":
133 step.WorkingDirectory = assertScalar(v, "steps item working-directory")
134 case "clean", "fetch-depth", "lfs", "path", "submodules":
135 assertScalar(v, "steps item "+key.Value)
136 default:
137 throwf(exArgument, "Error while reading 'steps item key'. Unexpected value '%s'", key.Value)
138 }
139 }
140 step.If = l.convertIf(ifToken)
141
142 if run != nil {
143 step.Run = run
144 return step, &stepRef{}
145 }
146 if uses == nil {
147 assertFail(nil, "steps item uses", "StringToken")
148 }
149 step.Uses = uses
150 u, err := ParseUses(uses.Value)
151 if err != nil {
152 l.ctx.ErrorAt(uses, err.Error())
153 if strings.HasPrefix(uses.Value, "$/") {
154 // A $/ value with a ref drops the step.
155 return nil, nil
156 }
157 return step, nil
158 }
159 ref := &stepRef{name: "self"}
160 switch u.Kind {
161 case UsesDocker:
162 ref.name = u.Image
163 case UsesRepository:
164 ref.name = u.Name
165 case UsesSelfRepository:
166 ref.name = "selfRepository"
167 }
168 return step, ref
169}
170
171// stepIfContexts are the contexts the runner's converter grants the "if" of a
172// step on top of the ones the schema's step-if gives (which lacks vars and
173// needs): s_stepNamedValues.
174var stepIfContexts = []string{"vars", "needs"}
175
176// convertIf is ConvertToIfCondition for a step: a condition without a status
177// function is wrapped as "success() && (cond)", an empty one is "success()".
178func (l *loader) convertIf(tok template.Token) *template.BasicExpression {
179 def := l.schema.Info("step-if").WithContexts(stepIfContexts...)
180 var condition string
181 switch s := tok.(type) {
182 case nil:
183 case *template.BasicExpression:
184 condition = s.Expression
185 default:
186 condition = assertString(tok, "step if").Value
187 }
188 final := "success()"
189 if !isNullOrWhiteSpace(condition) {
190 node, err := expr.Parse(condition, def.NamedContexts(), def.Functions())
191 if err != nil {
192 l.ctx.ErrorAt(tok, err.Error())
193 return nil
194 }
195 final = condition
196 if node != nil && !workflow.HasStatusFunction(node) {
197 final = "success() && (" + condition + ")"
198 }
199 }
200 var o template.Origin
201 if tok != nil {
202 o = template.Origin{File: tok.File(), Pos: tok.Pos()}
203 }
204 return template.NewExpression(o, final, def)
205}