refactor(executor): split steps.go into job.go, step.go and exec_step.go (pure move)
- commit
- 760ddf2848d4d7070a517909ed43bafe3b09154c
- author
- brice · · 2026-10-12 19:21 UTC
- parent
- 96d8ce3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +462 -439
A internal/executor/exec_step.go +185 -0
| @@ -0,0 +1,185 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package executor | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "fmt" | |
| 8 | + | "os" | |
| 9 | + | "path/filepath" | |
| 10 | + | "strings" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | |
| 14 | + | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result 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 | + | ||
| 26 | + | // File commands for this step: the github context carries the paths | |
| 27 | + | // (FileCommandManager.InitializeFiles), event.json is written | |
| 28 | + | // (ExecutionContext.WriteWebhookPayload). | |
| 29 | + | j.mu.Lock() | |
| 30 | + | files, ferr := j.fc.Init() | |
| 31 | + | if ferr == nil { | |
| 32 | + | for _, c := range fileCommands { | |
| 33 | + | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |
| 34 | + | } | |
| 35 | + | j.state.SetGitHub("action", st.ID) | |
| 36 | + | j.state.SetGitHub("action_repository", "") | |
| 37 | + | j.state.SetGitHub("action_ref", "") | |
| 38 | + | if ev := j.state.EventJSON(); ev != "" { | |
| 39 | + | dir := filepath.Join(j.state.Layout().Temp, "_github_workflow") | |
| 40 | + | if err := os.MkdirAll(dir, 0o755); err == nil { | |
| 41 | + | j.own(dir) | |
| 42 | + | p := filepath.Join(dir, "event.json") | |
| 43 | + | if err := os.WriteFile(p, []byte(ev), 0o644); err == nil { | |
| 44 | + | j.own(p) | |
| 45 | + | j.state.SetGitHub("event_path", p) | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | j.mu.Unlock() | |
| 51 | + | if ferr != nil { | |
| 52 | + | return fail(ferr) | |
| 53 | + | } | |
| 54 | + | defer func() { | |
| 55 | + | // ProcessFiles runs after the handler, even when it failed (finally). | |
| 56 | + | j.fc.Process(s) | |
| 57 | + | }() | |
| 58 | + | ||
| 59 | + | // Inputs (ActionRunner.cs:174-185). | |
| 60 | + | script, err := j.evalString(st.Run, s.env) | |
| 61 | + | if err != nil { | |
| 62 | + | return fail(err) | |
| 63 | + | } | |
| 64 | + | shellInput, err := j.evalString(st.Shell, s.env) | |
| 65 | + | if err != nil { | |
| 66 | + | return fail(err) | |
| 67 | + | } | |
| 68 | + | var wdInput string | |
| 69 | + | var hasWD bool | |
| 70 | + | if st.WorkingDirectory != nil { | |
| 71 | + | hasWD = true | |
| 72 | + | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |
| 73 | + | return fail(err) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | |
| 78 | + | j.out(num, "##[group]Run "+firstLine(script)) | |
| 79 | + | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | |
| 80 | + | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | |
| 81 | + | } | |
| 82 | + | shell := shellInput | |
| 83 | + | if shell == "" { | |
| 84 | + | shell = j.runDefaults.shell | |
| 85 | + | } | |
| 86 | + | j.mu.Lock() | |
| 87 | + | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | |
| 88 | + | j.mu.Unlock() | |
| 89 | + | plan, err := PlanShell(shell, lookup) | |
| 90 | + | if err != nil { | |
| 91 | + | return fail(err) // the group stays open, like the original | |
| 92 | + | } | |
| 93 | + | j.out(num, "shell: "+plan.ShellLine()) | |
| 94 | + | if s.env.Len() > 0 { | |
| 95 | + | j.write(num, "env:") | |
| 96 | + | for _, k := range s.env.Keys() { | |
| 97 | + | v, _ := s.env.Get(k) | |
| 98 | + | j.out(num, fmt.Sprintf(" %s: %s", k, v)) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | j.write(num, "##[endgroup]") | |
| 102 | + | ||
| 103 | + | // Run (ScriptHandler.RunAsync, :140-358). | |
| 104 | + | wd := wdInput | |
| 105 | + | if !hasWD { | |
| 106 | + | wd = j.runDefaults.workingDirectory | |
| 107 | + | } | |
| 108 | + | j.mu.Lock() | |
| 109 | + | workspace := j.state.GitHub("workspace") | |
| 110 | + | temp := j.state.Layout().Temp | |
| 111 | + | j.mu.Unlock() | |
| 112 | + | dir := ResolveWorkingDirectory(workspace, wd) | |
| 113 | + | sc, err := WriteScript(temp, plan, script) | |
| 114 | + | if err != nil { | |
| 115 | + | return fail(err) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | // Process environment: the inherited one, the step's environment, PATH with | |
| 119 | + | // GITHUB_PATH in front, then the runtime variables of the contexts. | |
| 120 | + | j.mu.Lock() | |
| 121 | + | runtime := j.state.RuntimeEnv() | |
| 122 | + | prepend := j.state.Prepend() | |
| 123 | + | j.mu.Unlock() | |
| 124 | + | penv := newOrdered() | |
| 125 | + | for _, kv := range j.baseEnv { | |
| 126 | + | if k, v, ok := strings.Cut(kv, "="); ok { | |
| 127 | + | penv.Set(k, v) | |
| 128 | + | } | |
| 129 | + | } | |
| 130 | + | for _, k := range s.env.Keys() { | |
| 131 | + | v, _ := s.env.Get(k) | |
| 132 | + | penv.Set(k, v) | |
| 133 | + | } | |
| 134 | + | if len(prepend) > 0 { | |
| 135 | + | orig, ok := s.env.Get("PATH") | |
| 136 | + | if !ok { | |
| 137 | + | orig = j.lookupPath | |
| 138 | + | } | |
| 139 | + | penv.Set("PATH", PrependedPath(prepend, orig)) | |
| 140 | + | } | |
| 141 | + | for _, k := range runtime.Keys() { | |
| 142 | + | v, _ := runtime.Get(k) | |
| 143 | + | penv.Set(k, v) | |
| 144 | + | } | |
| 145 | + | penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283 | |
| 146 | + | if _, ok := penv.Get("CI"); !ok { | |
| 147 | + | penv.Set("CI", "true") | |
| 148 | + | } | |
| 149 | + | penv.Set(ProcessTrackingID, j.trackID) | |
| 150 | + | var envList []string | |
| 151 | + | for _, k := range penv.Keys() { | |
| 152 | + | v, _ := penv.Get(k) | |
| 153 | + | envList = append(envList, k+"="+v) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | res, err := runProcess(procSpec{ | |
| 157 | + | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | |
| 158 | + | OnLine: func(line string) { | |
| 159 | + | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | |
| 160 | + | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | |
| 161 | + | } | |
| 162 | + | if (strings.Contains(line, "##[") || strings.Contains(line, "::")) && s.cmds.TryProcess(line) { | |
| 163 | + | return | |
| 164 | + | } | |
| 165 | + | j.write(num, line) | |
| 166 | + | }, | |
| 167 | + | Cancel: stepCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter, | |
| 168 | + | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | |
| 169 | + | }) | |
| 170 | + | if err != nil { | |
| 171 | + | return fail(errors.New(startError(plan.CommandPath, dir, err))) | |
| 172 | + | } | |
| 173 | + | if res.Cancelled { | |
| 174 | + | // The process was stopped on cancellation: ExecuteAsync throws | |
| 175 | + | // (ProcessInvoker.cs:369) and StepsRunner logs the exception | |
| 176 | + | // (StepsRunner.cs:333-337). | |
| 177 | + | j.errorf(num, "The operation was canceled.") | |
| 178 | + | return resCanceled | |
| 179 | + | } | |
| 180 | + | if res.ExitCode != 0 { | |
| 181 | + | j.errorf(num, "Process completed with exit code %d.", res.ExitCode) | |
| 182 | + | return resFailed | |
| 183 | + | } | |
| 184 | + | return resSucceeded | |
| 185 | + | } | |
| @@ -0,0 +1,185 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package executor | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "fmt" | |||
| 8 | + | "os" | |||
| 9 | + | "path/filepath" | |||
| 10 | + | "strings" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | |||
| 14 | + | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result 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 | + | ||||
| 26 | + | // File commands for this step: the github context carries the paths | |||
| 27 | + | // (FileCommandManager.InitializeFiles), event.json is written | |||
| 28 | + | // (ExecutionContext.WriteWebhookPayload). | |||
| 29 | + | j.mu.Lock() | |||
| 30 | + | files, ferr := j.fc.Init() | |||
| 31 | + | if ferr == nil { | |||
| 32 | + | for _, c := range fileCommands { | |||
| 33 | + | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |||
| 34 | + | } | |||
| 35 | + | j.state.SetGitHub("action", st.ID) | |||
| 36 | + | j.state.SetGitHub("action_repository", "") | |||
| 37 | + | j.state.SetGitHub("action_ref", "") | |||
| 38 | + | if ev := j.state.EventJSON(); ev != "" { | |||
| 39 | + | dir := filepath.Join(j.state.Layout().Temp, "_github_workflow") | |||
| 40 | + | if err := os.MkdirAll(dir, 0o755); err == nil { | |||
| 41 | + | j.own(dir) | |||
| 42 | + | p := filepath.Join(dir, "event.json") | |||
| 43 | + | if err := os.WriteFile(p, []byte(ev), 0o644); err == nil { | |||
| 44 | + | j.own(p) | |||
| 45 | + | j.state.SetGitHub("event_path", p) | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | j.mu.Unlock() | |||
| 51 | + | if ferr != nil { | |||
| 52 | + | return fail(ferr) | |||
| 53 | + | } | |||
| 54 | + | defer func() { | |||
| 55 | + | // ProcessFiles runs after the handler, even when it failed (finally). | |||
| 56 | + | j.fc.Process(s) | |||
| 57 | + | }() | |||
| 58 | + | ||||
| 59 | + | // Inputs (ActionRunner.cs:174-185). | |||
| 60 | + | script, err := j.evalString(st.Run, s.env) | |||
| 61 | + | if err != nil { | |||
| 62 | + | return fail(err) | |||
| 63 | + | } | |||
| 64 | + | shellInput, err := j.evalString(st.Shell, s.env) | |||
| 65 | + | if err != nil { | |||
| 66 | + | return fail(err) | |||
| 67 | + | } | |||
| 68 | + | var wdInput string | |||
| 69 | + | var hasWD bool | |||
| 70 | + | if st.WorkingDirectory != nil { | |||
| 71 | + | hasWD = true | |||
| 72 | + | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |||
| 73 | + | return fail(err) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | |||
| 78 | + | j.out(num, "##[group]Run "+firstLine(script)) | |||
| 79 | + | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | |||
| 80 | + | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | |||
| 81 | + | } | |||
| 82 | + | shell := shellInput | |||
| 83 | + | if shell == "" { | |||
| 84 | + | shell = j.runDefaults.shell | |||
| 85 | + | } | |||
| 86 | + | j.mu.Lock() | |||
| 87 | + | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | |||
| 88 | + | j.mu.Unlock() | |||
| 89 | + | plan, err := PlanShell(shell, lookup) | |||
| 90 | + | if err != nil { | |||
| 91 | + | return fail(err) // the group stays open, like the original | |||
| 92 | + | } | |||
| 93 | + | j.out(num, "shell: "+plan.ShellLine()) | |||
| 94 | + | if s.env.Len() > 0 { | |||
| 95 | + | j.write(num, "env:") | |||
| 96 | + | for _, k := range s.env.Keys() { | |||
| 97 | + | v, _ := s.env.Get(k) | |||
| 98 | + | j.out(num, fmt.Sprintf(" %s: %s", k, v)) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | j.write(num, "##[endgroup]") | |||
| 102 | + | ||||
| 103 | + | // Run (ScriptHandler.RunAsync, :140-358). | |||
| 104 | + | wd := wdInput | |||
| 105 | + | if !hasWD { | |||
| 106 | + | wd = j.runDefaults.workingDirectory | |||
| 107 | + | } | |||
| 108 | + | j.mu.Lock() | |||
| 109 | + | workspace := j.state.GitHub("workspace") | |||
| 110 | + | temp := j.state.Layout().Temp | |||
| 111 | + | j.mu.Unlock() | |||
| 112 | + | dir := ResolveWorkingDirectory(workspace, wd) | |||
| 113 | + | sc, err := WriteScript(temp, plan, script) | |||
| 114 | + | if err != nil { | |||
| 115 | + | return fail(err) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | // Process environment: the inherited one, the step's environment, PATH with | |||
| 119 | + | // GITHUB_PATH in front, then the runtime variables of the contexts. | |||
| 120 | + | j.mu.Lock() | |||
| 121 | + | runtime := j.state.RuntimeEnv() | |||
| 122 | + | prepend := j.state.Prepend() | |||
| 123 | + | j.mu.Unlock() | |||
| 124 | + | penv := newOrdered() | |||
| 125 | + | for _, kv := range j.baseEnv { | |||
| 126 | + | if k, v, ok := strings.Cut(kv, "="); ok { | |||
| 127 | + | penv.Set(k, v) | |||
| 128 | + | } | |||
| 129 | + | } | |||
| 130 | + | for _, k := range s.env.Keys() { | |||
| 131 | + | v, _ := s.env.Get(k) | |||
| 132 | + | penv.Set(k, v) | |||
| 133 | + | } | |||
| 134 | + | if len(prepend) > 0 { | |||
| 135 | + | orig, ok := s.env.Get("PATH") | |||
| 136 | + | if !ok { | |||
| 137 | + | orig = j.lookupPath | |||
| 138 | + | } | |||
| 139 | + | penv.Set("PATH", PrependedPath(prepend, orig)) | |||
| 140 | + | } | |||
| 141 | + | for _, k := range runtime.Keys() { | |||
| 142 | + | v, _ := runtime.Get(k) | |||
| 143 | + | penv.Set(k, v) | |||
| 144 | + | } | |||
| 145 | + | penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283 | |||
| 146 | + | if _, ok := penv.Get("CI"); !ok { | |||
| 147 | + | penv.Set("CI", "true") | |||
| 148 | + | } | |||
| 149 | + | penv.Set(ProcessTrackingID, j.trackID) | |||
| 150 | + | var envList []string | |||
| 151 | + | for _, k := range penv.Keys() { | |||
| 152 | + | v, _ := penv.Get(k) | |||
| 153 | + | envList = append(envList, k+"="+v) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | res, err := runProcess(procSpec{ | |||
| 157 | + | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | |||
| 158 | + | OnLine: func(line string) { | |||
| 159 | + | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | |||
| 160 | + | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | |||
| 161 | + | } | |||
| 162 | + | if (strings.Contains(line, "##[") || strings.Contains(line, "::")) && s.cmds.TryProcess(line) { | |||
| 163 | + | return | |||
| 164 | + | } | |||
| 165 | + | j.write(num, line) | |||
| 166 | + | }, | |||
| 167 | + | Cancel: stepCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter, | |||
| 168 | + | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | |||
| 169 | + | }) | |||
| 170 | + | if err != nil { | |||
| 171 | + | return fail(errors.New(startError(plan.CommandPath, dir, err))) | |||
| 172 | + | } | |||
| 173 | + | if res.Cancelled { | |||
| 174 | + | // The process was stopped on cancellation: ExecuteAsync throws | |||
| 175 | + | // (ProcessInvoker.cs:369) and StepsRunner logs the exception | |||
| 176 | + | // (StepsRunner.cs:333-337). | |||
| 177 | + | j.errorf(num, "The operation was canceled.") | |||
| 178 | + | return resCanceled | |||
| 179 | + | } | |||
| 180 | + | if res.ExitCode != 0 { | |||
| 181 | + | j.errorf(num, "Process completed with exit code %d.", res.ExitCode) | |||
| 182 | + | return resFailed | |||
| 183 | + | } | |||
| 184 | + | return resSucceeded | |||
| 185 | + | } | |||
R internal/executor/steps.go → internal/executor/job.go +1 -439 renamed, 55% similar
| @@ -13,14 +13,12 @@ import ( | |||
| 13 | 13 | "syscall" | |
| 14 | 14 | "time" | |
| 15 | 15 | ||
| 16 | - | "github.com/google/uuid" | |
| 17 | - | ||
| 18 | 16 | "bnema.dev/ermite-actions/expr" | |
| 19 | 17 | "bnema.dev/ermite-actions/template" | |
| 20 | 18 | "bnema.dev/ermite-actions/workflow" | |
| 19 | + | "github.com/google/uuid" | |
| 21 | 20 | ) | |
| 22 | 21 | ||
| 23 | - | // Names of the implicit steps (JobExtension.cs:70 and :15). | |
| 24 | 22 | const ( | |
| 25 | 23 | SetUpJobName = "Set up job" | |
| 26 | 24 | CompleteJobName = "Complete job" | |
| @@ -472,442 +470,6 @@ func envValue(env []string, key string) string { | |||
| 472 | 470 | return v | |
| 473 | 471 | } | |
| 474 | 472 | ||
| 475 | - | // ---------------------------------------------------------------- steps | |
| 476 | - | ||
| 477 | - | // stepRun is the state of one step being run; it implements Sink and | |
| 478 | - | // commandHost. | |
| 479 | - | type stepRun struct { | |
| 480 | - | j *job | |
| 481 | - | num int | |
| 482 | - | st *workflow.Step | |
| 483 | - | env *ordered // the step's environment: job env + step env | |
| 484 | - | cmdResult *stepResult | |
| 485 | - | outcome *stepResult | |
| 486 | - | cmds *commandManager | |
| 487 | - | } | |
| 488 | - | ||
| 489 | - | func (s *stepRun) Out(text string) { s.j.out(s.num, text) } | |
| 490 | - | func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) } | |
| 491 | - | func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) } | |
| 492 | - | func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) } | |
| 493 | - | func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f } | |
| 494 | - | func (s *stepRun) SetOutput(name, value string) { | |
| 495 | - | s.j.mu.Lock() | |
| 496 | - | s.j.state.SetOutput(s.st.ID, name, value) | |
| 497 | - | s.j.mu.Unlock() | |
| 498 | - | } | |
| 499 | - | func (s *stepRun) SaveState(name, value string) {} | |
| 500 | - | func (s *stepRun) SetEnv(name, value string) { | |
| 501 | - | s.j.mu.Lock() | |
| 502 | - | s.j.state.SetEnv(name, value) | |
| 503 | - | s.j.mu.Unlock() | |
| 504 | - | } | |
| 505 | - | func (s *stepRun) AddPath(p string) { | |
| 506 | - | s.j.mu.Lock() | |
| 507 | - | s.j.state.AddPath(p) | |
| 508 | - | s.j.mu.Unlock() | |
| 509 | - | } | |
| 510 | - | ||
| 511 | - | // EnvFlag reads a boolean variable of the env context, then of the process | |
| 512 | - | // (ActionCommandManager.cs:236-246). | |
| 513 | - | func (s *stepRun) EnvFlag(name string) bool { | |
| 514 | - | v := strings.ToLower(os.Getenv(name)) | |
| 515 | - | if v != "true" { | |
| 516 | - | if e, ok := s.env.Get(name); ok { | |
| 517 | - | v = strings.ToLower(e) | |
| 518 | - | } | |
| 519 | - | } | |
| 520 | - | return v == "true" | |
| 521 | - | } | |
| 522 | - | ||
| 523 | - | func firstLine(contents string) string { | |
| 524 | - | l := strings.TrimLeft(contents, " \t\r\n") | |
| 525 | - | if i := strings.IndexAny(l, "\r\n"); i >= 0 { | |
| 526 | - | l = l[:i] | |
| 527 | - | } | |
| 528 | - | return l | |
| 529 | - | } | |
| 530 | - | ||
| 531 | - | // displayName is the name shown for the step (ActionRunner.GenerateDisplayName, | |
| 532 | - | // ActionRunner.cs:363): the evaluated name, or "Run " and the first line of | |
| 533 | - | // the script or the action reference. | |
| 534 | - | func (j *job) displayName(st *workflow.Step, env *ordered) string { | |
| 535 | - | tok := st.Name | |
| 536 | - | prefix := "" | |
| 537 | - | switch { | |
| 538 | - | case tok != nil: | |
| 539 | - | case st.Run != nil: | |
| 540 | - | tok, prefix = st.Run, "Run " | |
| 541 | - | case st.Uses != nil: | |
| 542 | - | tok, prefix = st.Uses, "Run " | |
| 543 | - | default: | |
| 544 | - | return "" | |
| 545 | - | } | |
| 546 | - | name, err := j.evalString(tok, env) | |
| 547 | - | if err != nil { | |
| 548 | - | name = tokenSource(tok) | |
| 549 | - | } | |
| 550 | - | return formatStepName(prefix, name) | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | func tokenSource(t template.Token) string { | |
| 554 | - | if be, ok := t.(*template.BasicExpression); ok && be.Source != "" { | |
| 555 | - | return be.Source | |
| 556 | - | } | |
| 557 | - | return t.String() | |
| 558 | - | } | |
| 559 | - | ||
| 560 | - | // formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447). | |
| 561 | - | func formatStepName(prefix, name string) string { | |
| 562 | - | if name == "" { | |
| 563 | - | return "" | |
| 564 | - | } | |
| 565 | - | return prefix + firstLine(name) | |
| 566 | - | } | |
| 567 | - | ||
| 568 | - | func (j *job) statusFuncs() workflow.StatusFuncs { | |
| 569 | - | st := j.state.Status() | |
| 570 | - | return workflow.StatusFuncs{Success: st == "success", Failure: st == "failure", Cancelled: st == "cancelled"} | |
| 571 | - | } | |
| 572 | - | ||
| 573 | - | // markCancelled records the cancellation of the job: job result Canceled and | |
| 574 | - | // job.status cancelled (StepsRunner.cs:152-155 and :190-196). | |
| 575 | - | func (j *job) markCancelledLocked() { | |
| 576 | - | if j.cancelled { | |
| 577 | - | return | |
| 578 | - | } | |
| 579 | - | j.cancelled = true | |
| 580 | - | c := resCanceled | |
| 581 | - | j.result = &c | |
| 582 | - | j.state.SetStatus("cancelled") | |
| 583 | - | } | |
| 584 | - | ||
| 585 | - | func (j *job) runStep(num int, st *workflow.Step) { | |
| 586 | - | rep := j.cfg.Reporter | |
| 587 | - | s := &stepRun{j: j, num: num, st: st} | |
| 588 | - | s.cmds = newCommandManager(s, j.cfg.Debug) | |
| 589 | - | ||
| 590 | - | // Environment of the step (StepsRunner.cs:108-139): job env, then the step's | |
| 591 | - | // own, evaluated against the job env. | |
| 592 | - | j.mu.Lock() | |
| 593 | - | jobEnv := j.state.JobEnv() | |
| 594 | - | j.mu.Unlock() | |
| 595 | - | s.env = jobEnv.Clone() | |
| 596 | - | pairs, envErr := j.evalMap(st.Env, jobEnv) | |
| 597 | - | if envErr == nil { | |
| 598 | - | for _, p := range pairs { | |
| 599 | - | s.env.Set(p.Key, p.Value) | |
| 600 | - | } | |
| 601 | - | } | |
| 602 | - | name := j.displayName(st, s.env) | |
| 603 | - | rep.StepStarted(num, name, j.now()) | |
| 604 | - | ||
| 605 | - | complete := func(r stepResult) { | |
| 606 | - | res := r | |
| 607 | - | j.mu.Lock() | |
| 608 | - | // Update the steps context (ExecutionContext.UpdateGlobalStepsContext). | |
| 609 | - | outcome := res | |
| 610 | - | if s.outcome != nil { | |
| 611 | - | outcome = *s.outcome | |
| 612 | - | } | |
| 613 | - | j.state.SetStepResult(st.ID, string(outcome.conclusion()), string(res.conclusion())) | |
| 614 | - | if res == resFailed { | |
| 615 | - | m := merge(j.result, resFailed) | |
| 616 | - | j.result = &m | |
| 617 | - | j.state.SetStatus(statusName(m)) | |
| 618 | - | } | |
| 619 | - | j.mu.Unlock() | |
| 620 | - | rep.StepCompleted(num, name, res.conclusion(), j.now()) | |
| 621 | - | } | |
| 622 | - | ||
| 623 | - | if envErr != nil { | |
| 624 | - | j.errorf(num, "%s", envErr.Error()) | |
| 625 | - | complete(resFailed) | |
| 626 | - | return | |
| 627 | - | } | |
| 628 | - | ||
| 629 | - | // Cancellation seen before the step starts (StepsRunner.cs:188-197). | |
| 630 | - | cancelledAtStart := false | |
| 631 | - | select { | |
| 632 | - | case <-j.cancel.Done(): | |
| 633 | - | cancelledAtStart = true | |
| 634 | - | j.mu.Lock() | |
| 635 | - | j.markCancelledLocked() | |
| 636 | - | j.mu.Unlock() | |
| 637 | - | default: | |
| 638 | - | } | |
| 639 | - | ||
| 640 | - | // Condition (StepsRunner.cs:200-261). On runner shutdown conditions are not | |
| 641 | - | // evaluated and the step is skipped. | |
| 642 | - | cond := func() (bool, error) { | |
| 643 | - | j.mu.Lock() | |
| 644 | - | defer j.mu.Unlock() | |
| 645 | - | return st.EvaluateIf(context.Background(), j.evalEnv(s.env), j.statusFuncs()) | |
| 646 | - | } | |
| 647 | - | if cancelledAtStart && j.cancel.Reason() == CancelShutdown { | |
| 648 | - | complete(resSkipped) | |
| 649 | - | return | |
| 650 | - | } | |
| 651 | - | ok, err := cond() | |
| 652 | - | switch { | |
| 653 | - | case err != nil: | |
| 654 | - | j.errorf(num, "%s", err.Error()) | |
| 655 | - | complete(resFailed) | |
| 656 | - | return | |
| 657 | - | case !ok: | |
| 658 | - | complete(resSkipped) | |
| 659 | - | return | |
| 660 | - | } | |
| 661 | - | ||
| 662 | - | // Cancellation while the step runs (StepsRunner.cs:149-186): the job becomes | |
| 663 | - | // cancelled and the step is stopped unless its condition still holds | |
| 664 | - | // (always()). | |
| 665 | - | stepCancel := make(chan struct{}) | |
| 666 | - | stopWatch := make(chan struct{}) | |
| 667 | - | var watch sync.WaitGroup | |
| 668 | - | if !cancelledAtStart { | |
| 669 | - | watch.Add(1) | |
| 670 | - | go func() { | |
| 671 | - | defer watch.Done() | |
| 672 | - | select { | |
| 673 | - | case <-j.cancel.Done(): | |
| 674 | - | case <-stopWatch: | |
| 675 | - | return | |
| 676 | - | } | |
| 677 | - | j.mu.Lock() | |
| 678 | - | j.markCancelledLocked() | |
| 679 | - | j.mu.Unlock() | |
| 680 | - | keep := false | |
| 681 | - | if j.cancel.Reason() != CancelShutdown { | |
| 682 | - | keep, _ = cond() | |
| 683 | - | } | |
| 684 | - | if !keep { | |
| 685 | - | close(stepCancel) | |
| 686 | - | } | |
| 687 | - | }() | |
| 688 | - | } | |
| 689 | - | ||
| 690 | - | r := j.execute(s, stepCancel) | |
| 691 | - | close(stopWatch) | |
| 692 | - | watch.Wait() | |
| 693 | - | ||
| 694 | - | // Merge the result of the commands (StepsRunner.cs:347-351). | |
| 695 | - | if s.cmdResult != nil { | |
| 696 | - | m := merge(&r, *s.cmdResult) | |
| 697 | - | r = m | |
| 698 | - | } | |
| 699 | - | // continue-on-error (ExecutionContext.ApplyContinueOnError, :1422). | |
| 700 | - | if r == resFailed && st.ContinueOnError != nil { | |
| 701 | - | coe, cerr := template.EvaluateBool(context.Background(), st.ContinueOnError, j.evalEnv(s.env)) | |
| 702 | - | if cerr != nil { | |
| 703 | - | j.errorf(num, "The step failed and an error occurred when attempting to determine whether to continue on error.") | |
| 704 | - | j.errorf(num, "%s", cerr.Error()) | |
| 705 | - | } | |
| 706 | - | if coe && cerr == nil { | |
| 707 | - | o := resFailed | |
| 708 | - | s.outcome = &o | |
| 709 | - | r = resSucceeded | |
| 710 | - | } | |
| 711 | - | } | |
| 712 | - | complete(r) | |
| 713 | - | } | |
| 714 | - | ||
| 715 | - | // statusName is the job.status for a result (TaskResult.ToActionResult). | |
| 716 | - | func statusName(r stepResult) string { | |
| 717 | - | switch r { | |
| 718 | - | case resFailed: | |
| 719 | - | return "failure" | |
| 720 | - | case resCanceled: | |
| 721 | - | return "cancelled" | |
| 722 | - | } | |
| 723 | - | return "success" | |
| 724 | - | } | |
| 725 | - | ||
| 726 | - | // notSupported is the failure of a step that uses a feature of a later phase. | |
| 727 | - | func notSupported(st *workflow.Step) string { | |
| 728 | - | switch { | |
| 729 | - | case st.Uses != nil: | |
| 730 | - | return fmt.Sprintf("not supported yet: uses (%s)", st.Uses.Value) | |
| 731 | - | case st.TimeoutMinutes != nil: | |
| 732 | - | return "not supported yet: timeout-minutes" | |
| 733 | - | } | |
| 734 | - | return "" | |
| 735 | - | } | |
| 736 | - | ||
| 737 | - | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | |
| 738 | - | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult) { | |
| 739 | - | num, st := s.num, s.st | |
| 740 | - | if msg := notSupported(st); msg != "" { | |
| 741 | - | if st.Uses != nil { | |
| 742 | - | j.out(num, "##[group]Run "+st.Uses.Value) | |
| 743 | - | j.out(num, "##[endgroup]") | |
| 744 | - | } | |
| 745 | - | j.errorf(num, "%s", msg) | |
| 746 | - | return resFailed | |
| 747 | - | } | |
| 748 | - | fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed } | |
| 749 | - | ||
| 750 | - | // File commands for this step: the github context carries the paths | |
| 751 | - | // (FileCommandManager.InitializeFiles), event.json is written | |
| 752 | - | // (ExecutionContext.WriteWebhookPayload). | |
| 753 | - | j.mu.Lock() | |
| 754 | - | files, ferr := j.fc.Init() | |
| 755 | - | if ferr == nil { | |
| 756 | - | for _, c := range fileCommands { | |
| 757 | - | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |
| 758 | - | } | |
| 759 | - | j.state.SetGitHub("action", st.ID) | |
| 760 | - | j.state.SetGitHub("action_repository", "") | |
| 761 | - | j.state.SetGitHub("action_ref", "") | |
| 762 | - | if ev := j.state.EventJSON(); ev != "" { | |
| 763 | - | dir := filepath.Join(j.state.Layout().Temp, "_github_workflow") | |
| 764 | - | if err := os.MkdirAll(dir, 0o755); err == nil { | |
| 765 | - | j.own(dir) | |
| 766 | - | p := filepath.Join(dir, "event.json") | |
| 767 | - | if err := os.WriteFile(p, []byte(ev), 0o644); err == nil { | |
| 768 | - | j.own(p) | |
| 769 | - | j.state.SetGitHub("event_path", p) | |
| 770 | - | } | |
| 771 | - | } | |
| 772 | - | } | |
| 773 | - | } | |
| 774 | - | j.mu.Unlock() | |
| 775 | - | if ferr != nil { | |
| 776 | - | return fail(ferr) | |
| 777 | - | } | |
| 778 | - | defer func() { | |
| 779 | - | // ProcessFiles runs after the handler, even when it failed (finally). | |
| 780 | - | j.fc.Process(s) | |
| 781 | - | }() | |
| 782 | - | ||
| 783 | - | // Inputs (ActionRunner.cs:174-185). | |
| 784 | - | script, err := j.evalString(st.Run, s.env) | |
| 785 | - | if err != nil { | |
| 786 | - | return fail(err) | |
| 787 | - | } | |
| 788 | - | shellInput, err := j.evalString(st.Shell, s.env) | |
| 789 | - | if err != nil { | |
| 790 | - | return fail(err) | |
| 791 | - | } | |
| 792 | - | var wdInput string | |
| 793 | - | var hasWD bool | |
| 794 | - | if st.WorkingDirectory != nil { | |
| 795 | - | hasWD = true | |
| 796 | - | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |
| 797 | - | return fail(err) | |
| 798 | - | } | |
| 799 | - | } | |
| 800 | - | ||
| 801 | - | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | |
| 802 | - | j.out(num, "##[group]Run "+firstLine(script)) | |
| 803 | - | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | |
| 804 | - | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | |
| 805 | - | } | |
| 806 | - | shell := shellInput | |
| 807 | - | if shell == "" { | |
| 808 | - | shell = j.runDefaults.shell | |
| 809 | - | } | |
| 810 | - | j.mu.Lock() | |
| 811 | - | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | |
| 812 | - | j.mu.Unlock() | |
| 813 | - | plan, err := PlanShell(shell, lookup) | |
| 814 | - | if err != nil { | |
| 815 | - | return fail(err) // the group stays open, like the original | |
| 816 | - | } | |
| 817 | - | j.out(num, "shell: "+plan.ShellLine()) | |
| 818 | - | if s.env.Len() > 0 { | |
| 819 | - | j.write(num, "env:") | |
| 820 | - | for _, k := range s.env.Keys() { | |
| 821 | - | v, _ := s.env.Get(k) | |
| 822 | - | j.out(num, fmt.Sprintf(" %s: %s", k, v)) | |
| 823 | - | } | |
| 824 | - | } | |
| 825 | - | j.write(num, "##[endgroup]") | |
| 826 | - | ||
| 827 | - | // Run (ScriptHandler.RunAsync, :140-358). | |
| 828 | - | wd := wdInput | |
| 829 | - | if !hasWD { | |
| 830 | - | wd = j.runDefaults.workingDirectory | |
| 831 | - | } | |
| 832 | - | j.mu.Lock() | |
| 833 | - | workspace := j.state.GitHub("workspace") | |
| 834 | - | temp := j.state.Layout().Temp | |
| 835 | - | j.mu.Unlock() | |
| 836 | - | dir := ResolveWorkingDirectory(workspace, wd) | |
| 837 | - | sc, err := WriteScript(temp, plan, script) | |
| 838 | - | if err != nil { | |
| 839 | - | return fail(err) | |
| 840 | - | } | |
| 841 | - | ||
| 842 | - | // Process environment: the inherited one, the step's environment, PATH with | |
| 843 | - | // GITHUB_PATH in front, then the runtime variables of the contexts. | |
| 844 | - | j.mu.Lock() | |
| 845 | - | runtime := j.state.RuntimeEnv() | |
| 846 | - | prepend := j.state.Prepend() | |
| 847 | - | j.mu.Unlock() | |
| 848 | - | penv := newOrdered() | |
| 849 | - | for _, kv := range j.baseEnv { | |
| 850 | - | if k, v, ok := strings.Cut(kv, "="); ok { | |
| 851 | - | penv.Set(k, v) | |
| 852 | - | } | |
| 853 | - | } | |
| 854 | - | for _, k := range s.env.Keys() { | |
| 855 | - | v, _ := s.env.Get(k) | |
| 856 | - | penv.Set(k, v) | |
| 857 | - | } | |
| 858 | - | if len(prepend) > 0 { | |
| 859 | - | orig, ok := s.env.Get("PATH") | |
| 860 | - | if !ok { | |
| 861 | - | orig = j.lookupPath | |
| 862 | - | } | |
| 863 | - | penv.Set("PATH", PrependedPath(prepend, orig)) | |
| 864 | - | } | |
| 865 | - | for _, k := range runtime.Keys() { | |
| 866 | - | v, _ := runtime.Get(k) | |
| 867 | - | penv.Set(k, v) | |
| 868 | - | } | |
| 869 | - | penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283 | |
| 870 | - | if _, ok := penv.Get("CI"); !ok { | |
| 871 | - | penv.Set("CI", "true") | |
| 872 | - | } | |
| 873 | - | penv.Set(ProcessTrackingID, j.trackID) | |
| 874 | - | var envList []string | |
| 875 | - | for _, k := range penv.Keys() { | |
| 876 | - | v, _ := penv.Get(k) | |
| 877 | - | envList = append(envList, k+"="+v) | |
| 878 | - | } | |
| 879 | - | ||
| 880 | - | res, err := runProcess(procSpec{ | |
| 881 | - | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | |
| 882 | - | OnLine: func(line string) { | |
| 883 | - | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | |
| 884 | - | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | |
| 885 | - | } | |
| 886 | - | if (strings.Contains(line, "##[") || strings.Contains(line, "::")) && s.cmds.TryProcess(line) { | |
| 887 | - | return | |
| 888 | - | } | |
| 889 | - | j.write(num, line) | |
| 890 | - | }, | |
| 891 | - | Cancel: stepCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter, | |
| 892 | - | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | |
| 893 | - | }) | |
| 894 | - | if err != nil { | |
| 895 | - | return fail(errors.New(startError(plan.CommandPath, dir, err))) | |
| 896 | - | } | |
| 897 | - | if res.Cancelled { | |
| 898 | - | // The process was stopped on cancellation: ExecuteAsync throws | |
| 899 | - | // (ProcessInvoker.cs:369) and StepsRunner logs the exception | |
| 900 | - | // (StepsRunner.cs:333-337). | |
| 901 | - | j.errorf(num, "The operation was canceled.") | |
| 902 | - | return resCanceled | |
| 903 | - | } | |
| 904 | - | if res.ExitCode != 0 { | |
| 905 | - | j.errorf(num, "Process completed with exit code %d.", res.ExitCode) | |
| 906 | - | return resFailed | |
| 907 | - | } | |
| 908 | - | return resSucceeded | |
| 909 | - | } | |
| 910 | - | ||
| 911 | 473 | // ---------------------------------------------------------------- complete | |
| 912 | 474 | ||
| 913 | 475 | // completeJob is JobExtension.FinalizeJob (JobExtension.cs:9-190): job outputs | |
| @@ -13,14 +13,12 @@ import ( | |||||
| 13 | "syscall" | 13 | "syscall" | ||
| 14 | "time" | 14 | "time" | ||
| 15 | 15 | ||||
| 16 | - | "github.com/google/uuid" | |||
| 17 | - | ||||
| 18 | "bnema.dev/ermite-actions/expr" | 16 | "bnema.dev/ermite-actions/expr" | ||
| 19 | "bnema.dev/ermite-actions/template" | 17 | "bnema.dev/ermite-actions/template" | ||
| 20 | "bnema.dev/ermite-actions/workflow" | 18 | "bnema.dev/ermite-actions/workflow" | ||
| 19 | + | "github.com/google/uuid" | |||
| 21 | ) | 20 | ) | ||
| 22 | 21 | ||||
| 23 | - | // Names of the implicit steps (JobExtension.cs:70 and :15). | |||
| 24 | const ( | 22 | const ( | ||
| 25 | SetUpJobName = "Set up job" | 23 | SetUpJobName = "Set up job" | ||
| 26 | CompleteJobName = "Complete job" | 24 | CompleteJobName = "Complete job" | ||
| @@ -472,442 +470,6 @@ func envValue(env []string, key string) string { | |||||
| 472 | return v | 470 | return v | ||
| 473 | } | 471 | } | ||
| 474 | 472 | ||||
| 475 | - | // ---------------------------------------------------------------- steps | |||
| 476 | - | ||||
| 477 | - | // stepRun is the state of one step being run; it implements Sink and | |||
| 478 | - | // commandHost. | |||
| 479 | - | type stepRun struct { | |||
| 480 | - | j *job | |||
| 481 | - | num int | |||
| 482 | - | st *workflow.Step | |||
| 483 | - | env *ordered // the step's environment: job env + step env | |||
| 484 | - | cmdResult *stepResult | |||
| 485 | - | outcome *stepResult | |||
| 486 | - | cmds *commandManager | |||
| 487 | - | } | |||
| 488 | - | ||||
| 489 | - | func (s *stepRun) Out(text string) { s.j.out(s.num, text) } | |||
| 490 | - | func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) } | |||
| 491 | - | func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) } | |||
| 492 | - | func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) } | |||
| 493 | - | func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f } | |||
| 494 | - | func (s *stepRun) SetOutput(name, value string) { | |||
| 495 | - | s.j.mu.Lock() | |||
| 496 | - | s.j.state.SetOutput(s.st.ID, name, value) | |||
| 497 | - | s.j.mu.Unlock() | |||
| 498 | - | } | |||
| 499 | - | func (s *stepRun) SaveState(name, value string) {} | |||
| 500 | - | func (s *stepRun) SetEnv(name, value string) { | |||
| 501 | - | s.j.mu.Lock() | |||
| 502 | - | s.j.state.SetEnv(name, value) | |||
| 503 | - | s.j.mu.Unlock() | |||
| 504 | - | } | |||
| 505 | - | func (s *stepRun) AddPath(p string) { | |||
| 506 | - | s.j.mu.Lock() | |||
| 507 | - | s.j.state.AddPath(p) | |||
| 508 | - | s.j.mu.Unlock() | |||
| 509 | - | } | |||
| 510 | - | ||||
| 511 | - | // EnvFlag reads a boolean variable of the env context, then of the process | |||
| 512 | - | // (ActionCommandManager.cs:236-246). | |||
| 513 | - | func (s *stepRun) EnvFlag(name string) bool { | |||
| 514 | - | v := strings.ToLower(os.Getenv(name)) | |||
| 515 | - | if v != "true" { | |||
| 516 | - | if e, ok := s.env.Get(name); ok { | |||
| 517 | - | v = strings.ToLower(e) | |||
| 518 | - | } | |||
| 519 | - | } | |||
| 520 | - | return v == "true" | |||
| 521 | - | } | |||
| 522 | - | ||||
| 523 | - | func firstLine(contents string) string { | |||
| 524 | - | l := strings.TrimLeft(contents, " \t\r\n") | |||
| 525 | - | if i := strings.IndexAny(l, "\r\n"); i >= 0 { | |||
| 526 | - | l = l[:i] | |||
| 527 | - | } | |||
| 528 | - | return l | |||
| 529 | - | } | |||
| 530 | - | ||||
| 531 | - | // displayName is the name shown for the step (ActionRunner.GenerateDisplayName, | |||
| 532 | - | // ActionRunner.cs:363): the evaluated name, or "Run " and the first line of | |||
| 533 | - | // the script or the action reference. | |||
| 534 | - | func (j *job) displayName(st *workflow.Step, env *ordered) string { | |||
| 535 | - | tok := st.Name | |||
| 536 | - | prefix := "" | |||
| 537 | - | switch { | |||
| 538 | - | case tok != nil: | |||
| 539 | - | case st.Run != nil: | |||
| 540 | - | tok, prefix = st.Run, "Run " | |||
| 541 | - | case st.Uses != nil: | |||
| 542 | - | tok, prefix = st.Uses, "Run " | |||
| 543 | - | default: | |||
| 544 | - | return "" | |||
| 545 | - | } | |||
| 546 | - | name, err := j.evalString(tok, env) | |||
| 547 | - | if err != nil { | |||
| 548 | - | name = tokenSource(tok) | |||
| 549 | - | } | |||
| 550 | - | return formatStepName(prefix, name) | |||
| 551 | - | } | |||
| 552 | - | ||||
| 553 | - | func tokenSource(t template.Token) string { | |||
| 554 | - | if be, ok := t.(*template.BasicExpression); ok && be.Source != "" { | |||
| 555 | - | return be.Source | |||
| 556 | - | } | |||
| 557 | - | return t.String() | |||
| 558 | - | } | |||
| 559 | - | ||||
| 560 | - | // formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447). | |||
| 561 | - | func formatStepName(prefix, name string) string { | |||
| 562 | - | if name == "" { | |||
| 563 | - | return "" | |||
| 564 | - | } | |||
| 565 | - | return prefix + firstLine(name) | |||
| 566 | - | } | |||
| 567 | - | ||||
| 568 | - | func (j *job) statusFuncs() workflow.StatusFuncs { | |||
| 569 | - | st := j.state.Status() | |||
| 570 | - | return workflow.StatusFuncs{Success: st == "success", Failure: st == "failure", Cancelled: st == "cancelled"} | |||
| 571 | - | } | |||
| 572 | - | ||||
| 573 | - | // markCancelled records the cancellation of the job: job result Canceled and | |||
| 574 | - | // job.status cancelled (StepsRunner.cs:152-155 and :190-196). | |||
| 575 | - | func (j *job) markCancelledLocked() { | |||
| 576 | - | if j.cancelled { | |||
| 577 | - | return | |||
| 578 | - | } | |||
| 579 | - | j.cancelled = true | |||
| 580 | - | c := resCanceled | |||
| 581 | - | j.result = &c | |||
| 582 | - | j.state.SetStatus("cancelled") | |||
| 583 | - | } | |||
| 584 | - | ||||
| 585 | - | func (j *job) runStep(num int, st *workflow.Step) { | |||
| 586 | - | rep := j.cfg.Reporter | |||
| 587 | - | s := &stepRun{j: j, num: num, st: st} | |||
| 588 | - | s.cmds = newCommandManager(s, j.cfg.Debug) | |||
| 589 | - | ||||
| 590 | - | // Environment of the step (StepsRunner.cs:108-139): job env, then the step's | |||
| 591 | - | // own, evaluated against the job env. | |||
| 592 | - | j.mu.Lock() | |||
| 593 | - | jobEnv := j.state.JobEnv() | |||
| 594 | - | j.mu.Unlock() | |||
| 595 | - | s.env = jobEnv.Clone() | |||
| 596 | - | pairs, envErr := j.evalMap(st.Env, jobEnv) | |||
| 597 | - | if envErr == nil { | |||
| 598 | - | for _, p := range pairs { | |||
| 599 | - | s.env.Set(p.Key, p.Value) | |||
| 600 | - | } | |||
| 601 | - | } | |||
| 602 | - | name := j.displayName(st, s.env) | |||
| 603 | - | rep.StepStarted(num, name, j.now()) | |||
| 604 | - | ||||
| 605 | - | complete := func(r stepResult) { | |||
| 606 | - | res := r | |||
| 607 | - | j.mu.Lock() | |||
| 608 | - | // Update the steps context (ExecutionContext.UpdateGlobalStepsContext). | |||
| 609 | - | outcome := res | |||
| 610 | - | if s.outcome != nil { | |||
| 611 | - | outcome = *s.outcome | |||
| 612 | - | } | |||
| 613 | - | j.state.SetStepResult(st.ID, string(outcome.conclusion()), string(res.conclusion())) | |||
| 614 | - | if res == resFailed { | |||
| 615 | - | m := merge(j.result, resFailed) | |||
| 616 | - | j.result = &m | |||
| 617 | - | j.state.SetStatus(statusName(m)) | |||
| 618 | - | } | |||
| 619 | - | j.mu.Unlock() | |||
| 620 | - | rep.StepCompleted(num, name, res.conclusion(), j.now()) | |||
| 621 | - | } | |||
| 622 | - | ||||
| 623 | - | if envErr != nil { | |||
| 624 | - | j.errorf(num, "%s", envErr.Error()) | |||
| 625 | - | complete(resFailed) | |||
| 626 | - | return | |||
| 627 | - | } | |||
| 628 | - | ||||
| 629 | - | // Cancellation seen before the step starts (StepsRunner.cs:188-197). | |||
| 630 | - | cancelledAtStart := false | |||
| 631 | - | select { | |||
| 632 | - | case <-j.cancel.Done(): | |||
| 633 | - | cancelledAtStart = true | |||
| 634 | - | j.mu.Lock() | |||
| 635 | - | j.markCancelledLocked() | |||
| 636 | - | j.mu.Unlock() | |||
| 637 | - | default: | |||
| 638 | - | } | |||
| 639 | - | ||||
| 640 | - | // Condition (StepsRunner.cs:200-261). On runner shutdown conditions are not | |||
| 641 | - | // evaluated and the step is skipped. | |||
| 642 | - | cond := func() (bool, error) { | |||
| 643 | - | j.mu.Lock() | |||
| 644 | - | defer j.mu.Unlock() | |||
| 645 | - | return st.EvaluateIf(context.Background(), j.evalEnv(s.env), j.statusFuncs()) | |||
| 646 | - | } | |||
| 647 | - | if cancelledAtStart && j.cancel.Reason() == CancelShutdown { | |||
| 648 | - | complete(resSkipped) | |||
| 649 | - | return | |||
| 650 | - | } | |||
| 651 | - | ok, err := cond() | |||
| 652 | - | switch { | |||
| 653 | - | case err != nil: | |||
| 654 | - | j.errorf(num, "%s", err.Error()) | |||
| 655 | - | complete(resFailed) | |||
| 656 | - | return | |||
| 657 | - | case !ok: | |||
| 658 | - | complete(resSkipped) | |||
| 659 | - | return | |||
| 660 | - | } | |||
| 661 | - | ||||
| 662 | - | // Cancellation while the step runs (StepsRunner.cs:149-186): the job becomes | |||
| 663 | - | // cancelled and the step is stopped unless its condition still holds | |||
| 664 | - | // (always()). | |||
| 665 | - | stepCancel := make(chan struct{}) | |||
| 666 | - | stopWatch := make(chan struct{}) | |||
| 667 | - | var watch sync.WaitGroup | |||
| 668 | - | if !cancelledAtStart { | |||
| 669 | - | watch.Add(1) | |||
| 670 | - | go func() { | |||
| 671 | - | defer watch.Done() | |||
| 672 | - | select { | |||
| 673 | - | case <-j.cancel.Done(): | |||
| 674 | - | case <-stopWatch: | |||
| 675 | - | return | |||
| 676 | - | } | |||
| 677 | - | j.mu.Lock() | |||
| 678 | - | j.markCancelledLocked() | |||
| 679 | - | j.mu.Unlock() | |||
| 680 | - | keep := false | |||
| 681 | - | if j.cancel.Reason() != CancelShutdown { | |||
| 682 | - | keep, _ = cond() | |||
| 683 | - | } | |||
| 684 | - | if !keep { | |||
| 685 | - | close(stepCancel) | |||
| 686 | - | } | |||
| 687 | - | }() | |||
| 688 | - | } | |||
| 689 | - | ||||
| 690 | - | r := j.execute(s, stepCancel) | |||
| 691 | - | close(stopWatch) | |||
| 692 | - | watch.Wait() | |||
| 693 | - | ||||
| 694 | - | // Merge the result of the commands (StepsRunner.cs:347-351). | |||
| 695 | - | if s.cmdResult != nil { | |||
| 696 | - | m := merge(&r, *s.cmdResult) | |||
| 697 | - | r = m | |||
| 698 | - | } | |||
| 699 | - | // continue-on-error (ExecutionContext.ApplyContinueOnError, :1422). | |||
| 700 | - | if r == resFailed && st.ContinueOnError != nil { | |||
| 701 | - | coe, cerr := template.EvaluateBool(context.Background(), st.ContinueOnError, j.evalEnv(s.env)) | |||
| 702 | - | if cerr != nil { | |||
| 703 | - | j.errorf(num, "The step failed and an error occurred when attempting to determine whether to continue on error.") | |||
| 704 | - | j.errorf(num, "%s", cerr.Error()) | |||
| 705 | - | } | |||
| 706 | - | if coe && cerr == nil { | |||
| 707 | - | o := resFailed | |||
| 708 | - | s.outcome = &o | |||
| 709 | - | r = resSucceeded | |||
| 710 | - | } | |||
| 711 | - | } | |||
| 712 | - | complete(r) | |||
| 713 | - | } | |||
| 714 | - | ||||
| 715 | - | // statusName is the job.status for a result (TaskResult.ToActionResult). | |||
| 716 | - | func statusName(r stepResult) string { | |||
| 717 | - | switch r { | |||
| 718 | - | case resFailed: | |||
| 719 | - | return "failure" | |||
| 720 | - | case resCanceled: | |||
| 721 | - | return "cancelled" | |||
| 722 | - | } | |||
| 723 | - | return "success" | |||
| 724 | - | } | |||
| 725 | - | ||||
| 726 | - | // notSupported is the failure of a step that uses a feature of a later phase. | |||
| 727 | - | func notSupported(st *workflow.Step) string { | |||
| 728 | - | switch { | |||
| 729 | - | case st.Uses != nil: | |||
| 730 | - | return fmt.Sprintf("not supported yet: uses (%s)", st.Uses.Value) | |||
| 731 | - | case st.TimeoutMinutes != nil: | |||
| 732 | - | return "not supported yet: timeout-minutes" | |||
| 733 | - | } | |||
| 734 | - | return "" | |||
| 735 | - | } | |||
| 736 | - | ||||
| 737 | - | // execute is ActionRunner.RunAsync plus ScriptHandler for one run step. | |||
| 738 | - | func (j *job) execute(s *stepRun, stepCancel <-chan struct{}) (result stepResult) { | |||
| 739 | - | num, st := s.num, s.st | |||
| 740 | - | if msg := notSupported(st); msg != "" { | |||
| 741 | - | if st.Uses != nil { | |||
| 742 | - | j.out(num, "##[group]Run "+st.Uses.Value) | |||
| 743 | - | j.out(num, "##[endgroup]") | |||
| 744 | - | } | |||
| 745 | - | j.errorf(num, "%s", msg) | |||
| 746 | - | return resFailed | |||
| 747 | - | } | |||
| 748 | - | fail := func(err error) stepResult { j.errorf(num, "%s", err.Error()); return resFailed } | |||
| 749 | - | ||||
| 750 | - | // File commands for this step: the github context carries the paths | |||
| 751 | - | // (FileCommandManager.InitializeFiles), event.json is written | |||
| 752 | - | // (ExecutionContext.WriteWebhookPayload). | |||
| 753 | - | j.mu.Lock() | |||
| 754 | - | files, ferr := j.fc.Init() | |||
| 755 | - | if ferr == nil { | |||
| 756 | - | for _, c := range fileCommands { | |||
| 757 | - | j.state.SetGitHub(c.ContextName, files[envVarName(c.ContextName)]) | |||
| 758 | - | } | |||
| 759 | - | j.state.SetGitHub("action", st.ID) | |||
| 760 | - | j.state.SetGitHub("action_repository", "") | |||
| 761 | - | j.state.SetGitHub("action_ref", "") | |||
| 762 | - | if ev := j.state.EventJSON(); ev != "" { | |||
| 763 | - | dir := filepath.Join(j.state.Layout().Temp, "_github_workflow") | |||
| 764 | - | if err := os.MkdirAll(dir, 0o755); err == nil { | |||
| 765 | - | j.own(dir) | |||
| 766 | - | p := filepath.Join(dir, "event.json") | |||
| 767 | - | if err := os.WriteFile(p, []byte(ev), 0o644); err == nil { | |||
| 768 | - | j.own(p) | |||
| 769 | - | j.state.SetGitHub("event_path", p) | |||
| 770 | - | } | |||
| 771 | - | } | |||
| 772 | - | } | |||
| 773 | - | } | |||
| 774 | - | j.mu.Unlock() | |||
| 775 | - | if ferr != nil { | |||
| 776 | - | return fail(ferr) | |||
| 777 | - | } | |||
| 778 | - | defer func() { | |||
| 779 | - | // ProcessFiles runs after the handler, even when it failed (finally). | |||
| 780 | - | j.fc.Process(s) | |||
| 781 | - | }() | |||
| 782 | - | ||||
| 783 | - | // Inputs (ActionRunner.cs:174-185). | |||
| 784 | - | script, err := j.evalString(st.Run, s.env) | |||
| 785 | - | if err != nil { | |||
| 786 | - | return fail(err) | |||
| 787 | - | } | |||
| 788 | - | shellInput, err := j.evalString(st.Shell, s.env) | |||
| 789 | - | if err != nil { | |||
| 790 | - | return fail(err) | |||
| 791 | - | } | |||
| 792 | - | var wdInput string | |||
| 793 | - | var hasWD bool | |||
| 794 | - | if st.WorkingDirectory != nil { | |||
| 795 | - | hasWD = true | |||
| 796 | - | if wdInput, err = j.evalString(st.WorkingDirectory, s.env); err != nil { | |||
| 797 | - | return fail(err) | |||
| 798 | - | } | |||
| 799 | - | } | |||
| 800 | - | ||||
| 801 | - | // Header (ScriptHandler.PrintActionDetails, ScriptHandler.cs:27-137). | |||
| 802 | - | j.out(num, "##[group]Run "+firstLine(script)) | |||
| 803 | - | for _, l := range strings.Split(strings.TrimRight(strings.ReplaceAll(script, "\r\n", "\n"), "\n"), "\n") { | |||
| 804 | - | j.write(num, "\x1b[36;1m"+l+"\x1b[0m") // bright cyan | |||
| 805 | - | } | |||
| 806 | - | shell := shellInput | |||
| 807 | - | if shell == "" { | |||
| 808 | - | shell = j.runDefaults.shell | |||
| 809 | - | } | |||
| 810 | - | j.mu.Lock() | |||
| 811 | - | lookup := Lookup{Path: j.lookupPath, Prepend: j.state.Prepend()} | |||
| 812 | - | j.mu.Unlock() | |||
| 813 | - | plan, err := PlanShell(shell, lookup) | |||
| 814 | - | if err != nil { | |||
| 815 | - | return fail(err) // the group stays open, like the original | |||
| 816 | - | } | |||
| 817 | - | j.out(num, "shell: "+plan.ShellLine()) | |||
| 818 | - | if s.env.Len() > 0 { | |||
| 819 | - | j.write(num, "env:") | |||
| 820 | - | for _, k := range s.env.Keys() { | |||
| 821 | - | v, _ := s.env.Get(k) | |||
| 822 | - | j.out(num, fmt.Sprintf(" %s: %s", k, v)) | |||
| 823 | - | } | |||
| 824 | - | } | |||
| 825 | - | j.write(num, "##[endgroup]") | |||
| 826 | - | ||||
| 827 | - | // Run (ScriptHandler.RunAsync, :140-358). | |||
| 828 | - | wd := wdInput | |||
| 829 | - | if !hasWD { | |||
| 830 | - | wd = j.runDefaults.workingDirectory | |||
| 831 | - | } | |||
| 832 | - | j.mu.Lock() | |||
| 833 | - | workspace := j.state.GitHub("workspace") | |||
| 834 | - | temp := j.state.Layout().Temp | |||
| 835 | - | j.mu.Unlock() | |||
| 836 | - | dir := ResolveWorkingDirectory(workspace, wd) | |||
| 837 | - | sc, err := WriteScript(temp, plan, script) | |||
| 838 | - | if err != nil { | |||
| 839 | - | return fail(err) | |||
| 840 | - | } | |||
| 841 | - | ||||
| 842 | - | // Process environment: the inherited one, the step's environment, PATH with | |||
| 843 | - | // GITHUB_PATH in front, then the runtime variables of the contexts. | |||
| 844 | - | j.mu.Lock() | |||
| 845 | - | runtime := j.state.RuntimeEnv() | |||
| 846 | - | prepend := j.state.Prepend() | |||
| 847 | - | j.mu.Unlock() | |||
| 848 | - | penv := newOrdered() | |||
| 849 | - | for _, kv := range j.baseEnv { | |||
| 850 | - | if k, v, ok := strings.Cut(kv, "="); ok { | |||
| 851 | - | penv.Set(k, v) | |||
| 852 | - | } | |||
| 853 | - | } | |||
| 854 | - | for _, k := range s.env.Keys() { | |||
| 855 | - | v, _ := s.env.Get(k) | |||
| 856 | - | penv.Set(k, v) | |||
| 857 | - | } | |||
| 858 | - | if len(prepend) > 0 { | |||
| 859 | - | orig, ok := s.env.Get("PATH") | |||
| 860 | - | if !ok { | |||
| 861 | - | orig = j.lookupPath | |||
| 862 | - | } | |||
| 863 | - | penv.Set("PATH", PrependedPath(prepend, orig)) | |||
| 864 | - | } | |||
| 865 | - | for _, k := range runtime.Keys() { | |||
| 866 | - | v, _ := runtime.Get(k) | |||
| 867 | - | penv.Set(k, v) | |||
| 868 | - | } | |||
| 869 | - | penv.Set("GITHUB_ACTIONS", "true") // ProcessInvoker.cs:283 | |||
| 870 | - | if _, ok := penv.Get("CI"); !ok { | |||
| 871 | - | penv.Set("CI", "true") | |||
| 872 | - | } | |||
| 873 | - | penv.Set(ProcessTrackingID, j.trackID) | |||
| 874 | - | var envList []string | |||
| 875 | - | for _, k := range penv.Keys() { | |||
| 876 | - | v, _ := penv.Get(k) | |||
| 877 | - | envList = append(envList, k+"="+v) | |||
| 878 | - | } | |||
| 879 | - | ||||
| 880 | - | res, err := runProcess(procSpec{ | |||
| 881 | - | Argv: sc.Argv, Dir: dir, Env: envList, Credential: j.cfg.Credential, | |||
| 882 | - | OnLine: func(line string) { | |||
| 883 | - | if strings.Contains(line, "##[start-action") || strings.Contains(line, "##[end-action") { | |||
| 884 | - | line = strings.ReplaceAll(strings.ReplaceAll(line, "##[start-action", `##[\start-action`), "##[end-action", `##[\end-action`) | |||
| 885 | - | } | |||
| 886 | - | if (strings.Contains(line, "##[") || strings.Contains(line, "::")) && s.cmds.TryProcess(line) { | |||
| 887 | - | return | |||
| 888 | - | } | |||
| 889 | - | j.write(num, line) | |||
| 890 | - | }, | |||
| 891 | - | Cancel: stepCancel, TermAfter: j.cfg.TermAfter, KillAfter: j.cfg.KillAfter, | |||
| 892 | - | WaitDelay: j.cfg.WaitDelay, Signal: j.cfg.Signal, | |||
| 893 | - | }) | |||
| 894 | - | if err != nil { | |||
| 895 | - | return fail(errors.New(startError(plan.CommandPath, dir, err))) | |||
| 896 | - | } | |||
| 897 | - | if res.Cancelled { | |||
| 898 | - | // The process was stopped on cancellation: ExecuteAsync throws | |||
| 899 | - | // (ProcessInvoker.cs:369) and StepsRunner logs the exception | |||
| 900 | - | // (StepsRunner.cs:333-337). | |||
| 901 | - | j.errorf(num, "The operation was canceled.") | |||
| 902 | - | return resCanceled | |||
| 903 | - | } | |||
| 904 | - | if res.ExitCode != 0 { | |||
| 905 | - | j.errorf(num, "Process completed with exit code %d.", res.ExitCode) | |||
| 906 | - | return resFailed | |||
| 907 | - | } | |||
| 908 | - | return resSucceeded | |||
| 909 | - | } | |||
| 910 | - | ||||
| 911 | // ---------------------------------------------------------------- complete | 473 | // ---------------------------------------------------------------- complete | ||
| 912 | 474 | ||||
| 913 | // completeJob is JobExtension.FinalizeJob (JobExtension.cs:9-190): job outputs | 475 | // completeJob is JobExtension.FinalizeJob (JobExtension.cs:9-190): job outputs | ||
R internal/executor/steps_test.go → internal/executor/job_test.go +0 -0 renamed, 100% similar
renamed, 100% similar
A internal/executor/step.go +276 -0
| @@ -0,0 +1,276 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package executor | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "fmt" | |
| 8 | + | "os" | |
| 9 | + | "strings" | |
| 10 | + | "sync" | |
| 11 | + | ||
| 12 | + | "bnema.dev/ermite-actions/template" | |
| 13 | + | "bnema.dev/ermite-actions/workflow" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // ---------------------------------------------------------------- steps | |
| 17 | + | ||
| 18 | + | // stepRun is the state of one step being run; it implements Sink and | |
| 19 | + | // commandHost. | |
| 20 | + | type stepRun struct { | |
| 21 | + | j *job | |
| 22 | + | num int | |
| 23 | + | st *workflow.Step | |
| 24 | + | env *ordered // the step's environment: job env + step env | |
| 25 | + | cmdResult *stepResult | |
| 26 | + | outcome *stepResult | |
| 27 | + | cmds *commandManager | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | func (s *stepRun) Out(text string) { s.j.out(s.num, text) } | |
| 31 | + | func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) } | |
| 32 | + | func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) } | |
| 33 | + | func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) } | |
| 34 | + | func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f } | |
| 35 | + | func (s *stepRun) SetOutput(name, value string) { | |
| 36 | + | s.j.mu.Lock() | |
| 37 | + | s.j.state.SetOutput(s.st.ID, name, value) | |
| 38 | + | s.j.mu.Unlock() | |
| 39 | + | } | |
| 40 | + | func (s *stepRun) SaveState(name, value string) {} | |
| 41 | + | func (s *stepRun) SetEnv(name, value string) { | |
| 42 | + | s.j.mu.Lock() | |
| 43 | + | s.j.state.SetEnv(name, value) | |
| 44 | + | s.j.mu.Unlock() | |
| 45 | + | } | |
| 46 | + | func (s *stepRun) AddPath(p string) { | |
| 47 | + | s.j.mu.Lock() | |
| 48 | + | s.j.state.AddPath(p) | |
| 49 | + | s.j.mu.Unlock() | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | // EnvFlag reads a boolean variable of the env context, then of the process | |
| 53 | + | // (ActionCommandManager.cs:236-246). | |
| 54 | + | func (s *stepRun) EnvFlag(name string) bool { | |
| 55 | + | v := strings.ToLower(os.Getenv(name)) | |
| 56 | + | if v != "true" { | |
| 57 | + | if e, ok := s.env.Get(name); ok { | |
| 58 | + | v = strings.ToLower(e) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | return v == "true" | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | func firstLine(contents string) string { | |
| 65 | + | l := strings.TrimLeft(contents, " \t\r\n") | |
| 66 | + | if i := strings.IndexAny(l, "\r\n"); i >= 0 { | |
| 67 | + | l = l[:i] | |
| 68 | + | } | |
| 69 | + | return l | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // displayName is the name shown for the step (ActionRunner.GenerateDisplayName, | |
| 73 | + | // ActionRunner.cs:363): the evaluated name, or "Run " and the first line of | |
| 74 | + | // the script or the action reference. | |
| 75 | + | func (j *job) displayName(st *workflow.Step, env *ordered) string { | |
| 76 | + | tok := st.Name | |
| 77 | + | prefix := "" | |
| 78 | + | switch { | |
| 79 | + | case tok != nil: | |
| 80 | + | case st.Run != nil: | |
| 81 | + | tok, prefix = st.Run, "Run " | |
| 82 | + | case st.Uses != nil: | |
| 83 | + | tok, prefix = st.Uses, "Run " | |
| 84 | + | default: | |
| 85 | + | return "" | |
| 86 | + | } | |
| 87 | + | name, err := j.evalString(tok, env) | |
| 88 | + | if err != nil { | |
| 89 | + | name = tokenSource(tok) | |
| 90 | + | } | |
| 91 | + | return formatStepName(prefix, name) | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func tokenSource(t template.Token) string { | |
| 95 | + | if be, ok := t.(*template.BasicExpression); ok && be.Source != "" { | |
| 96 | + | return be.Source | |
| 97 | + | } | |
| 98 | + | return t.String() | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | // formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447). | |
| 102 | + | func formatStepName(prefix, name string) string { | |
| 103 | + | if name == "" { | |
| 104 | + | return "" | |
| 105 | + | } | |
| 106 | + | return prefix + firstLine(name) | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | func (j *job) statusFuncs() workflow.StatusFuncs { | |
| 110 | + | st := j.state.Status() | |
| 111 | + | return workflow.StatusFuncs{Success: st == "success", Failure: st == "failure", Cancelled: st == "cancelled"} | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // markCancelled records the cancellation of the job: job result Canceled and | |
| 115 | + | // job.status cancelled (StepsRunner.cs:152-155 and :190-196). | |
| 116 | + | func (j *job) markCancelledLocked() { | |
| 117 | + | if j.cancelled { | |
| 118 | + | return | |
| 119 | + | } | |
| 120 | + | j.cancelled = true | |
| 121 | + | c := resCanceled | |
| 122 | + | j.result = &c | |
| 123 | + | j.state.SetStatus("cancelled") | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | func (j *job) runStep(num int, st *workflow.Step) { | |
| 127 | + | rep := j.cfg.Reporter | |
| 128 | + | s := &stepRun{j: j, num: num, st: st} | |
| 129 | + | s.cmds = newCommandManager(s, j.cfg.Debug) | |
| 130 | + | ||
| 131 | + | // Environment of the step (StepsRunner.cs:108-139): job env, then the step's | |
| 132 | + | // own, evaluated against the job env. | |
| 133 | + | j.mu.Lock() | |
| 134 | + | jobEnv := j.state.JobEnv() | |
| 135 | + | j.mu.Unlock() | |
| 136 | + | s.env = jobEnv.Clone() | |
| 137 | + | pairs, envErr := j.evalMap(st.Env, jobEnv) | |
| 138 | + | if envErr == nil { | |
| 139 | + | for _, p := range pairs { | |
| 140 | + | s.env.Set(p.Key, p.Value) | |
| 141 | + | } | |
| 142 | + | } | |
| 143 | + | name := j.displayName(st, s.env) | |
| 144 | + | rep.StepStarted(num, name, j.now()) | |
| 145 | + | ||
| 146 | + | complete := func(r stepResult) { | |
| 147 | + | res := r | |
| 148 | + | j.mu.Lock() | |
| 149 | + | // Update the steps context (ExecutionContext.UpdateGlobalStepsContext). | |
| 150 | + | outcome := res | |
| 151 | + | if s.outcome != nil { | |
| 152 | + | outcome = *s.outcome | |
| 153 | + | } | |
| 154 | + | j.state.SetStepResult(st.ID, string(outcome.conclusion()), string(res.conclusion())) | |
| 155 | + | if res == resFailed { | |
| 156 | + | m := merge(j.result, resFailed) | |
| 157 | + | j.result = &m | |
| 158 | + | j.state.SetStatus(statusName(m)) | |
| 159 | + | } | |
| 160 | + | j.mu.Unlock() | |
| 161 | + | rep.StepCompleted(num, name, res.conclusion(), j.now()) | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | if envErr != nil { | |
| 165 | + | j.errorf(num, "%s", envErr.Error()) | |
| 166 | + | complete(resFailed) | |
| 167 | + | return | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | // Cancellation seen before the step starts (StepsRunner.cs:188-197). | |
| 171 | + | cancelledAtStart := false | |
| 172 | + | select { | |
| 173 | + | case <-j.cancel.Done(): | |
| 174 | + | cancelledAtStart = true | |
| 175 | + | j.mu.Lock() | |
| 176 | + | j.markCancelledLocked() | |
| 177 | + | j.mu.Unlock() | |
| 178 | + | default: | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | // Condition (StepsRunner.cs:200-261). On runner shutdown conditions are not | |
| 182 | + | // evaluated and the step is skipped. | |
| 183 | + | cond := func() (bool, error) { | |
| 184 | + | j.mu.Lock() | |
| 185 | + | defer j.mu.Unlock() | |
| 186 | + | return st.EvaluateIf(context.Background(), j.evalEnv(s.env), j.statusFuncs()) | |
| 187 | + | } | |
| 188 | + | if cancelledAtStart && j.cancel.Reason() == CancelShutdown { | |
| 189 | + | complete(resSkipped) | |
| 190 | + | return | |
| 191 | + | } | |
| 192 | + | ok, err := cond() | |
| 193 | + | switch { | |
| 194 | + | case err != nil: | |
| 195 | + | j.errorf(num, "%s", err.Error()) | |
| 196 | + | complete(resFailed) | |
| 197 | + | return | |
| 198 | + | case !ok: | |
| 199 | + | complete(resSkipped) | |
| 200 | + | return | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // Cancellation while the step runs (StepsRunner.cs:149-186): the job becomes | |
| 204 | + | // cancelled and the step is stopped unless its condition still holds | |
| 205 | + | // (always()). | |
| 206 | + | stepCancel := make(chan struct{}) | |
| 207 | + | stopWatch := make(chan struct{}) | |
| 208 | + | var watch sync.WaitGroup | |
| 209 | + | if !cancelledAtStart { | |
| 210 | + | watch.Add(1) | |
| 211 | + | go func() { | |
| 212 | + | defer watch.Done() | |
| 213 | + | select { | |
| 214 | + | case <-j.cancel.Done(): | |
| 215 | + | case <-stopWatch: | |
| 216 | + | return | |
| 217 | + | } | |
| 218 | + | j.mu.Lock() | |
| 219 | + | j.markCancelledLocked() | |
| 220 | + | j.mu.Unlock() | |
| 221 | + | keep := false | |
| 222 | + | if j.cancel.Reason() != CancelShutdown { | |
| 223 | + | keep, _ = cond() | |
| 224 | + | } | |
| 225 | + | if !keep { | |
| 226 | + | close(stepCancel) | |
| 227 | + | } | |
| 228 | + | }() | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | r := j.execute(s, stepCancel) | |
| 232 | + | close(stopWatch) | |
| 233 | + | watch.Wait() | |
| 234 | + | ||
| 235 | + | // Merge the result of the commands (StepsRunner.cs:347-351). | |
| 236 | + | if s.cmdResult != nil { | |
| 237 | + | m := merge(&r, *s.cmdResult) | |
| 238 | + | r = m | |
| 239 | + | } | |
| 240 | + | // continue-on-error (ExecutionContext.ApplyContinueOnError, :1422). | |
| 241 | + | if r == resFailed && st.ContinueOnError != nil { | |
| 242 | + | coe, cerr := template.EvaluateBool(context.Background(), st.ContinueOnError, j.evalEnv(s.env)) | |
| 243 | + | if cerr != nil { | |
| 244 | + | j.errorf(num, "The step failed and an error occurred when attempting to determine whether to continue on error.") | |
| 245 | + | j.errorf(num, "%s", cerr.Error()) | |
| 246 | + | } | |
| 247 | + | if coe && cerr == nil { | |
| 248 | + | o := resFailed | |
| 249 | + | s.outcome = &o | |
| 250 | + | r = resSucceeded | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | complete(r) | |
| 254 | + | } | |
| 255 | + | ||
| 256 | + | // statusName is the job.status for a result (TaskResult.ToActionResult). | |
| 257 | + | func statusName(r stepResult) string { | |
| 258 | + | switch r { | |
| 259 | + | case resFailed: | |
| 260 | + | return "failure" | |
| 261 | + | case resCanceled: | |
| 262 | + | return "cancelled" | |
| 263 | + | } | |
| 264 | + | return "success" | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | // notSupported is the failure of a step that uses a feature of a later phase. | |
| 268 | + | func notSupported(st *workflow.Step) string { | |
| 269 | + | switch { | |
| 270 | + | case st.Uses != nil: | |
| 271 | + | return fmt.Sprintf("not supported yet: uses (%s)", st.Uses.Value) | |
| 272 | + | case st.TimeoutMinutes != nil: | |
| 273 | + | return "not supported yet: timeout-minutes" | |
| 274 | + | } | |
| 275 | + | return "" | |
| 276 | + | } | |
| @@ -0,0 +1,276 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package executor | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "fmt" | |||
| 8 | + | "os" | |||
| 9 | + | "strings" | |||
| 10 | + | "sync" | |||
| 11 | + | ||||
| 12 | + | "bnema.dev/ermite-actions/template" | |||
| 13 | + | "bnema.dev/ermite-actions/workflow" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // ---------------------------------------------------------------- steps | |||
| 17 | + | ||||
| 18 | + | // stepRun is the state of one step being run; it implements Sink and | |||
| 19 | + | // commandHost. | |||
| 20 | + | type stepRun struct { | |||
| 21 | + | j *job | |||
| 22 | + | num int | |||
| 23 | + | st *workflow.Step | |||
| 24 | + | env *ordered // the step's environment: job env + step env | |||
| 25 | + | cmdResult *stepResult | |||
| 26 | + | outcome *stepResult | |||
| 27 | + | cmds *commandManager | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | func (s *stepRun) Out(text string) { s.j.out(s.num, text) } | |||
| 31 | + | func (s *stepRun) Tagged(tag, msg string) { s.j.tagged(s.num, tag, msg) } | |||
| 32 | + | func (s *stepRun) Debug(msg string) { s.j.debug(s.num, msg) } | |||
| 33 | + | func (s *stepRun) Error(msg string) { s.j.errorf(s.num, "%s", msg) } | |||
| 34 | + | func (s *stepRun) CommandFailed() { f := resFailed; s.cmdResult = &f } | |||
| 35 | + | func (s *stepRun) SetOutput(name, value string) { | |||
| 36 | + | s.j.mu.Lock() | |||
| 37 | + | s.j.state.SetOutput(s.st.ID, name, value) | |||
| 38 | + | s.j.mu.Unlock() | |||
| 39 | + | } | |||
| 40 | + | func (s *stepRun) SaveState(name, value string) {} | |||
| 41 | + | func (s *stepRun) SetEnv(name, value string) { | |||
| 42 | + | s.j.mu.Lock() | |||
| 43 | + | s.j.state.SetEnv(name, value) | |||
| 44 | + | s.j.mu.Unlock() | |||
| 45 | + | } | |||
| 46 | + | func (s *stepRun) AddPath(p string) { | |||
| 47 | + | s.j.mu.Lock() | |||
| 48 | + | s.j.state.AddPath(p) | |||
| 49 | + | s.j.mu.Unlock() | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | // EnvFlag reads a boolean variable of the env context, then of the process | |||
| 53 | + | // (ActionCommandManager.cs:236-246). | |||
| 54 | + | func (s *stepRun) EnvFlag(name string) bool { | |||
| 55 | + | v := strings.ToLower(os.Getenv(name)) | |||
| 56 | + | if v != "true" { | |||
| 57 | + | if e, ok := s.env.Get(name); ok { | |||
| 58 | + | v = strings.ToLower(e) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | return v == "true" | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | func firstLine(contents string) string { | |||
| 65 | + | l := strings.TrimLeft(contents, " \t\r\n") | |||
| 66 | + | if i := strings.IndexAny(l, "\r\n"); i >= 0 { | |||
| 67 | + | l = l[:i] | |||
| 68 | + | } | |||
| 69 | + | return l | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // displayName is the name shown for the step (ActionRunner.GenerateDisplayName, | |||
| 73 | + | // ActionRunner.cs:363): the evaluated name, or "Run " and the first line of | |||
| 74 | + | // the script or the action reference. | |||
| 75 | + | func (j *job) displayName(st *workflow.Step, env *ordered) string { | |||
| 76 | + | tok := st.Name | |||
| 77 | + | prefix := "" | |||
| 78 | + | switch { | |||
| 79 | + | case tok != nil: | |||
| 80 | + | case st.Run != nil: | |||
| 81 | + | tok, prefix = st.Run, "Run " | |||
| 82 | + | case st.Uses != nil: | |||
| 83 | + | tok, prefix = st.Uses, "Run " | |||
| 84 | + | default: | |||
| 85 | + | return "" | |||
| 86 | + | } | |||
| 87 | + | name, err := j.evalString(tok, env) | |||
| 88 | + | if err != nil { | |||
| 89 | + | name = tokenSource(tok) | |||
| 90 | + | } | |||
| 91 | + | return formatStepName(prefix, name) | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func tokenSource(t template.Token) string { | |||
| 95 | + | if be, ok := t.(*template.BasicExpression); ok && be.Source != "" { | |||
| 96 | + | return be.Source | |||
| 97 | + | } | |||
| 98 | + | return t.String() | |||
| 99 | + | } | |||
| 100 | + | ||||
| 101 | + | // formatStepName is ActionRunner.FormatStepName (ActionRunner.cs:447). | |||
| 102 | + | func formatStepName(prefix, name string) string { | |||
| 103 | + | if name == "" { | |||
| 104 | + | return "" | |||
| 105 | + | } | |||
| 106 | + | return prefix + firstLine(name) | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | func (j *job) statusFuncs() workflow.StatusFuncs { | |||
| 110 | + | st := j.state.Status() | |||
| 111 | + | return workflow.StatusFuncs{Success: st == "success", Failure: st == "failure", Cancelled: st == "cancelled"} | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // markCancelled records the cancellation of the job: job result Canceled and | |||
| 115 | + | // job.status cancelled (StepsRunner.cs:152-155 and :190-196). | |||
| 116 | + | func (j *job) markCancelledLocked() { | |||
| 117 | + | if j.cancelled { | |||
| 118 | + | return | |||
| 119 | + | } | |||
| 120 | + | j.cancelled = true | |||
| 121 | + | c := resCanceled | |||
| 122 | + | j.result = &c | |||
| 123 | + | j.state.SetStatus("cancelled") | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | func (j *job) runStep(num int, st *workflow.Step) { | |||
| 127 | + | rep := j.cfg.Reporter | |||
| 128 | + | s := &stepRun{j: j, num: num, st: st} | |||
| 129 | + | s.cmds = newCommandManager(s, j.cfg.Debug) | |||
| 130 | + | ||||
| 131 | + | // Environment of the step (StepsRunner.cs:108-139): job env, then the step's | |||
| 132 | + | // own, evaluated against the job env. | |||
| 133 | + | j.mu.Lock() | |||
| 134 | + | jobEnv := j.state.JobEnv() | |||
| 135 | + | j.mu.Unlock() | |||
| 136 | + | s.env = jobEnv.Clone() | |||
| 137 | + | pairs, envErr := j.evalMap(st.Env, jobEnv) | |||
| 138 | + | if envErr == nil { | |||
| 139 | + | for _, p := range pairs { | |||
| 140 | + | s.env.Set(p.Key, p.Value) | |||
| 141 | + | } | |||
| 142 | + | } | |||
| 143 | + | name := j.displayName(st, s.env) | |||
| 144 | + | rep.StepStarted(num, name, j.now()) | |||
| 145 | + | ||||
| 146 | + | complete := func(r stepResult) { | |||
| 147 | + | res := r | |||
| 148 | + | j.mu.Lock() | |||
| 149 | + | // Update the steps context (ExecutionContext.UpdateGlobalStepsContext). | |||
| 150 | + | outcome := res | |||
| 151 | + | if s.outcome != nil { | |||
| 152 | + | outcome = *s.outcome | |||
| 153 | + | } | |||
| 154 | + | j.state.SetStepResult(st.ID, string(outcome.conclusion()), string(res.conclusion())) | |||
| 155 | + | if res == resFailed { | |||
| 156 | + | m := merge(j.result, resFailed) | |||
| 157 | + | j.result = &m | |||
| 158 | + | j.state.SetStatus(statusName(m)) | |||
| 159 | + | } | |||
| 160 | + | j.mu.Unlock() | |||
| 161 | + | rep.StepCompleted(num, name, res.conclusion(), j.now()) | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | if envErr != nil { | |||
| 165 | + | j.errorf(num, "%s", envErr.Error()) | |||
| 166 | + | complete(resFailed) | |||
| 167 | + | return | |||
| 168 | + | } | |||
| 169 | + | ||||
| 170 | + | // Cancellation seen before the step starts (StepsRunner.cs:188-197). | |||
| 171 | + | cancelledAtStart := false | |||
| 172 | + | select { | |||
| 173 | + | case <-j.cancel.Done(): | |||
| 174 | + | cancelledAtStart = true | |||
| 175 | + | j.mu.Lock() | |||
| 176 | + | j.markCancelledLocked() | |||
| 177 | + | j.mu.Unlock() | |||
| 178 | + | default: | |||
| 179 | + | } | |||
| 180 | + | ||||
| 181 | + | // Condition (StepsRunner.cs:200-261). On runner shutdown conditions are not | |||
| 182 | + | // evaluated and the step is skipped. | |||
| 183 | + | cond := func() (bool, error) { | |||
| 184 | + | j.mu.Lock() | |||
| 185 | + | defer j.mu.Unlock() | |||
| 186 | + | return st.EvaluateIf(context.Background(), j.evalEnv(s.env), j.statusFuncs()) | |||
| 187 | + | } | |||
| 188 | + | if cancelledAtStart && j.cancel.Reason() == CancelShutdown { | |||
| 189 | + | complete(resSkipped) | |||
| 190 | + | return | |||
| 191 | + | } | |||
| 192 | + | ok, err := cond() | |||
| 193 | + | switch { | |||
| 194 | + | case err != nil: | |||
| 195 | + | j.errorf(num, "%s", err.Error()) | |||
| 196 | + | complete(resFailed) | |||
| 197 | + | return | |||
| 198 | + | case !ok: | |||
| 199 | + | complete(resSkipped) | |||
| 200 | + | return | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // Cancellation while the step runs (StepsRunner.cs:149-186): the job becomes | |||
| 204 | + | // cancelled and the step is stopped unless its condition still holds | |||
| 205 | + | // (always()). | |||
| 206 | + | stepCancel := make(chan struct{}) | |||
| 207 | + | stopWatch := make(chan struct{}) | |||
| 208 | + | var watch sync.WaitGroup | |||
| 209 | + | if !cancelledAtStart { | |||
| 210 | + | watch.Add(1) | |||
| 211 | + | go func() { | |||
| 212 | + | defer watch.Done() | |||
| 213 | + | select { | |||
| 214 | + | case <-j.cancel.Done(): | |||
| 215 | + | case <-stopWatch: | |||
| 216 | + | return | |||
| 217 | + | } | |||
| 218 | + | j.mu.Lock() | |||
| 219 | + | j.markCancelledLocked() | |||
| 220 | + | j.mu.Unlock() | |||
| 221 | + | keep := false | |||
| 222 | + | if j.cancel.Reason() != CancelShutdown { | |||
| 223 | + | keep, _ = cond() | |||
| 224 | + | } | |||
| 225 | + | if !keep { | |||
| 226 | + | close(stepCancel) | |||
| 227 | + | } | |||
| 228 | + | }() | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | r := j.execute(s, stepCancel) | |||
| 232 | + | close(stopWatch) | |||
| 233 | + | watch.Wait() | |||
| 234 | + | ||||
| 235 | + | // Merge the result of the commands (StepsRunner.cs:347-351). | |||
| 236 | + | if s.cmdResult != nil { | |||
| 237 | + | m := merge(&r, *s.cmdResult) | |||
| 238 | + | r = m | |||
| 239 | + | } | |||
| 240 | + | // continue-on-error (ExecutionContext.ApplyContinueOnError, :1422). | |||
| 241 | + | if r == resFailed && st.ContinueOnError != nil { | |||
| 242 | + | coe, cerr := template.EvaluateBool(context.Background(), st.ContinueOnError, j.evalEnv(s.env)) | |||
| 243 | + | if cerr != nil { | |||
| 244 | + | j.errorf(num, "The step failed and an error occurred when attempting to determine whether to continue on error.") | |||
| 245 | + | j.errorf(num, "%s", cerr.Error()) | |||
| 246 | + | } | |||
| 247 | + | if coe && cerr == nil { | |||
| 248 | + | o := resFailed | |||
| 249 | + | s.outcome = &o | |||
| 250 | + | r = resSucceeded | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | complete(r) | |||
| 254 | + | } | |||
| 255 | + | ||||
| 256 | + | // statusName is the job.status for a result (TaskResult.ToActionResult). | |||
| 257 | + | func statusName(r stepResult) string { | |||
| 258 | + | switch r { | |||
| 259 | + | case resFailed: | |||
| 260 | + | return "failure" | |||
| 261 | + | case resCanceled: | |||
| 262 | + | return "cancelled" | |||
| 263 | + | } | |||
| 264 | + | return "success" | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | // notSupported is the failure of a step that uses a feature of a later phase. | |||
| 268 | + | func notSupported(st *workflow.Step) string { | |||
| 269 | + | switch { | |||
| 270 | + | case st.Uses != nil: | |||
| 271 | + | return fmt.Sprintf("not supported yet: uses (%s)", st.Uses.Value) | |||
| 272 | + | case st.TimeoutMinutes != nil: | |||
| 273 | + | return "not supported yet: timeout-minutes" | |||
| 274 | + | } | |||
| 275 | + | return "" | |||
| 276 | + | } | |||