v0.12.1 7328df5

ci_convert.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6	"fmt"
7
8	pb "bnema.dev/ermite-api/gen/ermite"
9	"bnema.dev/ermite/internal/domain"
10	"bnema.dev/ermite/internal/ports"
11)
12
13func statusToPB(s domain.CIStatus) pb.CiStatus {
14	switch s {
15	case domain.CIQueued:
16		return pb.CiStatus_CI_STATUS_QUEUED
17	case domain.CIInProgress:
18		return pb.CiStatus_CI_STATUS_IN_PROGRESS
19	case domain.CICompleted:
20		return pb.CiStatus_CI_STATUS_COMPLETED
21	}
22	return pb.CiStatus_CI_STATUS_UNSPECIFIED
23}
24
25func statusFromPB(s pb.CiStatus) (domain.CIStatus, error) {
26	switch s {
27	case pb.CiStatus_CI_STATUS_UNSPECIFIED:
28		return "", nil
29	case pb.CiStatus_CI_STATUS_QUEUED:
30		return domain.CIQueued, nil
31	case pb.CiStatus_CI_STATUS_IN_PROGRESS:
32		return domain.CIInProgress, nil
33	case pb.CiStatus_CI_STATUS_COMPLETED:
34		return domain.CICompleted, nil
35	}
36	return "", fmt.Errorf("%w: status %v", domain.ErrInvalid, s)
37}
38
39var conclusionPairs = []struct {
40	d domain.CIConclusion
41	p pb.CiConclusion
42}{
43	{domain.CISuccess, pb.CiConclusion_CI_CONCLUSION_SUCCESS},
44	{domain.CIFailure, pb.CiConclusion_CI_CONCLUSION_FAILURE},
45	{domain.CICancelled, pb.CiConclusion_CI_CONCLUSION_CANCELLED},
46	{domain.CISkipped, pb.CiConclusion_CI_CONCLUSION_SKIPPED},
47	{domain.CITimedOut, pb.CiConclusion_CI_CONCLUSION_TIMED_OUT},
48	{domain.CINeutral, pb.CiConclusion_CI_CONCLUSION_NEUTRAL},
49	{domain.CIActionRequired, pb.CiConclusion_CI_CONCLUSION_ACTION_REQUIRED},
50}
51
52func conclusionToPB(c domain.CIConclusion) pb.CiConclusion {
53	for _, e := range conclusionPairs {
54		if e.d == c {
55			return e.p
56		}
57	}
58	return pb.CiConclusion_CI_CONCLUSION_UNSPECIFIED
59}
60
61func conclusionFromPB(c pb.CiConclusion) (domain.CIConclusion, error) {
62	if c == pb.CiConclusion_CI_CONCLUSION_UNSPECIFIED {
63		return domain.CINoConclusion, nil
64	}
65	for _, e := range conclusionPairs {
66		if e.p == c {
67			return e.d, nil
68		}
69	}
70	return "", fmt.Errorf("%w: conclusion %v", domain.ErrInvalid, c)
71}
72
73func isolationToPB(i domain.CIIsolation) pb.RunnerIsolation {
74	switch i {
75	case domain.CIIsolationVM:
76		return pb.RunnerIsolation_RUNNER_ISOLATION_VM
77	case domain.CIIsolationContainerSysbox:
78		return pb.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX
79	case domain.CIIsolationContainerPrivileged:
80		return pb.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED
81	}
82	return pb.RunnerIsolation_RUNNER_ISOLATION_UNSPECIFIED
83}
84
85func isolationFromPB(i pb.RunnerIsolation) (domain.CIIsolation, error) {
86	switch i {
87	case pb.RunnerIsolation_RUNNER_ISOLATION_VM:
88		return domain.CIIsolationVM, nil
89	case pb.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_SYSBOX:
90		return domain.CIIsolationContainerSysbox, nil
91	case pb.RunnerIsolation_RUNNER_ISOLATION_CONTAINER_PRIVILEGED:
92		return domain.CIIsolationContainerPrivileged, nil
93	}
94	return "", fmt.Errorf("%w: isolation is required", domain.ErrInvalid)
95}
96
97func stepStateToPB(s domain.CIStepState) pb.StepState {
98	switch s {
99	case domain.CIStepPending:
100		return pb.StepState_STEP_STATE_PENDING
101	case domain.CIStepInProgress:
102		return pb.StepState_STEP_STATE_IN_PROGRESS
103	case domain.CIStepCompleted:
104		return pb.StepState_STEP_STATE_COMPLETED
105	}
106	return pb.StepState_STEP_STATE_UNSPECIFIED
107}
108
109func stepStateFromPB(s pb.StepState) domain.CIStepState {
110	switch s {
111	case pb.StepState_STEP_STATE_PENDING:
112		return domain.CIStepPending
113	case pb.StepState_STEP_STATE_IN_PROGRESS:
114		return domain.CIStepInProgress
115	case pb.StepState_STEP_STATE_COMPLETED:
116		return domain.CIStepCompleted
117	}
118	return ""
119}
120
121func cancelReasonToPB(r domain.CICancelReason) pb.CancelReason {
122	switch r {
123	case domain.CICancelUser:
124		return pb.CancelReason_CANCEL_REASON_USER
125	case domain.CICancelFailFast:
126		return pb.CancelReason_CANCEL_REASON_FAIL_FAST
127	case domain.CICancelTimeout:
128		return pb.CancelReason_CANCEL_REASON_TIMEOUT
129	}
130	return pb.CancelReason_CANCEL_REASON_UNSPECIFIED
131}
132
133// runName is Run.name: the name of the run, the workflow name for a run that
134// has none of its own.
135func runName(r domain.CIRun) string {
136	if r.Name == "" {
137		return r.WorkflowName
138	}
139	return r.Name
140}
141
142func runToPB(r domain.CIRun) *pb.Run {
143	return &pb.Run{
144		Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt,
145		WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Name: runName(r), Title: r.Title, Event: r.Event,
146		Ref: r.Ref, Sha: r.Sha, Actor: r.Actor, Status: statusToPB(r.Status), Conclusion: conclusionToPB(r.Conclusion),
147		Error: r.Error, CreatedAt: timestamp(r.CreatedAt), StartedAt: timestamp(r.StartedAt), CompletedAt: timestamp(r.CompletedAt),
148		Trusted: r.Trusted,
149		Version: int64(r.Version), //nolint:gosec // a store sequence, far below 2^63
150	}
151}
152
153// stepOutcome is Step.outcome: unset until the step is completed, and for a
154// step stored without one (read as its conclusion) it is the conclusion, which
155// is what the contract says UNSPECIFIED means.
156func stepOutcome(s domain.CIStep) domain.CIConclusion {
157	if s.State != domain.CIStepCompleted {
158		return domain.CINoConclusion
159	}
160	if s.Outcome == domain.CINoConclusion {
161		return s.Conclusion
162	}
163	return s.Outcome
164}
165
166func stepsToPB(steps []domain.CIStep, lines int64) []*pb.Step {
167	ranges := domain.StepRanges(steps, lines)
168	out := make([]*pb.Step, 0, len(steps))
169	for i, s := range steps {
170		out = append(out, &pb.Step{
171			Number: s.Number, Name: s.Name, State: stepStateToPB(s.State), Conclusion: conclusionToPB(s.Conclusion),
172			Outcome: conclusionToPB(stepOutcome(s)), StartedAt: timestamp(s.StartedAt), CompletedAt: timestamp(s.CompletedAt),
173			LogStart: ranges[i].Start, LogEnd: ranges[i].End,
174		})
175	}
176	return out
177}
178
179func jobToPB(j domain.CIJob, lines int64) *pb.RunJob {
180	return &pb.RunJob{
181		Id: j.ID, RunId: j.RunID, Attempt: j.Attempt, Repo: j.Repo.String(), Key: j.Key, Name: j.Name,
182		Status: statusToPB(j.Status()), Conclusion: conclusionToPB(j.Conclusion), Error: j.Error,
183		Labels: j.Labels, Needs: j.Needs, RunnerId: j.RunnerID, RunnerName: j.RunnerName, Isolation: isolationToPB(j.Isolation),
184		Steps: stepsToPB(j.Steps, lines), CreatedAt: timestamp(j.CreatedAt), StartedAt: timestamp(j.StartedAt),
185		CompletedAt: timestamp(j.CompletedAt),
186		Version:     int64(j.Version), //nolint:gosec // a store sequence, far below 2^63
187	}
188}
189
190func jobViewToPB(v ports.JobView) *pb.RunJob {
191	out := jobToPB(v.CIJob, v.Lines)
192	if v.RunnerGone {
193		out.RunnerId = ""
194	}
195	return out
196}
197
198func checkToPB(c domain.CICheck) *pb.Check {
199	return &pb.Check{
200		Id: c.ID, Repo: c.Repo.String(), Sha: c.Sha, Name: c.Name, Status: statusToPB(c.Status),
201		Conclusion: conclusionToPB(c.Conclusion), RunId: c.RunID, JobId: c.JobID, Attempt: c.Attempt,
202		WorkflowName: c.WorkflowName, StartedAt: timestamp(c.StartedAt), CompletedAt: timestamp(c.CompletedAt),
203	}
204}
205
206func runnerToPB(r ports.RunnerView) *pb.Runner {
207	return &pb.Runner{
208		Id: r.ID, Name: r.Name, Labels: r.Labels, Isolation: isolationToPB(r.Isolation), Version: r.Version,
209		Arch: r.Arch, Os: r.OS, RegisteredAt: timestamp(r.RegisteredAt), LastSeen: timestamp(r.LastSeen), Busy: r.Busy,
210	}
211}
212
213func logLineToPB(l ports.JobLogLine) *pb.JobLogLine {
214	return &pb.JobLogLine{Step: l.Step, Time: timestamp(l.Time), Text: l.Text, Continued: l.Continued}
215}
216
217func runnerJobToPB(j *ports.RunnerJob) *pb.Job {
218	return &pb.Job{
219		JobId: j.Job.ID, RunId: j.Job.RunID, Attempt: j.Job.RunAttempt, JobToken: j.Token,
220		WorkflowPath: j.WorkflowPath, WorkflowYaml: j.WorkflowYAML, JobKey: j.Job.Key, Matrix: j.Job.MatrixJSON,
221		Contexts: j.Job.ContextsJSON, Labels: j.Job.Labels, TimeoutMinutes: j.Job.TimeoutMinutes,
222		Repo: j.Repo.String(), Sha: j.Sha, Ref: j.Ref, GithubToken: j.GithubToken,
223		Secrets: j.Secrets, Vars: j.Vars, Environment: j.Environment, Trusted: j.Trusted,
224		Permissions: &pb.JobPermissions{Levels: j.Permissions},
225	}
226}
227
228func resolvedActionToPB(a ports.ResolvedAction) *pb.ResolvedAction {
229	out := &pb.ResolvedAction{Ref: a.Ref, Error: a.Error}
230	if a.Error != "" {
231		return out
232	}
233	out.NameWithOwner, out.Path, out.ResolvedSha, out.TarballUrl, out.Token = a.NameWithOwner, a.Path, a.ResolvedSHA, a.TarballURL, a.Token
234	switch a.Source {
235	case domain.CIActionGitHub:
236		out.Source = pb.ActionSource_ACTION_SOURCE_GITHUB
237	case domain.CIActionErmite:
238		out.Source = pb.ActionSource_ACTION_SOURCE_ERMITE
239	}
240	return out
241}
242
243func stepUpdatesFromPB(steps []*pb.StepUpdate) ([]ports.StepUpdate, error) {
244	out := make([]ports.StepUpdate, 0, len(steps))
245	for _, s := range steps {
246		concl, err := conclusionFromPB(s.GetConclusion())
247		if err != nil {
248			return nil, err
249		}
250		outcome, err := conclusionFromPB(s.GetOutcome())
251		if err != nil {
252			return nil, err
253		}
254		out = append(out, ports.StepUpdate{
255			Number: s.GetNumber(), Name: s.GetName(), State: stepStateFromPB(s.GetState()), Conclusion: concl, Outcome: outcome,
256			StartedAt: goTime(s.GetStartedAt()), CompletedAt: goTime(s.GetCompletedAt()),
257		})
258	}
259	return out, nil
260}
261
262func jobEndedToPB(e *domain.JobEndedError) *pb.JobEnded {
263	return &pb.JobEnded{Conclusion: conclusionToPB(e.Conclusion), Error: e.Message}
264}
265
266func runEventToPB(ev domain.RunUpdated) *pb.Event_RunUpdated {
267	return &pb.Event_RunUpdated{RunUpdated: &pb.RunUpdated{Run: runToPB(ev.Run)}}
268}
269
270func jobEventToPB(ev domain.JobUpdated) *pb.Event_JobUpdated {
271	return &pb.Event_JobUpdated{JobUpdated: &pb.JobUpdated{Job: jobToPB(ev.Job, ev.Lines)}}
272}
273
274var annotationLevelPairs = []struct {
275	d domain.AnnotationLevel
276	p pb.AnnotationLevel
277}{
278	{domain.AnnotationNotice, pb.AnnotationLevel_ANNOTATION_LEVEL_NOTICE},
279	{domain.AnnotationWarning, pb.AnnotationLevel_ANNOTATION_LEVEL_WARNING},
280	{domain.AnnotationFailure, pb.AnnotationLevel_ANNOTATION_LEVEL_FAILURE},
281}
282
283func annotationLevelToPB(l domain.AnnotationLevel) pb.AnnotationLevel {
284	for _, e := range annotationLevelPairs {
285		if e.d == l {
286			return e.p
287		}
288	}
289	return pb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED
290}
291
292func annotationToPB(a domain.CIAnnotation) *pb.Annotation {
293	return &pb.Annotation{
294		Level: annotationLevelToPB(a.Level), Title: a.Title, Message: a.Message, File: a.File,
295		StartLine: a.StartLine, EndLine: a.EndLine, StartColumn: a.StartColumn, EndColumn: a.EndColumn, StepNumber: a.Step,
296	}
297}
298
299// annotationsFromPB maps the annotations of a request; an unset or unknown
300// level is invalid_argument. The other rules are the use case's.
301func annotationsFromPB(anns []*pb.Annotation) ([]domain.CIAnnotation, error) {
302	out := make([]domain.CIAnnotation, 0, len(anns))
303	for i, a := range anns {
304		var level domain.AnnotationLevel
305		for _, e := range annotationLevelPairs {
306			if e.p == a.GetLevel() {
307				level = e.d
308			}
309		}
310		if level == "" {
311			return nil, fmt.Errorf("%w: annotation %d has no level", domain.ErrInvalid, i)
312		}
313		out = append(out, domain.CIAnnotation{
314			Level: level, Title: a.GetTitle(), Message: a.GetMessage(), File: a.GetFile(),
315			StartLine: a.GetStartLine(), EndLine: a.GetEndLine(), StartColumn: a.GetStartColumn(), EndColumn: a.GetEndColumn(),
316			Step: a.GetStepNumber(),
317		})
318	}
319	return out, nil
320}

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 2

Tags 45

↑↓ moveenter openesc close