1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "context"
7 "errors"
8 "strings"
9 "time"
10
11 "bnema.dev/ermite-actions/action"
12 "bnema.dev/ermite-actions/expr"
13 "bnema.dev/ermite-actions/template"
14 "bnema.dev/ermite-actions/workflow"
15)
16
17// ---------------------------------------------------------------- steps
18
19// stepRun is the state of one step being run; it implements Sink and
20// commandHost. An embedded step (inside a composite action) is a stepRun too,
21// with a parent and the number of the outer step: its log lines belong to it.
22type stepRun struct {
23 j *job
24 num int
25 st *workflow.Step
26 n *node
27 stage stage
28 parent *stepRun
29 env *ordered // the step's environment: job env + step env
30 overrides *ordered // the step env of this step and of its composite parents
31 cmdResult *stepResult
32 outcome *stepResult
33 // envFailed: the env of an embedded step failed to evaluate, which marked it
34 // failed before it ran (CompositeActionHandler.cs:296-302).
35 envFailed bool
36 // ctxID: see qstep.ctxID.
37 ctxID string
38 cmds *commandManager
39 // result is ExecutionContext.Result of a step that runs embedded steps: it
40 // gives action_status to them.
41 result *stepResult
42 // fc is the file commands of the step: the job's for a step of the job, a
43 // fork for an embedded one.
44 fc *FileCommands
45 // actEnv, when set, is the environment of the process of an action step
46 // (env context, STATE_ and INPUT_); rawEnvList replaces the whole
47 // environment of the process (docker, whose -e options read it).
48 actEnv *ordered
49 rawEnvList []string
50 // def is the action the step runs.
51 def *actionDef
52 nodeVersion string
53 // serverURL, when set, replaces GITHUB_SERVER_URL in the environment of the
54 // process only (see setArtifactsServerURL); the github context keeps its value.
55 serverURL string
56 // scope is what the step shares with the step that contains it, see
57 // stepScope. name is the display name of the step in the log; timedOut is set when the step
58 // reached its timeout-minutes.
59 scope stepScope
60 name string
61 timedOut timedOutFlag
62 // deadline is when the timeout-minutes of the step, or of the step around it,
63 // ends: zero for none. An inner step of a composite action inherits what is
64 // left of it (ExecutionContext.CreateEmbeddedChild, :498).
65 deadline time.Time
66}
67
68// depth is the number of composite actions around the step.
69func (s *stepRun) depth() int {
70 d := 0
71 for p := s.parent; p != nil; p = p.parent {
72 d++
73 }
74 return d
75}
76
77// procEnv is the environment of the process the step starts.
78func (s *stepRun) procEnv() []string {
79 if s.rawEnvList != nil {
80 return s.rawEnvList
81 }
82 env := s.env
83 if s.actEnv != nil {
84 env = s.actEnv
85 }
86 list := s.j.processEnv(env)
87 if s.serverURL != "" {
88 // processEnv sets the runtime variables of the contexts last: the
89 // replacement comes after them.
90 list = setEnvList(list, "GITHUB_SERVER_URL", s.serverURL)
91 }
92 return list
93}
94
95// setEnvList sets name=value in a NAME=value list, in place of an existing
96// entry or at the end.
97func setEnvList(list []string, name, value string) []string {
98 for i, kv := range list {
99 if k, _, _ := strings.Cut(kv, "="); k == name {
100 list[i] = name + "=" + value
101 return list
102 }
103 }
104 return append(list, name+"="+value)
105}
106
107func (s *stepRun) Out(text string) { s.j.out(s.num, text) }
108func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) }
109func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) }
110func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) }
111func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f }
112func (s *stepRun) SetOutput(name, value string) {
113 s.j.mu.Lock()
114 s.j.state.SetOutput(s.st.ID, name, value)
115 s.j.mu.Unlock()
116}
117func (s *stepRun) SetEnv(name, value string) {
118 s.j.mu.Lock()
119 s.j.state.SetEnv(name, value)
120 s.j.mu.Unlock()
121}
122func (s *stepRun) AddPath(p string) {
123 s.j.mu.Lock()
124 s.j.state.AddPath(p)
125 s.j.mu.Unlock()
126}
127
128// EnvFlag reads a boolean variable of the env context, then of the process
129// (ActionCommandManager.cs:236-246).
130func (s *stepRun) EnvFlag(name string) bool {
131 v := strings.ToLower(envValue(s.j.baseEnv, name))
132 if v != "true" {
133 if e, ok := s.env.Get(name); ok {
134 v = strings.ToLower(e)
135 }
136 }
137 return v == "true"
138}
139
140func firstLine(contents string) string {
141 l := strings.TrimLeft(contents, " \t\r\n")
142 if i := strings.IndexAny(l, "\r\n"); i >= 0 {
143 l = l[:i]
144 }
145 return l
146}
147
148// displayName is the name shown for the step (ActionRunner.GenerateDisplayName,
149// ActionRunner.cs:363): the evaluated name, or "Run " and the first line of
150// the script or the action reference.
151func (j *job) displayName(st *workflow.Step, env *ordered) string {
152 return j.displayNameOf(st, env, false)
153}
154
155// displayNameOf is displayName; embedded is true for a step of a composite
156// action, whose name is evaluated only when the step starts
157// (CompositeActionHandler.cs:314-318), with the contexts of that step (the
158// inputs and steps of the action included), never with those of the job message.
159func (j *job) displayNameOf(st *workflow.Step, env *ordered, embedded bool) string {
160 tok := st.Name
161 prefix := ""
162 switch {
163 case tok != nil:
164 case st.Run != nil:
165 tok, prefix = st.Run, "Run "
166 case st.Uses != nil:
167 // The name is built from the parsed reference (ActionRunner.cs:305-333):
168 // the image of a docker:// step without its scheme, "Name[/Path]@Ref" for
169 // a repository, the path for a local action.
170 if u, err := action.ParseUses(st.Uses.Value); err == nil {
171 if u.Kind == action.UsesDocker {
172 return formatStepName("Run ", u.Image)
173 }
174 return formatStepName("Run ", refString(u))
175 }
176 tok, prefix = st.Uses, "Run "
177 default:
178 return ""
179 }
180 // JobExtension evaluates the display names once, when the job starts, with
181 // the contexts the server sent (github.job is empty there) and without the
182 // step functions; ActionRunner.GenerateDisplayName only does it when every
183 // context and function the name uses is available (CheckHasRequiredContext).
184 // A name that is fully evaluated then stays as it is (recorded:
185 // ctx-github-names, "Run echo \"job=\""); the others are evaluated again,
186 // with the contexts of the step, when the step starts.
187 var name string
188 var err error
189 if !embedded && serverEvaluable(tok) {
190 j.mu.Lock()
191 sc := j.state.ServerContexts()
192 j.mu.Unlock()
193 name, err = template.EvaluateString(context.Background(), tok, template.Env{Contexts: sc})
194 } else {
195 name, err = j.evalString(tok, env)
196 }
197 if err != nil {
198 name = tokenSource(tok)
199 }
200 return formatStepName(prefix, name)
201}
202
203// serverContextNames are the contexts of the job message (ContextData) when
204// display names are first evaluated.
205var serverContextNames = map[string]bool{"github": true, "needs": true, "strategy": true, "matrix": true, "inputs": true, "vars": true}
206
207// wellKnownFuncs are the functions available without the step functions.
208var wellKnownFuncs = map[string]bool{"case": true, "contains": true, "endswith": true, "format": true, "join": true, "startswith": true, "tojson": true, "fromjson": true}
209
210// serverEvaluable is TemplateTokenExtensions.CheckHasRequiredContext for the
211// first evaluation of a display name: every expression of the token only
212// reads contexts of the job message and calls well-known functions.
213func serverEvaluable(tok template.Token) bool {
214 ok := true
215 template.Walk(tok, func(t template.Token) bool {
216 if be, isExpr := t.(*template.BasicExpression); isExpr {
217 ok = ok && nodeServerEvaluable(be.Node())
218 }
219 return ok
220 })
221 return ok
222}
223
224func nodeServerEvaluable(n expr.Node) bool {
225 switch n := n.(type) {
226 case *expr.ContextAccess:
227 return serverContextNames[strings.ToLower(n.Name.Lexeme)]
228 case *expr.Unary:
229 return nodeServerEvaluable(n.Expr)
230 case *expr.Binary:
231 return nodeServerEvaluable(n.Left) && nodeServerEvaluable(n.Right)
232 case *expr.Grouping:
233 return nodeServerEvaluable(n.Group)
234 case *expr.IndexAccess:
235 return nodeServerEvaluable(n.Expr) && nodeServerEvaluable(n.Index)
236 case *expr.Logical:
237 for _, a := range n.Args {
238 if !nodeServerEvaluable(a) {
239 return false
240 }
241 }
242 case *expr.FunctionCall:
243 if !wellKnownFuncs[strings.ToLower(n.Name.Lexeme)] {
244 return false
245 }
246 for _, a := range n.Args {
247 if !nodeServerEvaluable(a) {
248 return false
249 }
250 }
251 }
252 return true
253}
254
255// exceptionClasser is implemented by the errors of package expr that stand for
256// a .NET exception (expr.JSONError).
257type exceptionClasser interface{ ExceptionClass() string }
258
259// templateNotValid is the message of the TemplateValidationException the
260// runner throws when the evaluation of a template fails
261// (TemplateStrings.TemplateNotValidWithErrors): "The template is not valid. "
262// and the errors. With a position the error reads "file (Line: l, Col: c):
263// message" (recorded: expr-runtime-errors, step env and run). A step condition
264// has no position: the error is the exception itself, "Class: message"
265// (withLocation is false).
266func templateNotValid(err error, withLocation bool) string {
267 var te *template.Error
268 if !errors.As(err, &te) {
269 return err.Error()
270 }
271 if withLocation {
272 return "The template is not valid. " + te.Error()
273 }
274 var c exceptionClasser
275 if errors.As(err, &c) {
276 return "The template is not valid. " + c.ExceptionClass() + ": " + te.Raw
277 }
278 return te.Error()
279}
280
281func tokenSource(t template.Token) string {
282 if be, ok := t.(*template.BasicExpression); ok && be.Source != "" {
283 return be.Source
284 }
285 return t.String()
286}
287
288// formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447).
289func formatStepName(prefix, name string) string {
290 if name == "" {
291 return ""
292 }
293 return prefix + firstLine(name)
294}
295
296// statusName is the job.status for a result (TaskResult.ToActionResult).
297func statusName(r stepResult) string {
298 switch r {
299 case resFailed:
300 return "failure"
301 case resCanceled:
302 return "cancelled"
303 }
304 return "success"
305}