v0.5.0 22211fd

exec_step.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"fmt"
7	"path/filepath"
8	"strings"
9	"sync"
10)
11
12// execute is ActionRunner.RunAsync plus ScriptHandler for one run step.
13func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) stepResult {
14	num, st := s.num, s.st
15	if st.Uses != nil {
16		return j.runAction(s, stepCancel)
17	}
18	fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
19	if err := j.containerReady(); err != nil {
20		return fail(err)
21	}
22	failTemplate := func(err error) stepResult {
23		j.errorf(num, "%s", templateNotValid(err, true))
24		return resFailed
25	}
26
27	if err := j.prepareFileCommands(s); err != nil {
28		return fail(err)
29	}
30	// ProcessFiles runs after the handler, even when it failed (a finally block).
31	defer s.fc.Process(s)
32
33	in, err := j.scriptInputs(s)
34	if err != nil {
35		return failTemplate(err)
36	}
37	plan, err := j.printHeader(s, in)
38	if err != nil {
39		return fail(err) // the group stays open, like the original
40	}
41	// What the handler adds goes to the process only, after the header.
42	s.actEnv = j.scriptEnv(s)
43	if c := j.stepContainer(); c != nil {
44		argv, err := j.containerScript(c, plan, in, s)
45		if err != nil {
46			return fail(err)
47		}
48		return j.runProc(s, argv[0], j.state.Layout().WorkRoot, argv, "", stepCancel)
49	}
50	dir, argv, err := j.scriptCommand(plan, in)
51	if err != nil {
52		return fail(err)
53	}
54	return j.startProcess(s, plan, dir, argv, stepCancel)
55}
56
57// scriptIn is what a run step evaluates before it prints its header.
58type scriptIn struct {
59	script, shell, workingDirectory string
60	hasWorkingDirectory             bool
61}
62
63// scriptInputs evaluates the run, shell and working-directory keys of the step
64// (ActionRunner.cs:174-185). The shell and the directory fall back to
65// defaults.run (ScriptHandler.cs:155-180).
66func (j *job) scriptInputs(s *stepRun) (scriptIn, error) {
67	st := s.st
68	var in scriptIn
69	var err error
70	if in.script, err = j.evalString(st.Run, s.env); err != nil {
71		return in, err
72	}
73	if in.shell, err = j.evalString(st.Shell, s.env); err != nil {
74		return in, err
75	}
76	// defaults.run applies to the steps of the job only, not to the ones of a
77	// composite action (ScriptHandler.cs:158-181: ScopeName is empty).
78	top := s.parent == nil
79	if in.shell == "" && top {
80		in.shell = j.runDefaults.shell
81	}
82	if st.WorkingDirectory != nil {
83		in.hasWorkingDirectory = true
84		if in.workingDirectory, err = j.evalString(st.WorkingDirectory, s.env); err != nil {
85			return in, err
86		}
87	}
88	if !in.hasWorkingDirectory && top {
89		in.workingDirectory = j.runDefaults.workingDirectory
90	}
91	return in, nil
92}
93
94// prepareFileCommands creates the files of the step, puts their paths in the
95// github context (FileCommandManager.InitializeFiles) and writes event.json
96// (ExecutionContext.WriteWebhookPayload).
97func (j *job) prepareFileCommands(s *stepRun) error {
98	j.mu.Lock()
99	defer j.mu.Unlock()
100	if s.fc == nil {
101		s.fc = j.fc
102	}
103	files, err := s.fc.Init()
104	if err != nil {
105		return err
106	}
107	for _, c := range fileCommands {
108		j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)])
109	}
110	if s.n == nil || s.n.st.Uses == nil {
111		// ActionRunner.cs:149-165 sets them for every action step, a run step
112		// included (its reference is a script, so they are empty).
113		j.state.SetGitHub("action_repository", "")
114		j.state.SetGitHub("action_ref", "")
115	}
116	if ev := j.state.EventJSON(); ev != "" {
117		if p, err := j.writeEvent(ev); err == nil {
118			j.state.SetGitHub("event_path", p)
119		}
120	}
121	return nil
122}
123
124// printHeader prints the group of the step as ScriptHandler.PrintActionDetails
125// does (ScriptHandler.cs:27-137): the first line of the script, the script in
126// bright cyan, the shell, the env block of the step, and the end of the group.
127func (j *job) printHeader(s *stepRun, in scriptIn) (ShellPlan, error) {
128	num := s.num
129	j.out(num, "##[group]Run "+firstLine(in.script))
130	for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(in.script, "\r\n", "\n"), "\n"), "\n") {
131		j.write(num, "\x1b[36;1m"+l+"\x1b[0m")
132	}
133	j.mu.Lock()
134	lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()}
135	j.mu.Unlock()
136	var plan ShellPlan
137	var err error
138	if j.stepContainer() != nil {
139		plan, err = PlanShellInContainer(in.shell)
140	} else {
141		plan, err = PlanShell(in.shell, lookup)
142	}
143	if err != nil {
144		return plan, err
145	}
146	j.out(num, "shell: "+plan.ShellLine())
147	if s.env.Len() > 0 {
148		j.write(num, "env:")
149		for _, k := range s.env.Keys() {
150			v, _ := s.env.Get(k)
151			j.out(num, fmt.Sprintf("  %s: %s", k, v))
152		}
153	}
154	j.write(num, "##[endgroup]")
155	return plan, nil
156}
157
158// scriptCommand writes the script file and returns the working directory and
159// the command line (ScriptHandler.RunAsync, :140-285).
160func (j *job) scriptCommand(plan ShellPlan, in scriptIn) (dir string, argv []string, err error) {
161	j.mu.Lock()
162	workspace := j.state.GitHub("workspace")
163	j.mu.Unlock()
164	sc, err := WriteScript(j.tempRoot, plan, in.script)
165	if err != nil {
166		return "", nil, err
167	}
168	return ResolveWorkingDirectory(workspace, in.workingDirectory), sc.Argv, nil
169}
170
171// processEnv builds the environment of the process: the inherited one, the
172// step's environment, PATH with GITHUB_PATH in front, then the runtime variables
173// of the contexts, GITHUB_ACTIONS, CI and the tracking id.
174func (j *job) processEnv(stepEnv *ordered) []string {
175	j.mu.Lock()
176	runtime := j.state.RuntimeEnv()
177	prepend := j.state.Prepend()
178	j.mu.Unlock()
179	penv := newOrdered()
180	for _, kv := range j.baseEnv {
181		if k, v, ok := strings.Cut(kv, "="); ok {
182			penv.Set(k, v)
183		}
184	}
185	for _, k := range stepEnv.Keys() {
186		v, _ := stepEnv.Get(k)
187		penv.Set(k, v)
188	}
189	if len(prepend) > 0 {
190		orig, ok := stepEnv.Get("PATH")
191		if !ok {
192			orig = j.lookupPath
193		}
194		penv.Set("PATH", PrependedPath(prepend, orig))
195	}
196	for _, k := range runtime.Keys() {
197		v, _ := runtime.Get(k)
198		penv.Set(k, v)
199	}
200	penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283
201	if _, ok := penv.Get("CI"); !ok {
202		penv.Set("CI", "true")
203	}
204	penv.Set(ProcessTrackingID, j.trackID)
205	var list []string
206	for _, k := range penv.Keys() {
207		v, _ := penv.Get(k)
208		list = append(list, k+"="+v)
209	}
210	return list
211}
212
213// startProcess runs the script and turns how it ended into the result of the
214// step.
215func (j *job) startProcess(s *stepRun, plan ShellPlan, dir string, argv []string, stepCancel <-chan struct{}) stepResult {
216	return j.runProc(s, plan.CommandPath, dir, argv, "", stepCancel)
217}
218
219// runProc starts a process (exitDebug, when set, is the format of the debug line that
220// replaces the error about the exit code) for the step and streams its output through the
221// commands of the step. A script reports its exit code (ScriptHandler.cs:
222// 267-275); the handlers of actions only mark the step failed (NodeScriptAction
223// Handler.cs:178-184), a failing action says why itself.
224func (j *job) runProc(s *stepRun, shown, dir string, argv []string, exitDebug string, stepCancel <-chan struct{}) stepResult {
225	num := s.num
226	om := newOutputManager(s, stepCancel)
227	defer om.dispose()
228	// The process is stopped when the step is cancelled or when the output
229	// handler throws (an invalid stop-commands token).
230	procCancel := make(chan struct{})
231	fatal := make(chan struct{})
232	var fatalOnce sync.Once
233	s.cmds.onFatal = func() { fatalOnce.Do(func() { close(fatal) }) }
234	defer func() { s.cmds.onFatal = nil }()
235	watchDone := make(chan struct{})
236	defer func() { close(watchDone) }()
237	go func() {
238		select {
239		case <-stepCancel:
240		case <-fatal:
241		case <-watchDone:
242			return
243		}
244		close(procCancel)
245	}()
246	res, err := runProcess(procSpec{
247		Argv: argv, Dir: dir, Env: s.procEnv(), Credential: j.cfg.Credential,
248		OnLine: om.onData,
249		Cancel: procCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter,
250		WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal,
251	})
252	if err != nil {
253		j.errorf(num, "%s", startError(shown, dir, err))
254		return resFailed
255	}
256	if err := s.cmds.fatal; err != nil {
257		// An exception of the output handler (an invalid stop-commands token)
258		// ends ExecuteAsync; StepsRunner logs its message and fails the step.
259		j.errorf(num, "%s", err.Error())
260		return resFailed
261	}
262	if res.Cancelled {
263		// The process was stopped on cancellation: ExecuteAsync throws
264		// (ProcessInvoker.cs:369) and StepsRunner logs the exception
265		// (StepsRunner.cs:333-337).
266		s.logCanceled()
267		return resCanceled
268	}
269	if exitDebug != "" {
270		j.debug(num, fmt.Sprintf(exitDebug, res.ExitCode))
271	}
272	if res.ExitCode != 0 {
273		if exitDebug == "" {
274			j.errorf(num, "Process completed with exit code %d.", res.ExitCode)
275		}
276		return resFailed
277	}
278	return resSucceeded
279}
280
281// writeEvent writes the webhook payload to RUNNER_TEMP/_github_workflow/event.json
282// (ExecutionContext.WriteWebhookPayload) and returns its path. The directory
283// is the user's: the file is replaced, never followed (see FileCommands).
284func (j *job) writeEvent(payload string) (string, error) {
285	const dir, name = "_github_workflow", "event.json"
286	if err := j.tempRoot.MkdirAll(dir, 0o755); err != nil {
287		return "", err
288	}
289	if c := j.cfg.Credential; c != nil {
290		_ = j.tempRoot.Lchown(dir, int(c.Uid), int(c.Gid))
291	}
292	sub, err := j.tempRoot.OpenRoot(dir)
293	if err != nil {
294		return "", err
295	}
296	defer func() { _ = sub.Close() }()
297	var owner *[2]int
298	if c := j.cfg.Credential; c != nil {
299		owner = &[2]int{int(c.Uid), int(c.Gid)}
300	}
301	if err := createExcl(sub, name, []byte(payload), 0o644, owner); err != nil {
302		return "", err
303	}
304	return filepath.Join(j.state.Layout().Temp, dir, name), nil
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