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 // scope is what the step shares with the step that contains it, see
54 // stepScope. name is the display name of the step in the log; timedOut is set when the step
55 // reached its timeout-minutes.
56 scope stepScope
57 name string
58 timedOut timedOutFlag
59 // deadline is when the timeout-minutes of the step, or of the step around it,
60 // ends: zero for none. An inner step of a composite action inherits what is
61 // left of it (ExecutionContext.CreateEmbeddedChild, :498).
62 deadline time.Time
63}
64
65// depth is the number of composite actions around the step.
66func (s *stepRun) depth() int {
67 d := 0
68 for p := s.parent; p != nil; p = p.parent {
69 d++
70 }
71 return d
72}
73
74// procEnv is the environment of the process the step starts.
75func (s *stepRun) procEnv() []string {
76 if s.rawEnvList != nil {
77 return s.rawEnvList
78 }
79 if s.actEnv != nil {
80 return s.j.processEnv(s.actEnv)
81 }
82 return s.j.processEnv(s.env)
83}
84
85func (s *stepRun) Out(text string) { s.j.out(s.num, text) }
86func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) }
87func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) }
88func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) }
89func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f }
90func (s *stepRun) SetOutput(name, value string) {
91 s.j.mu.Lock()
92 s.j.state.SetOutput(s.st.ID, name, value)
93 s.j.mu.Unlock()
94}
95func (s *stepRun) SetEnv(name, value string) {
96 s.j.mu.Lock()
97 s.j.state.SetEnv(name, value)
98 s.j.mu.Unlock()
99}
100func (s *stepRun) AddPath(p string) {
101 s.j.mu.Lock()
102 s.j.state.AddPath(p)
103 s.j.mu.Unlock()
104}
105
106// EnvFlag reads a boolean variable of the env context, then of the process
107// (ActionCommandManager.cs:236-246).
108func (s *stepRun) EnvFlag(name string) bool {
109 v := strings.ToLower(envValue(s.j.baseEnv, name))
110 if v != "true" {
111 if e, ok := s.env.Get(name); ok {
112 v = strings.ToLower(e)
113 }
114 }
115 return v == "true"
116}
117
118func firstLine(contents string) string {
119 l := strings.TrimLeft(contents, " \t\r\n")
120 if i := strings.IndexAny(l, "\r\n"); i >= 0 {
121 l = l[:i]
122 }
123 return l
124}
125
126// displayName is the name shown for the step (ActionRunner.GenerateDisplayName,
127// ActionRunner.cs:363): the evaluated name, or "Run " and the first line of
128// the script or the action reference.
129func (j *job) displayName(st *workflow.Step, env *ordered) string {
130 return j.displayNameOf(st, env, false)
131}
132
133// displayNameOf is displayName; embedded is true for a step of a composite
134// action, whose name is evaluated only when the step starts
135// (CompositeActionHandler.cs:314-318), with the contexts of that step (the
136// inputs and steps of the action included), never with those of the job message.
137func (j *job) displayNameOf(st *workflow.Step, env *ordered, embedded bool) string {
138 tok := st.Name
139 prefix := ""
140 switch {
141 case tok != nil:
142 case st.Run != nil:
143 tok, prefix = st.Run, "Run "
144 case st.Uses != nil:
145 // The name is built from the parsed reference (ActionRunner.cs:305-333):
146 // the image of a docker:// step without its scheme, "Name[/Path]@Ref" for
147 // a repository, the path for a local action.
148 if u, err := action.ParseUses(st.Uses.Value); err == nil {
149 if u.Kind == action.UsesDocker {
150 return formatStepName("Run ", u.Image)
151 }
152 return formatStepName("Run ", refString(u))
153 }
154 tok, prefix = st.Uses, "Run "
155 default:
156 return ""
157 }
158 // JobExtension evaluates the display names once, when the job starts, with
159 // the contexts the server sent (github.job is empty there) and without the
160 // step functions; ActionRunner.GenerateDisplayName only does it when every
161 // context and function the name uses is available (CheckHasRequiredContext).
162 // A name that is fully evaluated then stays as it is (recorded:
163 // ctx-github-names, "Run echo \"job=\""); the others are evaluated again,
164 // with the contexts of the step, when the step starts.
165 var name string
166 var err error
167 if !embedded && serverEvaluable(tok) {
168 j.mu.Lock()
169 sc := j.state.ServerContexts()
170 j.mu.Unlock()
171 name, err = template.EvaluateString(context.Background(), tok, template.Env{Contexts: sc})
172 } else {
173 name, err = j.evalString(tok, env)
174 }
175 if err != nil {
176 name = tokenSource(tok)
177 }
178 return formatStepName(prefix, name)
179}
180
181// serverContextNames are the contexts of the job message (ContextData) when
182// display names are first evaluated.
183var serverContextNames = map[string]bool{"github": true, "needs": true, "strategy": true, "matrix": true, "inputs": true, "vars": true}
184
185// wellKnownFuncs are the functions available without the step functions.
186var wellKnownFuncs = map[string]bool{"case": true, "contains": true, "endswith": true, "format": true, "join": true, "startswith": true, "tojson": true, "fromjson": true}
187
188// serverEvaluable is TemplateTokenExtensions.CheckHasRequiredContext for the
189// first evaluation of a display name: every expression of the token only
190// reads contexts of the job message and calls well-known functions.
191func serverEvaluable(tok template.Token) bool {
192 ok := true
193 template.Walk(tok, func(t template.Token) bool {
194 if be, isExpr := t.(*template.BasicExpression); isExpr {
195 ok = ok && nodeServerEvaluable(be.Node())
196 }
197 return ok
198 })
199 return ok
200}
201
202func nodeServerEvaluable(n expr.Node) bool {
203 switch n := n.(type) {
204 case *expr.ContextAccess:
205 return serverContextNames[strings.ToLower(n.Name.Lexeme)]
206 case *expr.Unary:
207 return nodeServerEvaluable(n.Expr)
208 case *expr.Binary:
209 return nodeServerEvaluable(n.Left) && nodeServerEvaluable(n.Right)
210 case *expr.Grouping:
211 return nodeServerEvaluable(n.Group)
212 case *expr.IndexAccess:
213 return nodeServerEvaluable(n.Expr) && nodeServerEvaluable(n.Index)
214 case *expr.Logical:
215 for _, a := range n.Args {
216 if !nodeServerEvaluable(a) {
217 return false
218 }
219 }
220 case *expr.FunctionCall:
221 if !wellKnownFuncs[strings.ToLower(n.Name.Lexeme)] {
222 return false
223 }
224 for _, a := range n.Args {
225 if !nodeServerEvaluable(a) {
226 return false
227 }
228 }
229 }
230 return true
231}
232
233// exceptionClasser is implemented by the errors of package expr that stand for
234// a .NET exception (expr.JSONError).
235type exceptionClasser interface{ ExceptionClass() string }
236
237// templateNotValid is the message of the TemplateValidationException the
238// runner throws when the evaluation of a template fails
239// (TemplateStrings.TemplateNotValidWithErrors): "The template is not valid. "
240// and the errors. With a position the error reads "file (Line: l, Col: c):
241// message" (recorded: expr-runtime-errors, step env and run). A step condition
242// has no position: the error is the exception itself, "Class: message"
243// (withLocation is false).
244func templateNotValid(err error, withLocation bool) string {
245 var te *template.Error
246 if !errors.As(err, &te) {
247 return err.Error()
248 }
249 if withLocation {
250 return "The template is not valid. " + te.Error()
251 }
252 var c exceptionClasser
253 if errors.As(err, &c) {
254 return "The template is not valid. " + c.ExceptionClass() + ": " + te.Raw
255 }
256 return te.Error()
257}
258
259func tokenSource(t template.Token) string {
260 if be, ok := t.(*template.BasicExpression); ok && be.Source != "" {
261 return be.Source
262 }
263 return t.String()
264}
265
266// formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447).
267func formatStepName(prefix, name string) string {
268 if name == "" {
269 return ""
270 }
271 return prefix + firstLine(name)
272}
273
274// statusName is the job.status for a result (TaskResult.ToActionResult).
275func statusName(r stepResult) string {
276 switch r {
277 case resFailed:
278 return "failure"
279 case resCanceled:
280 return "cancelled"
281 }
282 return "success"
283}