v0.7.0 45bfc5b

cli_run_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6	"strings"
7	"testing"
8
9	"connectrpc.com/connect"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	pb "bnema.dev/ermite-api/gen/ermite"
14	"bnema.dev/ermite/internal/domain"
15)
16
17// The CLI over a real server: `run list|view|watch|cancel|rerun`, `runner
18// list|remove`, and the runner token preset.
19func TestCLIRunsAndRunners(t *testing.T) {
20	ci := newCIHarness(t, nil)
21	h := &cliHarness{harness: ci.harness, url: ci.srv.URL}
22	runner := ci.registerRunner("alpha", "ubuntu-latest")
23	ci.push("main", map[string]string{".github/workflows/ci.yml": ciWorkflow}, "")
24	ci.waitFor("a run", func() bool { return len(ci.runs()) == 1 })
25
26	job := runner.await()
27	runner.runJob(job, pb.CiConclusion_CI_CONCLUSION_SUCCESS, nil, "hello from the job")
28	next := runner.await()
29	runner.runJob(next, pb.CiConclusion_CI_CONCLUSION_FAILURE, nil, "this one failed")
30	ci.waitFor("the run completes", func() bool { return ci.runs()[0].GetStatus() == pb.CiStatus_CI_STATUS_COMPLETED })
31	id := "1"
32
33	// list
34	out := h.cli(t, adminToken, "run", "list", "proj")
35	require.Empty(t, out.err, out.stderr)
36	assert.Contains(t, out.stdout, "RESULT")
37	assert.Contains(t, out.stdout, "failure")
38	assert.Contains(t, out.stdout, "ci")
39
40	js := h.cli(t, adminToken, "run", "list", "proj", "--json")
41	require.NoError(t, js.err)
42	runs := decode[[]jsonRun](t, js.stdout)
43	require.Len(t, runs, 1)
44	assert.Equal(t, "failure", runs[0].Conclusion)
45	assert.Equal(t, "completed", runs[0].Status)
46	assert.Equal(t, "refs/heads/main", runs[0].Ref)
47	assert.Equal(t, []string{"actor", "attempt", "completed_at", "conclusion", "created_at", "error", "event", "id", "number", "ref", "repo", "sha", "started_at", "status", "title", "workflow", "workflow_path"}, keys(t, strings.TrimSpace(strings.Trim(strings.TrimSpace(js.stdout), "[]"))))
48
49	none := h.cli(t, adminToken, "run", "list", "proj", "--status", "queued")
50	require.NoError(t, none.err)
51	assert.Contains(t, none.stderr, "no runs")
52	bad := h.cli(t, adminToken, "run", "list", "proj", "--status", "weird")
53	require.Error(t, bad.err)
54
55	// view
56	view := h.cli(t, adminToken, "run", "view", "proj", id)
57	require.NoError(t, view.err, view.stderr)
58	assert.Contains(t, view.stdout, "result")
59	assert.Contains(t, view.stdout, "build")
60	assert.Contains(t, view.stdout, "[alpha]")
61	assert.Contains(t, view.stdout, "Set up job")
62
63	vjs := h.cli(t, adminToken, "run", "view", "proj", id, "--json")
64	detail := decode[jsonRunDetail](t, vjs.stdout)
65	require.Len(t, detail.Jobs, 2)
66	assert.Equal(t, "build", detail.Jobs[0].Key)
67	assert.Equal(t, "vm", detail.Jobs[0].Isolation)
68	assert.Equal(t, int32(1), detail.LatestAttempt)
69
70	// --log, all jobs and one job
71	all := h.cli(t, adminToken, "run", "view", "proj", id, "--log")
72	require.NoError(t, all.err, all.stderr)
73	assert.Contains(t, all.stdout, "== build ==")
74	assert.Contains(t, all.stdout, "hello from the job")
75	assert.Contains(t, all.stdout, "this one failed")
76	one := h.cli(t, adminToken, "run", "view", "proj", id, "--log", "--job", "test")
77	require.NoError(t, one.err)
78	assert.Contains(t, one.stdout, "this one failed")
79	assert.NotContains(t, one.stdout, "hello from the job")
80	missing := h.cli(t, adminToken, "run", "view", "proj", id, "--log", "--job", "nope")
81	require.Error(t, missing.err)
82	assert.Contains(t, missing.err.Error(), `no job "nope"`)
83
84	// watch: a completed failing run prints once and exits with an error.
85	w := h.cli(t, adminToken, "run", "watch", "proj", id)
86	require.Error(t, w.err)
87	assert.Contains(t, w.err.Error(), "failure")
88	assert.Contains(t, w.stdout, "build success")
89
90	// rerun --failed, then cancel the new attempt.
91	re := h.cli(t, adminToken, "run", "rerun", "proj", id, "--failed")
92	require.NoError(t, re.err, re.stderr)
93	assert.Contains(t, re.stderr, "attempt 2 started")
94	ca := h.cli(t, adminToken, "run", "cancel", "proj", id, "--json")
95	require.NoError(t, ca.err, ca.stderr)
96	assert.Equal(t, int32(2), decode[jsonRun](t, ca.stdout).Attempt)
97	// No runner holds a job of attempt 2, so the cancel ended it at once: a
98	// completed run cannot be cancelled again.
99	again := h.cli(t, adminToken, "run", "cancel", "proj", id)
100	require.Error(t, again.err)
101	assert.Equal(t, connect.CodeFailedPrecondition, connect.CodeOf(again.err))
102
103	// A reader may look but not cancel or rerun.
104	reader := h.token(scopes(domain.ScopeRepoRead))
105	assert.NoError(t, h.cli(t, reader, "run", "view", "proj", id).err)
106	denied := h.cli(t, reader, "run", "cancel", "proj", id)
107	require.Error(t, denied.err)
108	assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(denied.err))
109	assert.Error(t, h.cli(t, adminToken, "run", "view", "proj", "abc").err)
110
111	// runners
112	rl := h.cli(t, adminToken, "runner", "list")
113	require.NoError(t, rl.err, rl.stderr)
114	assert.Contains(t, rl.stdout, "alpha")
115	assert.Contains(t, rl.stdout, "vm")
116	rjs := decode[[]jsonRunner](t, h.cli(t, adminToken, "runner", "list", "--json").stdout)
117	require.Len(t, rjs, 1)
118	assert.Equal(t, []string{"linux", "self-hosted", "ubuntu-latest", "x64"}, rjs[0].Labels)
119	rm := h.cli(t, adminToken, "runner", "remove", rjs[0].ID)
120	require.NoError(t, rm.err, rm.stderr)
121	assert.Error(t, h.cli(t, adminToken, "runner", "remove", rjs[0].ID).err, "already removed")
122	assert.Contains(t, h.cli(t, adminToken, "runner", "list").stderr, "no runners")
123
124	// The runner token preset makes a token that registers runners and nothing
125	// else.
126	tok := h.cli(t, adminToken, "token", "create", "runner host", "--preset", "runner", "--json")
127	require.NoError(t, tok.err, tok.stderr)
128	created := decode[jsonCreatedToken](t, tok.stdout)
129	assert.Equal(t, []string{"runner:register"}, created.Scopes)
130	assert.NoError(t, h.cli(t, created.Secret, "runner", "list").err)
131	assert.Error(t, h.cli(t, created.Secret, "run", "cancel", "proj", id).err)
132}

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 3

Tags 44

↑↓ moveenter openesc close