1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6 "strings"
7 "testing"
8)
9
10// execCalls are the `docker exec` calls recorded by the fake.
11func (f *fakeCtr) execCalls(t *testing.T) [][]string {
12 var out [][]string
13 for _, c := range f.calls(t) {
14 if c[0] == "exec" {
15 out = append(out, c)
16 }
17 }
18 return out
19}
20
21func TestRunStepThroughDockerExec(t *testing.T) {
22 f := newFakeCtr(t)
23 f.set(t, "exec_out", "hello from the container\n")
24 var work string
25 rec, res := ctrRun(t, f, " container:\n image: ubuntu:24.04\n env:\n CVAR: c\n",
26 " - name: s\n working-directory: sub\n env:\n FOO: bar\n run: echo hi\n", func(c *Config) { work = c.Layout.WorkRoot })
27 if res.Conclusion != ConclusionSuccess {
28 t.Fatalf("%+v\n%s", res, logOf(rec.step(t, "Initialize containers")))
29 }
30 if got := strings.Join(rec.names(), "|"); got != "Set up job|Initialize containers|s|Stop containers|Complete job" {
31 t.Fatalf("steps %s", got)
32 }
33 ex := f.execCalls(t)
34 if len(ex) != 1 {
35 t.Fatalf("exec calls %v", ex)
36 }
37 c := strings.Join(ex[0], "\n")
38 id := strings.Repeat("0", 63) + "1"
39 for _, w := range []string{
40 "exec\n-i\n--workdir\n/__w/hello/hello/sub\n-e\nFOO\n", "\n-e\nGITHUB_WORKSPACE\n", "\n-e\nGITHUB_OUTPUT\n",
41 "\n" + id + "\nsh\n-e\n/__w/_temp/",
42 } {
43 if !strings.Contains(c, w) {
44 t.Errorf("exec lacks %q:\n%s", w, c)
45 }
46 }
47 if strings.Contains(c, "bar") || strings.Contains(c, work) {
48 t.Errorf("values and host paths stay off the command line:\n%s", c)
49 }
50 log := logOf(rec.step(t, "s"))
51 if !strings.Contains(log, "shell: sh -e {0}\n") || !strings.Contains(log, "hello from the container") {
52 t.Errorf("log:\n%s", log)
53 }
54 // The create command of the job container.
55 create := logOf(rec.step(t, "Initialize containers"))
56 for _, w := range []string{
57 "--workdir /__w/hello/hello --network github_network_", " -e \"CVAR=c\" -e \"HOME=/github/home\" -e GITHUB_ACTIONS=true -e CI=true -v \"/var/run/docker.sock\":\"/var/run/docker.sock\" -v \"" + work + "\":\"/__w\"",
58 "\":\"/__e\":ro -v \"" + work + "/_temp\":\"/__w/_temp\"", "\":\"/__t\" -v \"" + work + "/_temp/_github_home\":\"/github/home\" -v \"" + work + "/_temp/_github_workflow\":\"/github/workflow\" --entrypoint \"tail\" ubuntu:24.04 \"-f\" \"/dev/null\"",
59 } {
60 if !strings.Contains(create, w) {
61 t.Errorf("create lacks %q:\n%s", w, create)
62 }
63 }
64}
65
66func TestRunEnvIsTranslatedForTheContainer(t *testing.T) {
67 f := newFakeCtr(t)
68 f.set(t, "exec_env", "1")
69 rec, res := ctrRun(t, f, " container: ubuntu:24.04\n",
70 " - name: s\n run: echo hi\n", nil)
71 if res.Conclusion != ConclusionSuccess {
72 t.Fatalf("%+v %v", res, rec.names())
73 }
74 // HOME is the container's own (/github/home): it is not passed with -e.
75 if c := strings.Join(f.execCalls(t)[0], "\n"); strings.Contains(c, "\nHOME\n") {
76 t.Errorf("HOME must not be passed:\n%s", c)
77 }
78 log := logOf(rec.step(t, "s"))
79 for _, w := range []string{
80 "ENV GITHUB_WORKSPACE=/__w/hello/hello", "RUNNER_TEMP=/__w/_temp",
81 "GITHUB_OUTPUT=/__w/_temp/_runner_file_commands/set_output_", "GITHUB_EVENT_PATH=/github/workflow/event.json",
82 } {
83 if !strings.Contains(log, w) {
84 t.Errorf("log lacks %q:\n%s", w, log)
85 }
86 }
87}
88
89func TestNodeActionInContainerUsesExternalsAndAlpine(t *testing.T) {
90 for _, tc := range []struct{ release, node string }{{"ID=ubuntu\nVERSION_ID=\"24.04\"\n", "node24"}, {"ID=alpine\nVERSION_ID=3.23.4\n", "node24_alpine"}} {
91 f := newFakeCtr(t)
92 f.set(t, "exec_out", tc.release)
93 tree := files{
94 ".github/actions/x/action.yml": "name: x\nruns:\n using: node24\n main: index.js\n",
95 ".github/actions/x/index.js": "x\n",
96 }
97 rec, res := ctrRun(t, f, " container: ubuntu:24.04\n", " - name: a\n uses: ./.github/actions/x\n", func(c *Config) {
98 writeFiles(t, c.Layout.Workspace, tree)
99 })
100 if res.Conclusion != ConclusionSuccess {
101 t.Fatalf("%s: %+v\n%s", tc.node, res, logOf(rec.step(t, "a")))
102 }
103 ex := f.execCalls(t)
104 if len(ex) != 2 {
105 t.Fatalf("%s: exec calls %v", tc.node, ex)
106 }
107 if got := strings.Join(ex[0][len(ex[0])-4:len(ex[0])-1], " "); got != "sh -c "+alpineCheck {
108 t.Errorf("probe: %v", ex[0])
109 }
110 run := strings.Join(ex[1], "\n")
111 if !strings.Contains(run, "\n/__e/"+tc.node+"/bin/node\n") || !strings.Contains(run, "\n/__w/hello/hello/./.github/actions/x/index.js") {
112 t.Errorf("%s: node call:\n%s", tc.node, run)
113 }
114 if got := logOf(rec.step(t, "a")); !strings.Contains(got, "exec "+strings.Repeat("0", 63)+"1 sh -c \"cat /etc/*release | grep ^ID\"") {
115 t.Errorf("probe line missing:\n%s", got)
116 }
117 }
118}
119
120func TestCommandPathsFromTheContainerAreTranslated(t *testing.T) {
121 f := newFakeCtr(t)
122 // The step prints an annotation for a file at its container path.
123 f.set(t, "exec_out", "::error file=/__w/hello/hello/src/a.go,line=3::boom\n")
124 rec, res := ctrRun(t, f, ctrJob, " - name: s\n run: echo\n", nil)
125 if res.Conclusion != ConclusionSuccess {
126 t.Fatalf("%+v", res)
127 }
128 log := logOf(rec.step(t, "s"))
129 if !strings.Contains(log, "##[error]boom") {
130 t.Errorf("log:\n%s", log)
131 }
132}
133
134func TestDockerActionJoinsTheJobNetworkAndContextHasContainerKey(t *testing.T) {
135 f := newFakeCtr(t)
136 rec, res := ctrRun(t, f, " services:\n redis:\n image: redis:7\n",
137 " - uses: docker://alpine:3.20\n - name: ctx\n run: echo \"${{ toJSON(job.container) }}\"\n", nil)
138 if res.Conclusion != ConclusionSuccess {
139 t.Fatalf("%+v %v", res, rec.names())
140 }
141 var run []string
142 for _, c := range f.calls(t) {
143 if c[0] == "run" {
144 run = c
145 }
146 }
147 if run == nil || !strings.Contains(strings.Join(run, "\n"), "\n--network\ngithub_network_") {
148 t.Errorf("docker run without the job network: %v", run)
149 }
150 if got := logOf(rec.step(t, "ctx")); !strings.Contains(got, "\"network\": \"github_network_") {
151 t.Errorf("ctx:\n%s", got)
152 }
153}