detached 67ae5e1

test(executor): opt-in tests against a real Docker daemon

commit
67ae5e1bc237d9d0a1910e392fda7863f6d573fe
author
brice · · 2026-10-14 05:57 UTC
parent
994b869
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +91 -5
A internal/executor/containers_real_test.go +89 -0
@@ -0,0 +1,89 @@
1+// SPDX-License-Identifier: MIT
2+
3+package executor
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "strings"
9+ "testing"
10+ "time"
11+)
12+
13+// Real docker: set ERMITE_RUNNER_IT_DOCKER=1 on a host with a Docker daemon
14+// and the images alpine:3.20 and busybox:latest (no pulls are needed; the
15+// tests do pull, so the network must work otherwise).
16+func realDocker(t *testing.T) string {
17+ t.Helper()
18+ if os.Getenv("ERMITE_RUNNER_IT_DOCKER") != "1" {
19+ t.Skip("set ERMITE_RUNNER_IT_DOCKER=1 (needs a Docker daemon)")
20+ }
21+ p, err := exec.LookPath("docker")
22+ if err != nil {
23+ t.Skip("no docker")
24+ }
25+ return p
26+}
27+
28+func TestRealDockerJobContainerAndService(t *testing.T) {
29+ docker := realDocker(t)
30+ needTools(t, "bash")
31+ yaml := ` container: alpine:3.20
32+ services:
33+ web:
34+ image: busybox:latest
35+ options: >-
36+ --health-cmd "true" --health-interval 1s --health-retries 3
37+ --entrypoint sh
38+ command: -c "sleep 300"
39+ steps:
40+ - name: where
41+ run: |
42+ echo "pwd=$PWD"
43+ echo "id=$(id -u)"
44+ echo "home=$HOME"
45+ echo "temp=$RUNNER_TEMP"
46+ ls -d /__w /__t /__e /github/home /github/workflow
47+ echo "hi from alpine" >> "$GITHUB_STEP_SUMMARY"
48+ echo "k=v" >> "$GITHUB_OUTPUT"
49+ - name: svc
50+ run: ping -c1 web >/dev/null && echo "web resolves"
51+`
52+ rec, res := runLocal(t, wfHead+yaml, "j", func(c *Config) {
53+ c.Actions.DockerPath = docker
54+ c.BaseEnv = append(c.BaseEnv, "ACTIONS_SERVICE_CONTAINER_COMMAND=true", "DOCKER_HOST="+os.Getenv("DOCKER_HOST"), "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR"))
55+ c.Actions.ExternalsDir = fakeNode(t)
56+ })
57+ log := logOf(rec.step(t, "where")) + logOf(rec.step(t, "svc"))
58+ if res.Conclusion != ConclusionSuccess {
59+ t.Fatalf("%+v\n%s\n%s", res, logOf(rec.step(t, "Initialize containers")), log)
60+ }
61+ for _, w := range []string{"pwd=/__w/hello/hello", "home=/github/home", "temp=/__w/_temp", "/github/workflow", "web resolves"} {
62+ if !strings.Contains(log, w) {
63+ t.Errorf("lacks %q:\n%s", w, log)
64+ }
65+ }
66+ if out, _ := exec.Command(docker, "ps", "-a", "--format", "{{.Names}}", "--filter", "name=busybox").Output(); strings.TrimSpace(string(out)) != "" {
67+ t.Errorf("containers left: %s", out)
68+ }
69+ if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" {
70+ t.Errorf("networks left: %s", out)
71+ }
72+}
73+
74+func TestRealDockerCancelRemovesEverything(t *testing.T) {
75+ docker := realDocker(t)
76+ needTools(t, "bash")
77+ cancel := NewCanceller()
78+ go func() { time.Sleep(6 * time.Second); cancel.Cancel(CancelUser) }()
79+ rec, res := runLocalC(t, cancel, wfHead+" container: alpine:3.20\n steps:\n - name: long\n run: sleep 120\n", "j", func(c *Config) {
80+ c.Actions.DockerPath = docker
81+ c.BaseEnv = append(c.BaseEnv, "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR"))
82+ })
83+ if res.Conclusion != ConclusionCancelled {
84+ t.Fatalf("%+v %v", res, rec.names())
85+ }
86+ if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" {
87+ t.Errorf("networks left: %s", out)
88+ }
89+}
@@ -0,0 +1,89 @@
1+// SPDX-License-Identifier: MIT
2+
3+package executor
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "strings"
9+ "testing"
10+ "time"
11+)
12+
13+// Real docker: set ERMITE_RUNNER_IT_DOCKER=1 on a host with a Docker daemon
14+// and the images alpine:3.20 and busybox:latest (no pulls are needed; the
15+// tests do pull, so the network must work otherwise).
16+func realDocker(t *testing.T) string {
17+ t.Helper()
18+ if os.Getenv("ERMITE_RUNNER_IT_DOCKER") != "1" {
19+ t.Skip("set ERMITE_RUNNER_IT_DOCKER=1 (needs a Docker daemon)")
20+ }
21+ p, err := exec.LookPath("docker")
22+ if err != nil {
23+ t.Skip("no docker")
24+ }
25+ return p
26+}
27+
28+func TestRealDockerJobContainerAndService(t *testing.T) {
29+ docker := realDocker(t)
30+ needTools(t, "bash")
31+ yaml := ` container: alpine:3.20
32+ services:
33+ web:
34+ image: busybox:latest
35+ options: >-
36+ --health-cmd "true" --health-interval 1s --health-retries 3
37+ --entrypoint sh
38+ command: -c "sleep 300"
39+ steps:
40+ - name: where
41+ run: |
42+ echo "pwd=$PWD"
43+ echo "id=$(id -u)"
44+ echo "home=$HOME"
45+ echo "temp=$RUNNER_TEMP"
46+ ls -d /__w /__t /__e /github/home /github/workflow
47+ echo "hi from alpine" >> "$GITHUB_STEP_SUMMARY"
48+ echo "k=v" >> "$GITHUB_OUTPUT"
49+ - name: svc
50+ run: ping -c1 web >/dev/null && echo "web resolves"
51+`
52+ rec, res := runLocal(t, wfHead+yaml, "j", func(c *Config) {
53+ c.Actions.DockerPath = docker
54+ c.BaseEnv = append(c.BaseEnv, "ACTIONS_SERVICE_CONTAINER_COMMAND=true", "DOCKER_HOST="+os.Getenv("DOCKER_HOST"), "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR"))
55+ c.Actions.ExternalsDir = fakeNode(t)
56+ })
57+ log := logOf(rec.step(t, "where")) + logOf(rec.step(t, "svc"))
58+ if res.Conclusion != ConclusionSuccess {
59+ t.Fatalf("%+v\n%s\n%s", res, logOf(rec.step(t, "Initialize containers")), log)
60+ }
61+ for _, w := range []string{"pwd=/__w/hello/hello", "home=/github/home", "temp=/__w/_temp", "/github/workflow", "web resolves"} {
62+ if !strings.Contains(log, w) {
63+ t.Errorf("lacks %q:\n%s", w, log)
64+ }
65+ }
66+ if out, _ := exec.Command(docker, "ps", "-a", "--format", "{{.Names}}", "--filter", "name=busybox").Output(); strings.TrimSpace(string(out)) != "" {
67+ t.Errorf("containers left: %s", out)
68+ }
69+ if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" {
70+ t.Errorf("networks left: %s", out)
71+ }
72+}
73+
74+func TestRealDockerCancelRemovesEverything(t *testing.T) {
75+ docker := realDocker(t)
76+ needTools(t, "bash")
77+ cancel := NewCanceller()
78+ go func() { time.Sleep(6 * time.Second); cancel.Cancel(CancelUser) }()
79+ rec, res := runLocalC(t, cancel, wfHead+" container: alpine:3.20\n steps:\n - name: long\n run: sleep 120\n", "j", func(c *Config) {
80+ c.Actions.DockerPath = docker
81+ c.BaseEnv = append(c.BaseEnv, "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR"))
82+ })
83+ if res.Conclusion != ConclusionCancelled {
84+ t.Fatalf("%+v %v", res, rec.names())
85+ }
86+ if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" {
87+ t.Errorf("networks left: %s", out)
88+ }
89+}
M internal/executor/containers_spec.go +0 -4
@@ -299,10 +299,6 @@ func (j *job) convertContainer(v expr.Value, service bool) (*containerSpec, erro
299299 if spec.image == "" {
300300 return nil, nil
301301 }
302- // The image is one argument of docker; a leading "-" would make it an option.
303- if strings.HasPrefix(spec.image, "-") {
304- return nil, ghErrorf("Invalid container image '%s'", spec.image)
305- }
306302 return spec, nil
307303 }
308304
@@ -299,10 +299,6 @@ func (j *job) convertContainer(v expr.Value, service bool) (*containerSpec, erro
299 if spec.image == "" {299 if spec.image == "" {
300 return nil, nil300 return nil, nil
301 }301 }
302- // The image is one argument of docker; a leading "-" would make it an option.
303- if strings.HasPrefix(spec.image, "-") {
304- return nil, ghErrorf("Invalid container image '%s'", spec.image)
305- }
306 return spec, nil302 return spec, nil
307 }303 }
308 304
M internal/executor/containers_spec_test.go +1 -1
@@ -81,7 +81,7 @@ func TestConvertContainerForms(t *testing.T) {
8181 if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil {
8282 t.Errorf("empty image: %v %v", s, err)
8383 }
84- for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`, `"-v"`} {
84+ for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`} {
8585 if _, err := j.convertContainer(mustValue(t, bad), false); err == nil {
8686 t.Errorf("%s accepted", bad)
8787 }
@@ -81,7 +81,7 @@ func TestConvertContainerForms(t *testing.T) {
81 if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil {81 if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil {
82 t.Errorf("empty image: %v %v", s, err)82 t.Errorf("empty image: %v %v", s, err)
83 }83 }
84- for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`, `"-v"`} {84+ for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`} {
85 if _, err := j.convertContainer(mustValue(t, bad), false); err == nil {85 if _, err := j.convertContainer(mustValue(t, bad), false); err == nil {
86 t.Errorf("%s accepted", bad)86 t.Errorf("%s accepted", bad)
87 }87 }
M internal/executor/job.go +1 -0
@@ -509,6 +509,7 @@ func (j *job) setUp() ([]qstep, error) {
509509 // JobExtension.cs:250-275: the job container and the services are
510510 // evaluated after the job environment, before the actions are prepared.
511511 j.debug(1, "Evaluating job container")
512+ j.debug(1, "Evaluating job service containers")
512513 jc, svcs, err := j.evalContainers()
513514 if err != nil {
514515 return nil, err
@@ -509,6 +509,7 @@ func (j *job) setUp() ([]qstep, error) {
509 // JobExtension.cs:250-275: the job container and the services are509 // JobExtension.cs:250-275: the job container and the services are
510 // evaluated after the job environment, before the actions are prepared.510 // evaluated after the job environment, before the actions are prepared.
511 j.debug(1, "Evaluating job container")511 j.debug(1, "Evaluating job container")
512+ j.debug(1, "Evaluating job service containers")
512 jc, svcs, err := j.evalContainers()513 jc, svcs, err := j.evalContainers()
513 if err != nil {514 if err != nil {
514 return nil, err515 return nil, err

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