v0.3.0 8f98fd3

step.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close