detached c5b1e6d

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +147 -86
M internal/executor/exec_step.go +106 -61
@@ -3,14 +3,15 @@
33 package executor
44
55 import (
6- "errors"
76 "fmt"
87 "path/filepath"
98 "strings"
9+
10+ "bnema.dev/ermite-actions/workflow"
1011 )
1112
1213 // 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 {
1415 num, st := s.num, s.st
1516 if msg := notSupported(st); msg != "" {
1617 if st.Uses != nil {
@@ -22,66 +23,100 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult
2223 }
2324 fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed }
2425
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)
4628 }
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)
5131
52- // Inputs (ActionRunner.cs:174-185).
53- script, err := j.evalString(st.Run, s.env)
32+ in, err := j.scriptInputs(s)
5433 if err != nil {
5534 return fail(err)
5635 }
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)
5841 if err != nil {
5942 return fail(err)
6043 }
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+ }
6369 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
6773 }
6874 }
75+ if !in.hasWorkingDirectory {
76+ in.workingDirectory = j.runDefaults.workingDirectory
77+ }
78+ return in, nil
79+}
6980
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
7490 }
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")
78113 }
79114 j.mu.Lock()
80115 lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()}
81116 j.mu.Unlock()
82- plan, err := PlanShell(shell, lookup)
117+ plan, err := PlanShell(in.shell, lookup)
83118 if err != nil {
84- return fail(err) // the group stays open, like the original
119+ return plan, err
85120 }
86121 j.out(num, "shell: "+plan.ShellLine())
87122 if s.env.Len() > 0 {
@@ -92,24 +127,27 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult
92127 }
93128 }
94129 j.write(num, "##[endgroup]")
130+ return plan, nil
131+}
95132
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) {
101136 j.mu.Lock()
102137 workspace := j.state.GitHub("workspace")
103138 temp := j.state.Layout().Temp
104139 j.mu.Unlock()
105- dir := ResolveWorkingDirectory(workspace, wd)
106- sc, err := WriteScript(temp, plan, script)
140+ sc, err := WriteScript(temp, plan, in.script)
107141 if err != nil {
108- return fail(err)
142+ return "", nil, err
109143 }
144+ return ResolveWorkingDirectory(workspace, in.workingDirectory), sc.Argv, nil
145+}
110146
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 {
113151 j.mu.Lock()
114152 runtime := j.state.RuntimeEnv()
115153 prepend := j.state.Prepend()
@@ -120,12 +158,12 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult
120158 penv.Set(k, v)
121159 }
122160 }
123- for _, k := range s.env.Keys() {
124- v, _ := s.env.Get(k)
161+ for _, k := range stepEnv.Keys() {
162+ v, _ := stepEnv.Get(k)
125163 penv.Set(k, v)
126164 }
127165 if len(prepend) > 0 {
128- orig, ok := s.env.Get("PATH")
166+ orig, ok := stepEnv.Get("PATH")
129167 if !ok {
130168 orig = j.lookupPath
131169 }
@@ -140,14 +178,20 @@ func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult
140178 penv.Set("CI", "true")
141179 }
142180 penv.Set(ProcessTrackingID, j.trackID)
143- var envList []string
181+ var list []string
144182 for _, k := range penv.Keys() {
145183 v, _ := penv.Get(k)
146- envList = append(envList, k+"="+v)
184+ list = append(list, k+"="+v)
147185 }
186+ return list
187+}
148188
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
149193 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,
151195 OnLine: func(line string) {
152196 if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") {
153197 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
161205 WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal,
162206 })
163207 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
165210 }
166211 if res.Cancelled {
167212 // The process was stopped on cancellation: ExecuteAsync throws
@@ -3,14 +3,15 @@
3 package executor3 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.st15 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 paths26+ if err := j.prepareFileCommands(st); err != nil {
26- // (FileCommandManager.InitializeFiles), event.json is written27+ 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 string44+ return j.startProcess(s, plan, dir, argv, stepCancel)
62- var hasWD bool45+}
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 = true70+ 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 cyan84+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 := shellInput91+ for _, c := range fileCommands {
76- if shell == "" {92+ j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)])
77- shell = j.runDefaults.shell93+ }
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 original119+ 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 := wdInput134+// 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().Temp138 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 with147+// 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.lookupPath168 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 []string181+ 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 throws212 // 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
190190 }}}); err != nil {
191191 return 1, fmt.Errorf("send hello: %w", err)
192192 }
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+}
193202
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) {
195206 type recv struct {
196207 f *execpb.Frame
197208 err error
198209 }
199210 first := make(chan recv, 1)
200211 go func() { f, err := conn.Recv(); first <- recv{f, err} }()
201- var run *execpb.Run
212+ timer := time.NewTimer(timeout)
213+ defer timer.Stop()
202214 select {
203215 case r := <-first:
204216 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)
206218 }
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")
209222 }
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")
212226 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()
219228 }
229+}
220230
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) {
222234 script := run.GetScript()
223235 if script == "" {
224236 script = DefaultScript
@@ -259,26 +271,30 @@ func serve(ctx context.Context, rw io.ReadWriteCloser, cfg ExecConfig) (int, err
259271
260272 waitErr := cmd.Wait()
261273 esc.finish()
274+ code, err := exitCode(waitErr)
275+ return sendExit(conn, code, err)
276+}
262277
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) {
264283 var ee *exec.ExitError
265284 switch {
266285 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
269287 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 {
272290 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
276292 }
293+ return 1, nil
277294 }
278- default:
279- return sendExit(conn, 1, waitErr)
295+ return code, nil
280296 }
281- return sendExit(conn, code, nil)
297+ return 1, waitErr
282298 }
283299
284300 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.Frame207 f *execpb.Frame
197 err error208 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.Run212+ 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 = DefaultScript236 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 := 0278+// 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.ExitError283 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 the286+ 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 shell289+ 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) {

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