v0.4.4 edcdd4c

actions_cancel_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package executor
4
5import (
6	"context"
7	"net/http"
8	"net/http/httptest"
9	"os"
10	"path/filepath"
11	"strings"
12	"sync"
13	"testing"
14	"time"
15)
16
17// GO-006: the image setup of a local composite action (pull, retry sleep) stops
18// with the step.
19func TestLocalCompositeSetupIsCancellable(t *testing.T) {
20	needTools(t, "bash")
21	dir := t.TempDir()
22	pulls := filepath.Join(dir, "pulls")
23	script := "#!/bin/sh\necho x >> " + pulls + "\necho ready\nexit 1\n"
24	if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil {
25		t.Fatal(err)
26	}
27	c := NewCanceller()
28	started := make(chan struct{})
29	go func() {
30		<-started
31		c.Cancel(CancelUser)
32	}()
33	home := t.TempDir()
34	layout := NewLayout(home, "hello", home+"/tc")
35	writeFiles(t, layout.Workspace, files{"c/action.yml": "name: c\nruns:\n  using: composite\n  steps:\n    - uses: docker://alpine:3\n"})
36	rec := newRecorder()
37	rep := &signalReporter{recorder: rec, started: started, match: "ready"}
38	done := make(chan Result, 1)
39	go func() {
40		done <- Run(c, Config{
41			WorkflowYAML: []byte(wfHead + "    steps:\n      - uses: ./c\n      - if: always()\n        run: echo cleanup-ran\n"),
42			JobKey:       "j", ContextsJSON: testGitHubContext("j"), Layout: &layout, Reporter: rep,
43			BaseEnv:   []string{"PATH=" + os.Getenv("PATH"), "HOME=" + home},
44			TermAfter: 200 * time.Millisecond, KillAfter: 200 * time.Millisecond, WaitDelay: 500 * time.Millisecond,
45			Actions: ActionsConfig{DockerPath: filepath.Join(dir, "docker"), Backoff: func() time.Duration { return time.Hour }},
46		})
47	}()
48	select {
49	case res := <-done:
50		if res.Conclusion != ConclusionCancelled {
51			t.Fatalf("%+v", res)
52		}
53	case <-time.After(20 * time.Second):
54		t.Fatal("the retry sleep of the pull ignored the cancellation")
55	}
56	if b, _ := os.ReadFile(pulls); strings.Count(string(b), "x") != 1 {
57		t.Errorf("no retry after the cancellation: %q", b)
58	}
59	if got := rec.step(t, "Run ./c").Conclusion; got != ConclusionCancelled {
60		t.Errorf("step %s", got)
61	}
62	if got := rec.step(t, "Run echo cleanup-ran").Conclusion; got != ConclusionSuccess {
63		t.Errorf("always() step %s", got)
64	}
65}
66
67// GO-014: what the timeout or the cancellation of a step breaks while a local
68// composite action is prepared (a resolution, a download, an image pull and its
69// retry sleep) ends the step as the cancellation of the step does: the timeout
70// message and a failed step for timeout-minutes, a cancelled step for a user
71// cancel, and no stray "The operation was canceled.".
72func TestPreparationInterruptedByTheStepTimeout(t *testing.T) {
73	skew := -(60*time.Second - 1500*time.Millisecond)
74	leaf := files{"c/action.yml": "name: c\nruns:\n  using: composite\n  steps:\n    - uses: o/leaf@v1\n"}
75	docker := files{"c/action.yml": "name: c\nruns:\n  using: composite\n  steps:\n    - uses: docker://alpine:3\n"}
76	hang := make(chan struct{})
77	slow := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
78		select {
79		case <-r.Context().Done():
80		case <-hang:
81		}
82	}))
83	defer slow.Close()
84	defer close(hang)
85	failDocker := filepath.Join(t.TempDir(), "docker")
86	writeFiles(t, filepath.Dir(failDocker), files{"docker": "#!/bin/sh\necho pull failed\nexit 1\n"})
87
88	for name, tc := range map[string]struct {
89		tree  files
90		tweak func(*Config)
91	}{
92		"blocked resolver": {leaf, func(c *Config) {
93			c.Actions.Resolver = func(ctx context.Context, _ []string) ([]ResolvedAction, error) {
94				<-ctx.Done()
95				return nil, ctx.Err()
96			}
97		}},
98		"blocked download": {leaf, func(c *Config) {
99			c.Actions.Resolver = func(context.Context, []string) ([]ResolvedAction, error) {
100				return []ResolvedAction{{Ref: "o/leaf@v1", NameWithOwner: "o/leaf", ResolvedSHA: strings.Repeat("a", 40), TarballURL: slow.URL + "/t"}}, nil
101			}
102		}},
103		"docker pull retry sleep": {docker, func(c *Config) {
104			c.Actions.DockerPath = failDocker
105			c.Actions.Backoff = func() time.Duration { return time.Hour }
106		}},
107	} {
108		t.Run(name, func(t *testing.T) {
109			start := time.Now()
110			rr, res := actionResults(t, tc.tree, "      - name: comp\n        timeout-minutes: 1\n        uses: ./c\n      - name: after\n        if: always()\n        run: echo after-ran\n", func(c *Config) {
111				c.StepTimeoutSkew = skew
112				tc.tweak(c)
113			})
114			if time.Since(start) > 15*time.Second {
115				t.Fatalf("the preparation was not interrupted: %v", time.Since(start))
116			}
117			comp := rr.step(t, "comp")
118			log := logOf(comp)
119			if res.Conclusion != ConclusionFailure || comp.Conclusion != ConclusionFailure {
120				t.Fatalf("job %s, step %s\n%s", res.Conclusion, comp.Conclusion, log)
121			}
122			if !strings.Contains(log, "##[error]The action 'comp' has timed out after 1 minutes.") {
123				t.Errorf("no timeout message:\n%s", log)
124			}
125			if strings.Contains(log, "The operation was canceled.") {
126				t.Errorf("a stray cancel text:\n%s", log)
127			}
128			if rr.step(t, "after").Conclusion != ConclusionSuccess {
129				t.Error("the job is not cancelled by a step timeout")
130			}
131		})
132	}
133}
134
135func TestPreparationInterruptedByAUserCancel(t *testing.T) {
136	needTools(t, "bash")
137	entered := make(chan struct{})
138	var once sync.Once
139	home := t.TempDir()
140	layout := NewLayout(home, "hello", home+"/tc")
141	writeFiles(t, layout.Workspace, files{"c/action.yml": "name: c\nruns:\n  using: composite\n  steps:\n    - uses: o/leaf@v1\n"})
142	c := NewCanceller()
143	go func() {
144		<-entered
145		c.Cancel(CancelUser)
146	}()
147	rec := newRecorder()
148	done := make(chan Result, 1)
149	go func() {
150		done <- Run(c, Config{
151			WorkflowYAML: []byte(wfHead + "    steps:\n      - name: comp\n        uses: ./c\n      - name: cleanup\n        if: always()\n        run: echo cleanup-ran\n"),
152			JobKey:       "j", ContextsJSON: testGitHubContext("j"), Layout: &layout, Reporter: rec,
153			BaseEnv:   []string{"PATH=" + os.Getenv("PATH"), "HOME=" + home},
154			TermAfter: 200 * time.Millisecond, KillAfter: 200 * time.Millisecond, WaitDelay: 500 * time.Millisecond,
155			Actions: ActionsConfig{Resolver: func(ctx context.Context, _ []string) ([]ResolvedAction, error) {
156				once.Do(func() { close(entered) })
157				<-ctx.Done()
158				return nil, ctx.Err()
159			}},
160		})
161	}()
162	select {
163	case res := <-done:
164		if res.Conclusion != ConclusionCancelled {
165			t.Fatalf("%+v", res)
166		}
167	case <-time.After(20 * time.Second):
168		t.Fatal("the resolution ignored the cancellation")
169	}
170	if got := rec.step(t, "comp").Conclusion; got != ConclusionCancelled {
171		t.Errorf("step %s, want cancelled", got)
172	}
173	if got := rec.step(t, "cleanup").Conclusion; got != ConclusionSuccess {
174		t.Errorf("always() step %s", got)
175	}
176}
177
178// A genuine error of the preparation still fails the step.
179func TestPreparationErrorStillFailsTheStep(t *testing.T) {
180	tree := files{"c/action.yml": "name: c\nruns:\n  using: composite\n  steps:\n    - uses: o/leaf@v1\n"}
181	rr, res := actionResults(t, tree, "      - name: comp\n        uses: ./c\n", func(c *Config) {
182		c.Actions.Resolver = func(context.Context, []string) ([]ResolvedAction, error) {
183			return []ResolvedAction{{Ref: "o/leaf@v1", Error: "Unable to resolve action `o/leaf@v1`, repository not found: `o/leaf`"}}, nil
184		}
185	})
186	comp := rr.step(t, "comp")
187	if res.Conclusion != ConclusionFailure || comp.Conclusion != ConclusionFailure || !strings.Contains(logOf(comp), "##[error]Unable to resolve action `o/leaf@v1`, repository not found: `o/leaf`") {
188		t.Errorf("%s %s\n%s", res.Conclusion, comp.Conclusion, logOf(comp))
189	}
190}

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