v0.14.0 815fb98

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

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