1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "fmt"
7 "path/filepath"
8 "time"
9)
10
11const (
12 pullAttempts = 3
13 buildAttempts = 3
14)
15
16// setupSteps are the steps that pull and build the images of the actions, as
17// JobExtensionRunner steps before the first one of the job
18// (ActionManager.PrepareActionsAsync, ActionManager.cs:134-166): the pulls in
19// the order the images were met, then the builds. They run if success().
20func (j *job) setupSteps() []qstep {
21 st := j.actions
22 var out []qstep
23 for _, img := range st.pullOrder {
24 img, users := img, st.pullUsers[img]
25 out = append(out, qstep{name: "Pull " + img, setup: func(s *stepRun, cancel <-chan struct{}) stepResult {
26 return j.pullImage(s, img, users, cancel)
27 }})
28 }
29 for _, repo := range st.buildOrder {
30 users, info := st.buildUsers[repo], st.buildInfo[repo]
31 out = append(out, qstep{name: "Build " + info.repository, setup: func(s *stepRun, cancel <-chan struct{}) stepResult {
32 return j.buildActionImage(s, info, users, cancel)
33 }})
34 }
35 return out
36}
37
38// pullImage is ActionManager.PullActionContainerAsync (ActionManager.cs:803-846).
39func (j *job) pullImage(s *stepRun, image string, users []*node, cancel <-chan struct{}) stepResult {
40 num := s.num
41 j.out(num, fmt.Sprintf("##[group]Pull down action image '%s'", image))
42 code := 0
43 for try := 1; try <= pullAttempts; try++ {
44 c := j.newDockerCmd("pull", raw(image))
45 var err error
46 code, err = j.runDockerCmd(s, c, dirOrCwd(j.state.GitHub("workspace")), nil, cancel)
47 if err == errCancelled {
48 j.out(num, "##[endgroup]")
49 return j.cancelledStep(s)
50 }
51 if err != nil {
52 j.out(num, "##[endgroup]")
53 j.errorf(num, "%s", err.Error())
54 return resFailed
55 }
56 if code == 0 {
57 break
58 }
59 if try < pullAttempts {
60 d := j.pullBackoff()
61 j.tagged(num, "##[warning]", fmt.Sprintf("Docker pull failed with exit code %d, back off %v seconds before retry.", code, d.Seconds()))
62 if !j.sleepOrCancel(d, cancel) {
63 j.out(num, "##[endgroup]")
64 return j.cancelledStep(s)
65 }
66 }
67 }
68 j.out(num, "##[endgroup]")
69 if code != 0 {
70 j.errorf(num, "Docker pull failed with exit code %d", code)
71 return resFailed
72 }
73 for _, n := range users {
74 n.image = image
75 }
76 return resSucceeded
77}
78
79// buildActionImage is ActionManager.BuildActionContainerAsync (:848-910).
80func (j *job) buildActionImage(s *stepRun, info buildInfo, users []*node, cancel <-chan struct{}) stepResult {
81 num := s.num
82 j.out(num, fmt.Sprintf("##[group]Build container for action use: '%s'.", info.dockerfile))
83 tag := newImageTag(j.dockerLabel())
84 code := 0
85 for try := 1; try <= buildAttempts; try++ {
86 // Directory.GetParent(Dockerfile).FullName: the parent after Path.GetFullPath,
87 // which folds "." and ".." lexically, as filepath.Dir does; the Dockerfile
88 // itself is passed as written.
89 c := j.dockerBuildCmd(tag, info.dockerfile, filepath.Dir(info.dockerfile))
90 var err error
91 code, err = j.runDockerCmd(s, c, dirOrCwd(info.workDir), nil, cancel)
92 if err == errCancelled {
93 j.out(num, "##[endgroup]")
94 return j.cancelledStep(s)
95 }
96 if err != nil {
97 j.out(num, "##[endgroup]")
98 j.errorf(num, "%s", err.Error())
99 return resFailed
100 }
101 if code == 0 {
102 break
103 }
104 if try < buildAttempts {
105 d := j.pullBackoff()
106 j.tagged(num, "##[warning]", fmt.Sprintf("Docker build failed with exit code %d, back off %v seconds before retry.", code, d.Seconds()))
107 if !j.sleepOrCancel(d, cancel) {
108 j.out(num, "##[endgroup]")
109 return j.cancelledStep(s)
110 }
111 }
112 }
113 j.out(num, "##[endgroup]")
114 if code != 0 {
115 j.errorf(num, "Docker build failed with exit code %d", code)
116 return resFailed
117 }
118 for _, n := range users {
119 n.image = tag
120 }
121 return resSucceeded
122}
123
124// pullBackoff is a random wait of 1 to 10 seconds.
125func (j *job) pullBackoff() time.Duration {
126 if f := j.cfg.Actions.Backoff; f != nil {
127 return f()
128 }
129 return time.Duration(1+time.Now().UnixNano()%10) * time.Second
130}
131
132func (j *job) sleepOrCancel(d time.Duration, cancel <-chan struct{}) bool {
133 select {
134 case <-time.After(d):
135 return true
136 case <-cancel:
137 return false
138 }
139}