v0.1.0 cbb8ad6

exec_step.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"fmt"
7	"path/filepath"
8	"strings"
9
10	"bnema.dev/ermite-actions/workflow"
11)
12
13// execute is ActionRunner.RunAsync plus ScriptHandler for one run step.
14func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) stepResult {
15	num, st := s.num, s.st
16	if msg := notSupported(st); msg != "" {
17		if st.Uses != nil {
18			j.out(num, "##[group]Run "+st.Uses.Value)
19			j.out(num, "##[endgroup]")
20		}
21		j.errorf(num, "%s", msg)
22		return resFailed
23	}
24	fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
25	failTemplate := func(err error) stepResult {
26		j.errorf(num, "%s", templateNotValid(err, true))
27		return resFailed
28	}
29
30	if err := j.prepareFileCommands(st); err != nil {
31		return fail(err)
32	}
33	// ProcessFiles runs after the handler, even when it failed (a finally block).
34	defer j.fc.Process(s)
35
36	in, err := j.scriptInputs(s)
37	if err != nil {
38		return failTemplate(err)
39	}
40	plan, err := j.printHeader(s, in)
41	if err != nil {
42		return fail(err) // the group stays open, like the original
43	}
44	dir, argv, err := j.scriptCommand(plan, in)
45	if err != nil {
46		return fail(err)
47	}
48	return j.startProcess(s, plan, dir, argv, stepCancel)
49}
50
51// scriptIn is what a run step evaluates before it prints its header.
52type scriptIn struct {
53	script, shell, workingDirectory string
54	hasWorkingDirectory             bool
55}
56
57// scriptInputs evaluates the run, shell and working-directory keys of the step
58// (ActionRunner.cs:174-185). The shell and the directory fall back to
59// defaults.run (ScriptHandler.cs:155-180).
60func (j *job) scriptInputs(s *stepRun) (scriptIn, error) {
61	st := s.st
62	var in scriptIn
63	var err error
64	if in.script, err = j.evalString(st.Run, s.env); err != nil {
65		return in, err
66	}
67	if in.shell, err = j.evalString(st.Shell, s.env); err != nil {
68		return in, err
69	}
70	if in.shell == "" {
71		in.shell = j.runDefaults.shell
72	}
73	if st.WorkingDirectory != nil {
74		in.hasWorkingDirectory = true
75		if in.workingDirectory, err = j.evalString(st.WorkingDirectory, s.env); err != nil {
76			return in, err
77		}
78	}
79	if !in.hasWorkingDirectory {
80		in.workingDirectory = j.runDefaults.workingDirectory
81	}
82	return in, nil
83}
84
85// prepareFileCommands creates the files of the step, puts their paths in the
86// github context (FileCommandManager.InitializeFiles) and writes event.json
87// (ExecutionContext.WriteWebhookPayload).
88func (j *job) prepareFileCommands(st *workflow.Step) error {
89	j.mu.Lock()
90	defer j.mu.Unlock()
91	files, err := j.fc.Init()
92	if err != nil {
93		return err
94	}
95	for _, c := range fileCommands {
96		j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)])
97	}
98	j.state.SetGitHub("action_repository", "")
99	j.state.SetGitHub("action_ref", "")
100	if ev := j.state.EventJSON(); ev != "" {
101		if p, err := j.writeEvent(ev); err == nil {
102			j.state.SetGitHub("event_path", p)
103		}
104	}
105	return nil
106}
107
108// printHeader prints the group of the step as ScriptHandler.PrintActionDetails
109// does (ScriptHandler.cs:27-137): the first line of the script, the script in
110// bright cyan, the shell, the env block of the step, and the end of the group.
111func (j *job) printHeader(s *stepRun, in scriptIn) (ShellPlan, error) {
112	num := s.num
113	j.out(num, "##[group]Run "+firstLine(in.script))
114	for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(in.script, "\r\n", "\n"), "\n"), "\n") {
115		j.write(num, "\x1b[36;1m"+l+"\x1b[0m")
116	}
117	j.mu.Lock()
118	lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()}
119	j.mu.Unlock()
120	plan, err := PlanShell(in.shell, lookup)
121	if err != nil {
122		return plan, err
123	}
124	j.out(num, "shell: "+plan.ShellLine())
125	if s.env.Len() > 0 {
126		j.write(num, "env:")
127		for _, k := range s.env.Keys() {
128			v, _ := s.env.Get(k)
129			j.out(num, fmt.Sprintf("  %s: %s", k, v))
130		}
131	}
132	j.write(num, "##[endgroup]")
133	return plan, nil
134}
135
136// scriptCommand writes the script file and returns the working directory and
137// the command line (ScriptHandler.RunAsync, :140-285).
138func (j *job) scriptCommand(plan ShellPlan, in scriptIn) (dir string, argv []string, err error) {
139	j.mu.Lock()
140	workspace := j.state.GitHub("workspace")
141	j.mu.Unlock()
142	sc, err := WriteScript(j.tempRoot, plan, in.script)
143	if err != nil {
144		return "", nil, err
145	}
146	return ResolveWorkingDirectory(workspace, in.workingDirectory), sc.Argv, nil
147}
148
149// processEnv builds the environment of the process: the inherited one, the
150// step's environment, PATH with GITHUB_PATH in front, then the runtime variables
151// of the contexts, GITHUB_ACTIONS, CI and the tracking id.
152func (j *job) processEnv(stepEnv *ordered) []string {
153	j.mu.Lock()
154	runtime := j.state.RuntimeEnv()
155	prepend := j.state.Prepend()
156	j.mu.Unlock()
157	penv := newOrdered()
158	for _, kv := range j.baseEnv {
159		if k, v, ok := strings.Cut(kv, "="); ok {
160			penv.Set(k, v)
161		}
162	}
163	for _, k := range stepEnv.Keys() {
164		v, _ := stepEnv.Get(k)
165		penv.Set(k, v)
166	}
167	if len(prepend) > 0 {
168		orig, ok := stepEnv.Get("PATH")
169		if !ok {
170			orig = j.lookupPath
171		}
172		penv.Set("PATH", PrependedPath(prepend, orig))
173	}
174	for _, k := range runtime.Keys() {
175		v, _ := runtime.Get(k)
176		penv.Set(k, v)
177	}
178	penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283
179	if _, ok := penv.Get("CI"); !ok {
180		penv.Set("CI", "true")
181	}
182	penv.Set(ProcessTrackingID, j.trackID)
183	var list []string
184	for _, k := range penv.Keys() {
185		v, _ := penv.Get(k)
186		list = append(list, k+"="+v)
187	}
188	return list
189}
190
191// startProcess runs the script and turns how it ended into the result of the
192// step.
193func (j *job) startProcess(s *stepRun, plan ShellPlan, dir string, argv []string, stepCancel <-chan struct{}) stepResult {
194	num := s.num
195	res, err := runProcess(procSpec{
196		Argv: argv, Dir: dir, Env: j.processEnv(s.env), Credential: j.cfg.Credential,
197		OnLine: func(line string) {
198			if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") {
199				line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`)
200			}
201			if (strings.Contains(line, "##[") || strings.Contains(line, "::")) && s.cmds.TryProcess(line) {
202				return
203			}
204			j.write(num, line)
205		},
206		Cancel: stepCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter,
207		WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal,
208	})
209	if err != nil {
210		j.errorf(num, "%s", startError(plan.CommandPath, dir, err))
211		return resFailed
212	}
213	if res.Cancelled {
214		// The process was stopped on cancellation: ExecuteAsync throws
215		// (ProcessInvoker.cs:369) and StepsRunner logs the exception
216		// (StepsRunner.cs:333-337).
217		j.errorf(num, "The operation was canceled.")
218		return resCanceled
219	}
220	if res.ExitCode != 0 {
221		j.errorf(num, "Process completed with exit code %d.", res.ExitCode)
222		return resFailed
223	}
224	return resSucceeded
225}
226
227// writeEvent writes the webhook payload to RUNNER_TEMP/_github_workflow/event.json
228// (ExecutionContext.WriteWebhookPayload) and returns its path. The directory
229// is the user's: the file is replaced, never followed (see FileCommands).
230func (j *job) writeEvent(payload string) (string, error) {
231	const dir, name = "_github_workflow", "event.json"
232	if err := j.tempRoot.MkdirAll(dir, 0o755); err != nil {
233		return "", err
234	}
235	if c := j.cfg.Credential; c != nil {
236		_ = j.tempRoot.Lchown(dir, int(c.Uid), int(c.Gid))
237	}
238	sub, err := j.tempRoot.OpenRoot(dir)
239	if err != nil {
240		return "", err
241	}
242	defer func() { _ = sub.Close() }()
243	var owner *[2]int
244	if c := j.cfg.Credential; c != nil {
245		owner = &[2]int{int(c.Uid), int(c.Gid)}
246	}
247	if err := createExcl(sub, name, []byte(payload), 0o644, owner); err != nil {
248		return "", err
249	}
250	return filepath.Join(j.state.Layout().Temp, dir, name), nil
251}

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