exec.go

 1package engine
 2
 3import (
 4	"context"
 5	"errors"
 6	"io"
 7	"os/exec"
 8	"syscall"
 9	"time"
10)
11
12// Spec is one action execution.
13type Spec struct {
14	Root    string // main checkout: where the devenv shell is activated
15	Dir     string // absolute working directory for the command
16	Command string
17	Env     []string
18	Stdout  io.Writer
19	Stderr  io.Writer
20}
21
22// Executor runs an action's command and returns its exit code. An error
23// means the command could not be run at all (or was cancelled).
24type Executor interface {
25	Exec(ctx context.Context, s Spec) (int, error)
26}
27
28// script runs the command in its working directory under bash's strict mode.
29// The directory comes in through CI_ACTION_DIR so it needs no quoting.
30func script(command string) string {
31	return "cd -- \"$CI_ACTION_DIR\"\n" + command
32}
33
34// Devenv runs actions in the main checkout's devenv shell: `devenv shell
35// bash -euo pipefail -c <script>` with the main checkout as working
36// directory, so the workflow's repo pins the toolchain (and the engine) even
37// when the action's dir is another repo.
38type Devenv struct {
39	// Binary is the devenv executable; default "devenv".
40	Binary string
41}
42
43func (d Devenv) Exec(ctx context.Context, s Spec) (int, error) {
44	bin := d.Binary
45	if bin == "" {
46		bin = "devenv"
47	}
48	return run(ctx, s, s.Root, bin, "shell", "bash", "-euo", "pipefail", "-c", script(s.Command))
49}
50
51// Bash runs actions with plain bash, without devenv: for tests, and for
52// debugging a recipe outside its shell (ci run --shell bash).
53type Bash struct{}
54
55func (Bash) Exec(ctx context.Context, s Spec) (int, error) {
56	return run(ctx, s, s.Dir, "bash", "-euo", "pipefail", "-c", script(s.Command))
57}
58
59// run starts the command in its own process group, so cancelling kills the
60// whole tree (devenv, bash and whatever the recipe started).
61func run(ctx context.Context, s Spec, dir, name string, args ...string) (int, error) {
62	cmd := exec.CommandContext(ctx, name, args...)
63	cmd.Dir = dir
64	cmd.Env = append(s.Env, "CI_ACTION_DIR="+s.Dir)
65	cmd.Stdout = s.Stdout
66	cmd.Stderr = s.Stderr
67	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
68	cmd.Cancel = func() error {
69		return syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM)
70	}
71	cmd.WaitDelay = 10 * time.Second
72	err := cmd.Run()
73	if ctx.Err() != nil {
74		return -1, ctx.Err()
75	}
76	var ee *exec.ExitError
77	if errors.As(err, &ee) {
78		if ee.ExitCode() >= 0 {
79			return ee.ExitCode(), nil
80		}
81		return -1, err
82	}
83	if err != nil {
84		return -1, err
85	}
86	return 0, nil
87}