refactor(executor,guest): split execute into prepareFileCommands, scriptInputs, printHeader, scriptCommand, processEnv and startProcess; serve into waitForRun and runStub (G2)
- commit
- c5b1e6dbc9f7a86bb4a092b3079413338fd80d32
- author
- brice · · 2026-10-12 19:43 UTC
- parent
- 610fdc0
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +147 -86
M internal/executor/exec_step.go +106 -61
| @@ -3,14 +3,15 @@ | |||
| 3 | 3 | package executor | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | - | "errors" | |
| 7 | 6 | "fmt" | |
| 8 | 7 | "path/filepath" | |
| 9 | 8 | "strings" | |
| 9 | + | ||
| 10 | + | "bnema.dev/ermite-actions/workflow" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | |
| 13 | - | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult) { | |
| 14 | + | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) stepResult { | |
| 14 | 15 | num, st := s.num, s.st | |
| 15 | 16 | if msg := notSupported(st); msg != "" { | |
| 16 | 17 | if st.Uses != nil { | |
| @@ -22,66 +23,100 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||
| 22 | 23 | } | |
| 23 | 24 | fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed } | |
| 24 | 25 | ||
| 25 | - | // File commands for this step: the github context carries the paths | |
| 26 | - | // (FileCommandManager.InitializeFiles), event.json is written | |
| 27 | - | // (ExecutionContext.WriteWebhookPayload). | |
| 28 | - | j.mu.Lock() | |
| 29 | - | files, ferr := j.fc.Init() | |
| 30 | - | if ferr == nil { | |
| 31 | - | for _, c := range fileCommands { | |
| 32 | - | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |
| 33 | - | } | |
| 34 | - | j.state.SetGitHub("action", st.ID) | |
| 35 | - | j.state.SetGitHub("action_repository", "") | |
| 36 | - | j.state.SetGitHub("action_ref", "") | |
| 37 | - | if ev := j.state.EventJSON(); ev != "" { | |
| 38 | - | if p, err := j.writeEvent(ev); err == nil { | |
| 39 | - | j.state.SetGitHub("event_path", p) | |
| 40 | - | } | |
| 41 | - | } | |
| 42 | - | } | |
| 43 | - | j.mu.Unlock() | |
| 44 | - | if ferr != nil { | |
| 45 | - | return fail(ferr) | |
| 26 | + | if err := j.prepareFileCommands(st); err != nil { | |
| 27 | + | return fail(err) | |
| 46 | 28 | } | |
| 47 | - | defer func() { | |
| 48 | - | // ProcessFiles runs after the handler, even when it failed (finally). | |
| 49 | - | j.fc.Process(s) | |
| 50 | - | }() | |
| 29 | + | // ProcessFiles runs after the handler, even when it failed (a finally block). | |
| 30 | + | defer j.fc.Process(s) | |
| 51 | 31 | ||
| 52 | - | // Inputs (ActionRunner.cs:174-185). | |
| 53 | - | script, err := j.evalString(st.Run, s.env) | |
| 32 | + | in, err := j.scriptInputs(s) | |
| 54 | 33 | if err != nil { | |
| 55 | 34 | return fail(err) | |
| 56 | 35 | } | |
| 57 | - | shellInput, err := j.evalString(st.Shell, s.env) | |
| 36 | + | plan, err := j.printHeader(s, in) | |
| 37 | + | if err != nil { | |
| 38 | + | return fail(err) // the group stays open, like the original | |
| 39 | + | } | |
| 40 | + | dir, argv, err := j.scriptCommand(plan, in) | |
| 58 | 41 | if err != nil { | |
| 59 | 42 | return fail(err) | |
| 60 | 43 | } | |
| 61 | - | var wdInput string | |
| 62 | - | var hasWD bool | |
| 44 | + | return j.startProcess(s, plan, dir, argv, stepCancel) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // scriptIn is what a run step evaluates before it prints its header. | |
| 48 | + | type scriptIn struct { | |
| 49 | + | script, shell, workingDirectory string | |
| 50 | + | hasWorkingDirectory bool | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // scriptInputs evaluates the run, shell and working-directory keys of the step | |
| 54 | + | // (ActionRunner.cs:174-185). The shell and the directory fall back to | |
| 55 | + | // defaults.run (ScriptHandler.cs:155-180). | |
| 56 | + | func (j *job) scriptInputs(s *stepRun) (scriptIn, error) { | |
| 57 | + | st := s.st | |
| 58 | + | var in scriptIn | |
| 59 | + | var err error | |
| 60 | + | if in.script, err = j.evalString(st.Run, s.env); err != nil { | |
| 61 | + | return in, err | |
| 62 | + | } | |
| 63 | + | if in.shell, err = j.evalString(st.Shell, s.env); err != nil { | |
| 64 | + | return in, err | |
| 65 | + | } | |
| 66 | + | if in.shell == "" { | |
| 67 | + | in.shell = j.runDefaults.shell | |
| 68 | + | } | |
| 63 | 69 | if st.WorkingDirectory != nil { | |
| 64 | - | hasWD = true | |
| 65 | - | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |
| 66 | - | return fail(err) | |
| 70 | + | in.hasWorkingDirectory = true | |
| 71 | + | if in.workingDirectory, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |
| 72 | + | return in, err | |
| 67 | 73 | } | |
| 68 | 74 | } | |
| 75 | + | if !in.hasWorkingDirectory { | |
| 76 | + | in.workingDirectory = j.runDefaults.workingDirectory | |
| 77 | + | } | |
| 78 | + | return in, nil | |
| 79 | + | } | |
| 69 | 80 | ||
| 70 | - | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | |
| 71 | - | j.out(num, "##[group]Run "+firstLine(script)) | |
| 72 | - | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | |
| 73 | - | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | |
| 81 | + | // prepareFileCommands creates the files of the step, puts their paths in the | |
| 82 | + | // github context (FileCommandManager.InitializeFiles) and writes event.json | |
| 83 | + | // (ExecutionContext.WriteWebhookPayload). | |
| 84 | + | func (j *job) prepareFileCommands(st *workflow.Step) error { | |
| 85 | + | j.mu.Lock() | |
| 86 | + | defer j.mu.Unlock() | |
| 87 | + | files, err := j.fc.Init() | |
| 88 | + | if err != nil { | |
| 89 | + | return err | |
| 74 | 90 | } | |
| 75 | - | shell := shellInput | |
| 76 | - | if shell == "" { | |
| 77 | - | shell = j.runDefaults.shell | |
| 91 | + | for _, c := range fileCommands { | |
| 92 | + | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |
| 93 | + | } | |
| 94 | + | j.state.SetGitHub("action", st.ID) | |
| 95 | + | j.state.SetGitHub("action_repository", "") | |
| 96 | + | j.state.SetGitHub("action_ref", "") | |
| 97 | + | if ev := j.state.EventJSON(); ev != "" { | |
| 98 | + | if p, err := j.writeEvent(ev); err == nil { | |
| 99 | + | j.state.SetGitHub("event_path", p) | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | return nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // printHeader prints the group of the step as ScriptHandler.PrintActionDetails | |
| 106 | + | // does (ScriptHandler.cs:27-137): the first line of the script, the script in | |
| 107 | + | // bright cyan, the shell, the env block of the step, and the end of the group. | |
| 108 | + | func (j *job) printHeader(s *stepRun, in scriptIn) (ShellPlan, error) { | |
| 109 | + | num := s.num | |
| 110 | + | j.out(num, "##[group]Run "+firstLine(in.script)) | |
| 111 | + | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(in.script, "\r\n", "\n"), "\n"), "\n") { | |
| 112 | + | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") | |
| 78 | 113 | } | |
| 79 | 114 | j.mu.Lock() | |
| 80 | 115 | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | |
| 81 | 116 | j.mu.Unlock() | |
| 82 | - | plan, err := PlanShell(shell, lookup) | |
| 117 | + | plan, err := PlanShell(in.shell, lookup) | |
| 83 | 118 | if err != nil { | |
| 84 | - | return fail(err) // the group stays open, like the original | |
| 119 | + | return plan, err | |
| 85 | 120 | } | |
| 86 | 121 | j.out(num, "shell: "+plan.ShellLine()) | |
| 87 | 122 | if s.env.Len() > 0 { | |
| @@ -92,24 +127,27 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||
| 92 | 127 | } | |
| 93 | 128 | } | |
| 94 | 129 | j.write(num, "##[endgroup]") | |
| 130 | + | return plan, nil | |
| 131 | + | } | |
| 95 | 132 | ||
| 96 | - | // Run (ScriptHandler.RunAsync, :140-358). | |
| 97 | - | wd := wdInput | |
| 98 | - | if !hasWD { | |
| 99 | - | wd = j.runDefaults.workingDirectory | |
| 100 | - | } | |
| 133 | + | // scriptCommand writes the script file and returns the working directory and | |
| 134 | + | // the command line (ScriptHandler.RunAsync, :140-285). | |
| 135 | + | func (j *job) scriptCommand(plan ShellPlan, in scriptIn) (dir string, argv []string, err error) { | |
| 101 | 136 | j.mu.Lock() | |
| 102 | 137 | workspace := j.state.GitHub("workspace") | |
| 103 | 138 | temp := j.state.Layout().Temp | |
| 104 | 139 | j.mu.Unlock() | |
| 105 | - | dir := ResolveWorkingDirectory(workspace, wd) | |
| 106 | - | sc, err := WriteScript(temp, plan, script) | |
| 140 | + | sc, err := WriteScript(temp, plan, in.script) | |
| 107 | 141 | if err != nil { | |
| 108 | - | return fail(err) | |
| 142 | + | return "", nil, err | |
| 109 | 143 | } | |
| 144 | + | return ResolveWorkingDirectory(workspace, in.workingDirectory), sc.Argv, nil | |
| 145 | + | } | |
| 110 | 146 | ||
| 111 | - | // Process environment: the inherited one, the step's environment, PATH with | |
| 112 | - | // GITHUB_PATH in front, then the runtime variables of the contexts. | |
| 147 | + | // processEnv builds the environment of the process: the inherited one, the | |
| 148 | + | // step's environment, PATH with GITHUB_PATH in front, then the runtime variables | |
| 149 | + | // of the contexts, GITHUB_ACTIONS, CI and the tracking id. | |
| 150 | + | func (j *job) processEnv(stepEnv *ordered) []string { | |
| 113 | 151 | j.mu.Lock() | |
| 114 | 152 | runtime := j.state.RuntimeEnv() | |
| 115 | 153 | prepend := j.state.Prepend() | |
| @@ -120,12 +158,12 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||
| 120 | 158 | penv.Set(k, v) | |
| 121 | 159 | } | |
| 122 | 160 | } | |
| 123 | - | for _, k := range s.env.Keys() { | |
| 124 | - | v, _ := s.env.Get(k) | |
| 161 | + | for _, k := range stepEnv.Keys() { | |
| 162 | + | v, _ := stepEnv.Get(k) | |
| 125 | 163 | penv.Set(k, v) | |
| 126 | 164 | } | |
| 127 | 165 | if len(prepend) > 0 { | |
| 128 | - | orig, ok := s.env.Get("PATH") | |
| 166 | + | orig, ok := stepEnv.Get("PATH") | |
| 129 | 167 | if !ok { | |
| 130 | 168 | orig = j.lookupPath | |
| 131 | 169 | } | |
| @@ -140,14 +178,20 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||
| 140 | 178 | penv.Set("CI", "true") | |
| 141 | 179 | } | |
| 142 | 180 | penv.Set(ProcessTrackingID, j.trackID) | |
| 143 | - | var envList []string | |
| 181 | + | var list []string | |
| 144 | 182 | for _, k := range penv.Keys() { | |
| 145 | 183 | v, _ := penv.Get(k) | |
| 146 | - | envList = append(envList, k+"="+v) | |
| 184 | + | list = append(list, k+"="+v) | |
| 147 | 185 | } | |
| 186 | + | return list | |
| 187 | + | } | |
| 148 | 188 | ||
| 189 | + | // startProcess runs the script and turns how it ended into the result of the | |
| 190 | + | // step. | |
| 191 | + | func (j *job) startProcess(s *stepRun, plan ShellPlan, dir string, argv []string, stepCancel <-chan struct{}) stepResult { | |
| 192 | + | num := s.num | |
| 149 | 193 | res, err := runProcess(procSpec{ | |
| 150 | - | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | |
| 194 | + | Argv: argv, Dir: dir, Env: j.processEnv(s.env), Credential: j.cfg.Credential, | |
| 151 | 195 | OnLine: func(line string) { | |
| 152 | 196 | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | |
| 153 | 197 | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | |
| @@ -161,7 +205,8 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||
| 161 | 205 | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | |
| 162 | 206 | }) | |
| 163 | 207 | if err != nil { | |
| 164 | - | return fail(errors.New(startError(plan.CommandPath, dir, err))) | |
| 208 | + | j.errorf(num, "%s", startError(plan.CommandPath, dir, err)) | |
| 209 | + | return resFailed | |
| 165 | 210 | } | |
| 166 | 211 | if res.Cancelled { | |
| 167 | 212 | // The process was stopped on cancellation: ExecuteAsync throws | |
| @@ -3,14 +3,15 @@ | |||||
| 3 | package executor | 3 | package executor | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | - | "errors" | |||
| 7 | "fmt" | 6 | "fmt" | ||
| 8 | "path/filepath" | 7 | "path/filepath" | ||
| 9 | "strings" | 8 | "strings" | ||
| 9 | + | ||||
| 10 | + | "bnema.dev/ermite-actions/workflow" | |||
| 10 | ) | 11 | ) | ||
| 11 | 12 | ||||
| 12 | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | 13 | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | ||
| 13 | - | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult) { | 14 | + | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) stepResult { |
| 14 | num, st := s.num, s.st | 15 | num, st := s.num, s.st | ||
| 15 | if msg := notSupported(st); msg != "" { | 16 | if msg := notSupported(st); msg != "" { | ||
| 16 | if st.Uses != nil { | 17 | if st.Uses != nil { | ||
| @@ -22,66 +23,100 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||||
| 22 | } | 23 | } | ||
| 23 | fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed } | 24 | fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed } | ||
| 24 | 25 | ||||
| 25 | - | // File commands for this step: the github context carries the paths | 26 | + | if err := j.prepareFileCommands(st); err != nil { |
| 26 | - | // (FileCommandManager.InitializeFiles), event.json is written | 27 | + | return fail(err) |
| 27 | - | // (ExecutionContext.WriteWebhookPayload). | |||
| 28 | - | j.mu.Lock() | |||
| 29 | - | files, ferr := j.fc.Init() | |||
| 30 | - | if ferr == nil { | |||
| 31 | - | for _, c := range fileCommands { | |||
| 32 | - | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |||
| 33 | - | } | |||
| 34 | - | j.state.SetGitHub("action", st.ID) | |||
| 35 | - | j.state.SetGitHub("action_repository", "") | |||
| 36 | - | j.state.SetGitHub("action_ref", "") | |||
| 37 | - | if ev := j.state.EventJSON(); ev != "" { | |||
| 38 | - | if p, err := j.writeEvent(ev); err == nil { | |||
| 39 | - | j.state.SetGitHub("event_path", p) | |||
| 40 | - | } | |||
| 41 | - | } | |||
| 42 | - | } | |||
| 43 | - | j.mu.Unlock() | |||
| 44 | - | if ferr != nil { | |||
| 45 | - | return fail(ferr) | |||
| 46 | } | 28 | } | ||
| 47 | - | defer func() { | 29 | + | // ProcessFiles runs after the handler, even when it failed (a finally block). |
| 48 | - | // ProcessFiles runs after the handler, even when it failed (finally). | 30 | + | defer j.fc.Process(s) |
| 49 | - | j.fc.Process(s) | |||
| 50 | - | }() | |||
| 51 | 31 | ||||
| 52 | - | // Inputs (ActionRunner.cs:174-185). | 32 | + | in, err := j.scriptInputs(s) |
| 53 | - | script, err := j.evalString(st.Run, s.env) | |||
| 54 | if err != nil { | 33 | if err != nil { | ||
| 55 | return fail(err) | 34 | return fail(err) | ||
| 56 | } | 35 | } | ||
| 57 | - | shellInput, err := j.evalString(st.Shell, s.env) | 36 | + | plan, err := j.printHeader(s, in) |
| 37 | + | if err != nil { | |||
| 38 | + | return fail(err) // the group stays open, like the original | |||
| 39 | + | } | |||
| 40 | + | dir, argv, err := j.scriptCommand(plan, in) | |||
| 58 | if err != nil { | 41 | if err != nil { | ||
| 59 | return fail(err) | 42 | return fail(err) | ||
| 60 | } | 43 | } | ||
| 61 | - | var wdInput string | 44 | + | return j.startProcess(s, plan, dir, argv, stepCancel) |
| 62 | - | var hasWD bool | 45 | + | } |
| 46 | + | ||||
| 47 | + | // scriptIn is what a run step evaluates before it prints its header. | |||
| 48 | + | type scriptIn struct { | |||
| 49 | + | script, shell, workingDirectory string | |||
| 50 | + | hasWorkingDirectory bool | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // scriptInputs evaluates the run, shell and working-directory keys of the step | |||
| 54 | + | // (ActionRunner.cs:174-185). The shell and the directory fall back to | |||
| 55 | + | // defaults.run (ScriptHandler.cs:155-180). | |||
| 56 | + | func (j *job) scriptInputs(s *stepRun) (scriptIn, error) { | |||
| 57 | + | st := s.st | |||
| 58 | + | var in scriptIn | |||
| 59 | + | var err error | |||
| 60 | + | if in.script, err = j.evalString(st.Run, s.env); err != nil { | |||
| 61 | + | return in, err | |||
| 62 | + | } | |||
| 63 | + | if in.shell, err = j.evalString(st.Shell, s.env); err != nil { | |||
| 64 | + | return in, err | |||
| 65 | + | } | |||
| 66 | + | if in.shell == "" { | |||
| 67 | + | in.shell = j.runDefaults.shell | |||
| 68 | + | } | |||
| 63 | if st.WorkingDirectory != nil { | 69 | if st.WorkingDirectory != nil { | ||
| 64 | - | hasWD = true | 70 | + | in.hasWorkingDirectory = true |
| 65 | - | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | 71 | + | if in.workingDirectory, err = j.evalString(st.WorkingDirectory, s.env); err != nil { |
| 66 | - | return fail(err) | 72 | + | return in, err |
| 67 | } | 73 | } | ||
| 68 | } | 74 | } | ||
| 75 | + | if !in.hasWorkingDirectory { | |||
| 76 | + | in.workingDirectory = j.runDefaults.workingDirectory | |||
| 77 | + | } | |||
| 78 | + | return in, nil | |||
| 79 | + | } | |||
| 69 | 80 | ||||
| 70 | - | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | 81 | + | // prepareFileCommands creates the files of the step, puts their paths in the |
| 71 | - | j.out(num, "##[group]Run "+firstLine(script)) | 82 | + | // github context (FileCommandManager.InitializeFiles) and writes event.json |
| 72 | - | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | 83 | + | // (ExecutionContext.WriteWebhookPayload). |
| 73 | - | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | 84 | + | func (j *job) prepareFileCommands(st *workflow.Step) error { |
| 85 | + | j.mu.Lock() | |||
| 86 | + | defer j.mu.Unlock() | |||
| 87 | + | files, err := j.fc.Init() | |||
| 88 | + | if err != nil { | |||
| 89 | + | return err | |||
| 74 | } | 90 | } | ||
| 75 | - | shell := shellInput | 91 | + | for _, c := range fileCommands { |
| 76 | - | if shell == "" { | 92 | + | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) |
| 77 | - | shell = j.runDefaults.shell | 93 | + | } |
| 94 | + | j.state.SetGitHub("action", st.ID) | |||
| 95 | + | j.state.SetGitHub("action_repository", "") | |||
| 96 | + | j.state.SetGitHub("action_ref", "") | |||
| 97 | + | if ev := j.state.EventJSON(); ev != "" { | |||
| 98 | + | if p, err := j.writeEvent(ev); err == nil { | |||
| 99 | + | j.state.SetGitHub("event_path", p) | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | return nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // printHeader prints the group of the step as ScriptHandler.PrintActionDetails | |||
| 106 | + | // does (ScriptHandler.cs:27-137): the first line of the script, the script in | |||
| 107 | + | // bright cyan, the shell, the env block of the step, and the end of the group. | |||
| 108 | + | func (j *job) printHeader(s *stepRun, in scriptIn) (ShellPlan, error) { | |||
| 109 | + | num := s.num | |||
| 110 | + | j.out(num, "##[group]Run "+firstLine(in.script)) | |||
| 111 | + | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(in.script, "\r\n", "\n"), "\n"), "\n") { | |||
| 112 | + | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") | |||
| 78 | } | 113 | } | ||
| 79 | j.mu.Lock() | 114 | j.mu.Lock() | ||
| 80 | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | 115 | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | ||
| 81 | j.mu.Unlock() | 116 | j.mu.Unlock() | ||
| 82 | - | plan, err := PlanShell(shell, lookup) | 117 | + | plan, err := PlanShell(in.shell, lookup) |
| 83 | if err != nil { | 118 | if err != nil { | ||
| 84 | - | return fail(err) // the group stays open, like the original | 119 | + | return plan, err |
| 85 | } | 120 | } | ||
| 86 | j.out(num, "shell: "+plan.ShellLine()) | 121 | j.out(num, "shell: "+plan.ShellLine()) | ||
| 87 | if s.env.Len() > 0 { | 122 | if s.env.Len() > 0 { | ||
| @@ -92,24 +127,27 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||||
| 92 | } | 127 | } | ||
| 93 | } | 128 | } | ||
| 94 | j.write(num, "##[endgroup]") | 129 | j.write(num, "##[endgroup]") | ||
| 130 | + | return plan, nil | |||
| 131 | + | } | |||
| 95 | 132 | ||||
| 96 | - | // Run (ScriptHandler.RunAsync, :140-358). | 133 | + | // scriptCommand writes the script file and returns the working directory and |
| 97 | - | wd := wdInput | 134 | + | // the command line (ScriptHandler.RunAsync, :140-285). |
| 98 | - | if !hasWD { | 135 | + | func (j *job) scriptCommand(plan ShellPlan, in scriptIn) (dir string, argv []string, err error) { |
| 99 | - | wd = j.runDefaults.workingDirectory | |||
| 100 | - | } | |||
| 101 | j.mu.Lock() | 136 | j.mu.Lock() | ||
| 102 | workspace := j.state.GitHub("workspace") | 137 | workspace := j.state.GitHub("workspace") | ||
| 103 | temp := j.state.Layout().Temp | 138 | temp := j.state.Layout().Temp | ||
| 104 | j.mu.Unlock() | 139 | j.mu.Unlock() | ||
| 105 | - | dir := ResolveWorkingDirectory(workspace, wd) | 140 | + | sc, err := WriteScript(temp, plan, in.script) |
| 106 | - | sc, err := WriteScript(temp, plan, script) | |||
| 107 | if err != nil { | 141 | if err != nil { | ||
| 108 | - | return fail(err) | 142 | + | return "", nil, err |
| 109 | } | 143 | } | ||
| 144 | + | return ResolveWorkingDirectory(workspace, in.workingDirectory), sc.Argv, nil | |||
| 145 | + | } | |||
| 110 | 146 | ||||
| 111 | - | // Process environment: the inherited one, the step's environment, PATH with | 147 | + | // processEnv builds the environment of the process: the inherited one, the |
| 112 | - | // GITHUB_PATH in front, then the runtime variables of the contexts. | 148 | + | // step's environment, PATH with GITHUB_PATH in front, then the runtime variables |
| 149 | + | // of the contexts, GITHUB_ACTIONS, CI and the tracking id. | |||
| 150 | + | func (j *job) processEnv(stepEnv *ordered) []string { | |||
| 113 | j.mu.Lock() | 151 | j.mu.Lock() | ||
| 114 | runtime := j.state.RuntimeEnv() | 152 | runtime := j.state.RuntimeEnv() | ||
| 115 | prepend := j.state.Prepend() | 153 | prepend := j.state.Prepend() | ||
| @@ -120,12 +158,12 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||||
| 120 | penv.Set(k, v) | 158 | penv.Set(k, v) | ||
| 121 | } | 159 | } | ||
| 122 | } | 160 | } | ||
| 123 | - | for _, k := range s.env.Keys() { | 161 | + | for _, k := range stepEnv.Keys() { |
| 124 | - | v, _ := s.env.Get(k) | 162 | + | v, _ := stepEnv.Get(k) |
| 125 | penv.Set(k, v) | 163 | penv.Set(k, v) | ||
| 126 | } | 164 | } | ||
| 127 | if len(prepend) > 0 { | 165 | if len(prepend) > 0 { | ||
| 128 | - | orig, ok := s.env.Get("PATH") | 166 | + | orig, ok := stepEnv.Get("PATH") |
| 129 | if !ok { | 167 | if !ok { | ||
| 130 | orig = j.lookupPath | 168 | orig = j.lookupPath | ||
| 131 | } | 169 | } | ||
| @@ -140,14 +178,20 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||||
| 140 | penv.Set("CI", "true") | 178 | penv.Set("CI", "true") | ||
| 141 | } | 179 | } | ||
| 142 | penv.Set(ProcessTrackingID, j.trackID) | 180 | penv.Set(ProcessTrackingID, j.trackID) | ||
| 143 | - | var envList []string | 181 | + | var list []string |
| 144 | for _, k := range penv.Keys() { | 182 | for _, k := range penv.Keys() { | ||
| 145 | v, _ := penv.Get(k) | 183 | v, _ := penv.Get(k) | ||
| 146 | - | envList = append(envList, k+"="+v) | 184 | + | list = append(list, k+"="+v) |
| 147 | } | 185 | } | ||
| 186 | + | return list | |||
| 187 | + | } | |||
| 148 | 188 | ||||
| 189 | + | // startProcess runs the script and turns how it ended into the result of the | |||
| 190 | + | // step. | |||
| 191 | + | func (j *job) startProcess(s *stepRun, plan ShellPlan, dir string, argv []string, stepCancel <-chan struct{}) stepResult { | |||
| 192 | + | num := s.num | |||
| 149 | res, err := runProcess(procSpec{ | 193 | res, err := runProcess(procSpec{ | ||
| 150 | - | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | 194 | + | Argv: argv, Dir: dir, Env: j.processEnv(s.env), Credential: j.cfg.Credential, |
| 151 | OnLine: func(line string) { | 195 | OnLine: func(line string) { | ||
| 152 | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | 196 | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | ||
| 153 | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | 197 | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | ||
| @@ -161,7 +205,8 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult | |||||
| 161 | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | 205 | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | ||
| 162 | }) | 206 | }) | ||
| 163 | if err != nil { | 207 | if err != nil { | ||
| 164 | - | return fail(errors.New(startError(plan.CommandPath, dir, err))) | 208 | + | j.errorf(num, "%s", startError(plan.CommandPath, dir, err)) |
| 209 | + | return resFailed | |||
| 165 | } | 210 | } | ||
| 166 | if res.Cancelled { | 211 | if res.Cancelled { | ||
| 167 | // The process was stopped on cancellation: ExecuteAsync throws | 212 | // The process was stopped on cancellation: ExecuteAsync throws | ||
M internal/guest/exec.go +41 -25
| @@ -190,35 +190,47 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err | |||
| 190 | 190 | }}}); err != nil { | |
| 191 | 191 | return 1, fmt.Errorf("send hello: %w", err) | |
| 192 | 192 | } | |
| 193 | + | run, err := waitForRun(ctx, conn, cfg.RunTimeout) | |
| 194 | + | if err != nil { | |
| 195 | + | return 1, err | |
| 196 | + | } | |
| 197 | + | if run.GetJob() != nil { | |
| 198 | + | return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob()), nil) | |
| 199 | + | } | |
| 200 | + | return runStub(ctx, conn, cfg, run) | |
| 201 | + | } | |
| 193 | 202 | ||
| 194 | - | // Wait for Run, bounded: a host that never answers must not pin the sandbox. | |
| 203 | + | // waitForRun waits for the Run frame of the host, bounded: a host that never | |
| 204 | + | // answers must not pin the sandbox. | |
| 205 | + | func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, error) { | |
| 195 | 206 | type recv struct { | |
| 196 | 207 | f *execpb.Frame | |
| 197 | 208 | err error | |
| 198 | 209 | } | |
| 199 | 210 | first := make(chan recv, 1) | |
| 200 | 211 | go func() { f, err := conn.Recv(); first <- recv{f, err} }() | |
| 201 | - | var run *execpb.Run | |
| 212 | + | timer := time.NewTimer(timeout) | |
| 213 | + | defer timer.Stop() | |
| 202 | 214 | select { | |
| 203 | 215 | case r := <-first: | |
| 204 | 216 | if r.err != nil { | |
| 205 | - | return 1, fmt.Errorf("waiting for run: %w", r.err) | |
| 217 | + | return nil, fmt.Errorf("waiting for run: %w", r.err) | |
| 206 | 218 | } | |
| 207 | - | if run = r.f.GetRun(); run == nil { | |
| 208 | - | return 1, errors.New("first frame from host is not run") | |
| 219 | + | run := r.f.GetRun() | |
| 220 | + | if run == nil { | |
| 221 | + | return nil, errors.New("first frame from host is not run") | |
| 209 | 222 | } | |
| 210 | - | case <-time.After(cfg.RunTimeout): | |
| 211 | - | return 1, errors.New("timed out waiting for the host's run frame") | |
| 223 | + | return run, nil | |
| 224 | + | case <-timer.C: | |
| 225 | + | return nil, errors.New("timed out waiting for the host's run frame") | |
| 212 | 226 | case <-ctx.Done(): | |
| 213 | - | return 1, ctx.Err() | |
| 214 | - | } | |
| 215 | - | ||
| 216 | - | if run.GetJob() != nil { | |
| 217 | - | code := runJob(ctx, conn, cfg, run.GetJob()) | |
| 218 | - | return sendExit(conn, code, nil) | |
| 227 | + | return nil, ctx.Err() | |
| 219 | 228 | } | |
| 229 | + | } | |
| 220 | 230 | ||
| 221 | - | // Without a job this is the stub executor of sandbox-check. | |
| 231 | + | // runStub is the stub executor of sandbox-check: it runs the script of a Run | |
| 232 | + | // without a job with the shell and streams its output. | |
| 233 | + | func runStub(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, run *execpb.Run) (int, error) { | |
| 222 | 234 | script := run.GetScript() | |
| 223 | 235 | if script == "" { | |
| 224 | 236 | script = DefaultScript | |
| @@ -259,26 +271,30 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err | |||
| 259 | 271 | ||
| 260 | 272 | waitErr := cmd.Wait() | |
| 261 | 273 | esc.finish() | |
| 274 | + | code, err := exitCode(waitErr) | |
| 275 | + | return sendExit(conn, code, err) | |
| 276 | + | } | |
| 262 | 277 | ||
| 263 | - | code := 0 | |
| 278 | + | // exitCode is the exit code to report for the error of cmd.Wait: 0 for a clean | |
| 279 | + | // exit (also when a background child kept the pipes open past WaitDelay: the | |
| 280 | + | // group was killed by esc.finish), the code of the process (128+signal when it | |
| 281 | + | // was killed, like a shell), or 1 with the error for anything else. | |
| 282 | + | func exitCode(waitErr error) (int, error) { | |
| 264 | 283 | var ee *exec.ExitError | |
| 265 | 284 | switch { | |
| 266 | 285 | case waitErr == nil, errors.Is(waitErr, exec.ErrWaitDelay): | |
| 267 | - | // ErrWaitDelay: the shell exited cleanly but a background child kept the | |
| 268 | - | // pipes open; the group was killed above. | |
| 286 | + | return 0, nil | |
| 269 | 287 | case errors.As(waitErr, &ee): | |
| 270 | - | code = ee.ExitCode() | |
| 271 | - | if code < 0 { // killed by a signal: 128+signal like a shell | |
| 288 | + | code := ee.ExitCode() | |
| 289 | + | if code < 0 { | |
| 272 | 290 | if ws, ok := ee.Sys().(syscall.WaitStatus); ok && ws.Signaled() { | |
| 273 | - | code = 128 + int(ws.Signal()) | |
| 274 | - | } else { | |
| 275 | - | code = 1 | |
| 291 | + | return 128 + int(ws.Signal()), nil | |
| 276 | 292 | } | |
| 293 | + | return 1, nil | |
| 277 | 294 | } | |
| 278 | - | default: | |
| 279 | - | return sendExit(conn, 1, waitErr) | |
| 295 | + | return code, nil | |
| 280 | 296 | } | |
| 281 | - | return sendExit(conn, code, nil) | |
| 297 | + | return 1, waitErr | |
| 282 | 298 | } | |
| 283 | 299 | ||
| 284 | 300 | func sendExit(c *execstream.Conn, code int, cause error) (int, error) { | |
| @@ -190,35 +190,47 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err | |||||
| 190 | }}}); err != nil { | 190 | }}}); err != nil { | ||
| 191 | return 1, fmt.Errorf("send hello: %w", err) | 191 | return 1, fmt.Errorf("send hello: %w", err) | ||
| 192 | } | 192 | } | ||
| 193 | + | run, err := waitForRun(ctx, conn, cfg.RunTimeout) | |||
| 194 | + | if err != nil { | |||
| 195 | + | return 1, err | |||
| 196 | + | } | |||
| 197 | + | if run.GetJob() != nil { | |||
| 198 | + | return sendExit(conn, runJob(ctx, conn, cfg, run.GetJob()), nil) | |||
| 199 | + | } | |||
| 200 | + | return runStub(ctx, conn, cfg, run) | |||
| 201 | + | } | |||
| 193 | 202 | ||||
| 194 | - | // Wait for Run, bounded: a host that never answers must not pin the sandbox. | 203 | + | // waitForRun waits for the Run frame of the host, bounded: a host that never |
| 204 | + | // answers must not pin the sandbox. | |||
| 205 | + | func waitForRun(ctx context.Context, conn *execstream.Conn, timeout time.Duration) (*execpb.Run, error) { | |||
| 195 | type recv struct { | 206 | type recv struct { | ||
| 196 | f *execpb.Frame | 207 | f *execpb.Frame | ||
| 197 | err error | 208 | err error | ||
| 198 | } | 209 | } | ||
| 199 | first := make(chan recv, 1) | 210 | first := make(chan recv, 1) | ||
| 200 | go func() { f, err := conn.Recv(); first <- recv{f, err} }() | 211 | go func() { f, err := conn.Recv(); first <- recv{f, err} }() | ||
| 201 | - | var run *execpb.Run | 212 | + | timer := time.NewTimer(timeout) |
| 213 | + | defer timer.Stop() | |||
| 202 | select { | 214 | select { | ||
| 203 | case r := <-first: | 215 | case r := <-first: | ||
| 204 | if r.err != nil { | 216 | if r.err != nil { | ||
| 205 | - | return 1, fmt.Errorf("waiting for run: %w", r.err) | 217 | + | return nil, fmt.Errorf("waiting for run: %w", r.err) |
| 206 | } | 218 | } | ||
| 207 | - | if run = r.f.GetRun(); run == nil { | 219 | + | run := r.f.GetRun() |
| 208 | - | return 1, errors.New("first frame from host is not run") | 220 | + | if run == nil { |
| 221 | + | return nil, errors.New("first frame from host is not run") | |||
| 209 | } | 222 | } | ||
| 210 | - | case <-time.After(cfg.RunTimeout): | 223 | + | return run, nil |
| 211 | - | return 1, errors.New("timed out waiting for the host's run frame") | 224 | + | case <-timer.C: |
| 225 | + | return nil, errors.New("timed out waiting for the host's run frame") | |||
| 212 | case <-ctx.Done(): | 226 | case <-ctx.Done(): | ||
| 213 | - | return 1, ctx.Err() | 227 | + | return nil, ctx.Err() |
| 214 | - | } | |||
| 215 | - | ||||
| 216 | - | if run.GetJob() != nil { | |||
| 217 | - | code := runJob(ctx, conn, cfg, run.GetJob()) | |||
| 218 | - | return sendExit(conn, code, nil) | |||
| 219 | } | 228 | } | ||
| 229 | + | } | |||
| 220 | 230 | ||||
| 221 | - | // Without a job this is the stub executor of sandbox-check. | 231 | + | // runStub is the stub executor of sandbox-check: it runs the script of a Run |
| 232 | + | // without a job with the shell and streams its output. | |||
| 233 | + | func runStub(ctx context.Context, conn *execstream.Conn, cfg ExecConfig, run *execpb.Run) (int, error) { | |||
| 222 | script := run.GetScript() | 234 | script := run.GetScript() | ||
| 223 | if script == "" { | 235 | if script == "" { | ||
| 224 | script = DefaultScript | 236 | script = DefaultScript | ||
| @@ -259,26 +271,30 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err | |||||
| 259 | 271 | ||||
| 260 | waitErr := cmd.Wait() | 272 | waitErr := cmd.Wait() | ||
| 261 | esc.finish() | 273 | esc.finish() | ||
| 274 | + | code, err := exitCode(waitErr) | |||
| 275 | + | return sendExit(conn, code, err) | |||
| 276 | + | } | |||
| 262 | 277 | ||||
| 263 | - | code := 0 | 278 | + | // exitCode is the exit code to report for the error of cmd.Wait: 0 for a clean |
| 279 | + | // exit (also when a background child kept the pipes open past WaitDelay: the | |||
| 280 | + | // group was killed by esc.finish), the code of the process (128+signal when it | |||
| 281 | + | // was killed, like a shell), or 1 with the error for anything else. | |||
| 282 | + | func exitCode(waitErr error) (int, error) { | |||
| 264 | var ee *exec.ExitError | 283 | var ee *exec.ExitError | ||
| 265 | switch { | 284 | switch { | ||
| 266 | case waitErr == nil, errors.Is(waitErr, exec.ErrWaitDelay): | 285 | case waitErr == nil, errors.Is(waitErr, exec.ErrWaitDelay): | ||
| 267 | - | // ErrWaitDelay: the shell exited cleanly but a background child kept the | 286 | + | return 0, nil |
| 268 | - | // pipes open; the group was killed above. | |||
| 269 | case errors.As(waitErr, &ee): | 287 | case errors.As(waitErr, &ee): | ||
| 270 | - | code = ee.ExitCode() | 288 | + | code := ee.ExitCode() |
| 271 | - | if code < 0 { // killed by a signal: 128+signal like a shell | 289 | + | if code < 0 { |
| 272 | if ws, ok := ee.Sys().(syscall.WaitStatus); ok && ws.Signaled() { | 290 | if ws, ok := ee.Sys().(syscall.WaitStatus); ok && ws.Signaled() { | ||
| 273 | - | code = 128 + int(ws.Signal()) | 291 | + | return 128 + int(ws.Signal()), nil |
| 274 | - | } else { | |||
| 275 | - | code = 1 | |||
| 276 | } | 292 | } | ||
| 293 | + | return 1, nil | |||
| 277 | } | 294 | } | ||
| 278 | - | default: | 295 | + | return code, nil |
| 279 | - | return sendExit(conn, 1, waitErr) | |||
| 280 | } | 296 | } | ||
| 281 | - | return sendExit(conn, code, nil) | 297 | + | return 1, waitErr |
| 282 | } | 298 | } | ||
| 283 | 299 | ||||
| 284 | func sendExit(c *execstream.Conn, code int, cause error) (int, error) { | 300 | func sendExit(c *execstream.Conn, code int, cause error) (int, error) { | ||