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	// 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}

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