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}