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 Version: int64(r.Version), //nolint:gosec // a store sequence, far below 2^63
149 }
150}
151
152// stepOutcome is Step.outcome: unset until the step is completed, and for a
153// step stored without one (read as its conclusion) it is the conclusion, which
154// is what the contract says UNSPECIFIED means.
155func stepOutcome(s domain.CIStep) domain.CIConclusion {
156 if s.State != domain.CIStepCompleted {
157 return domain.CINoConclusion
158 }
159 if s.Outcome == domain.CINoConclusion {
160 return s.Conclusion
161 }
162 return s.Outcome
163}
164
165func stepsToPB(steps []domain.CIStep, lines int64) []*pb.Step {
166 ranges := domain.StepRanges(steps, lines)
167 out := make([]*pb.Step, 0, len(steps))
168 for i, s := range steps {
169 out = append(out, &pb.Step{
170 Number: s.Number, Name: s.Name, State: stepStateToPB(s.State), Conclusion: conclusionToPB(s.Conclusion),
171 Outcome: conclusionToPB(stepOutcome(s)), StartedAt: timestamp(s.StartedAt), CompletedAt: timestamp(s.CompletedAt),
172 LogStart: ranges[i].Start, LogEnd: ranges[i].End,
173 })
174 }
175 return out
176}
177
178func jobToPB(j domain.CIJob, lines int64) *pb.RunJob {
179 return &pb.RunJob{
180 Id: j.ID, RunId: j.RunID, Attempt: j.Attempt, Repo: j.Repo.String(), Key: j.Key, Name: j.Name,
181 Status: statusToPB(j.Status()), Conclusion: conclusionToPB(j.Conclusion), Error: j.Error,
182 Labels: j.Labels, Needs: j.Needs, RunnerId: j.RunnerID, RunnerName: j.RunnerName, Isolation: isolationToPB(j.Isolation),
183 Steps: stepsToPB(j.Steps, lines), CreatedAt: timestamp(j.CreatedAt), StartedAt: timestamp(j.StartedAt),
184 CompletedAt: timestamp(j.CompletedAt),
185 Version: int64(j.Version), //nolint:gosec // a store sequence, far below 2^63
186 }
187}
188
189func jobViewToPB(v ports.JobView) *pb.RunJob {
190 out := jobToPB(v.CIJob, v.Lines)
191 if v.RunnerGone {
192 out.RunnerId = ""
193 }
194 return out
195}
196
197func checkToPB(c domain.CICheck) *pb.Check {
198 return &pb.Check{
199 Id: c.ID, Repo: c.Repo.String(), Sha: c.Sha, Name: c.Name, Status: statusToPB(c.Status),
200 Conclusion: conclusionToPB(c.Conclusion), RunId: c.RunID, JobId: c.JobID, Attempt: c.Attempt,
201 WorkflowName: c.WorkflowName, StartedAt: timestamp(c.StartedAt), CompletedAt: timestamp(c.CompletedAt),
202 }
203}
204
205func runnerToPB(r ports.RunnerView) *pb.Runner {
206 return &pb.Runner{
207 Id: r.ID, Name: r.Name, Labels: r.Labels, Isolation: isolationToPB(r.Isolation), Version: r.Version,
208 Arch: r.Arch, Os: r.OS, RegisteredAt: timestamp(r.RegisteredAt), LastSeen: timestamp(r.LastSeen), Busy: r.Busy,
209 }
210}
211
212func logLineToPB(l ports.JobLogLine) *pb.JobLogLine {
213 return &pb.JobLogLine{Step: l.Step, Time: timestamp(l.Time), Text: l.Text, Continued: l.Continued}
214}
215
216func runnerJobToPB(j *ports.RunnerJob) *pb.Job {
217 return &pb.Job{
218 JobId: j.Job.ID, RunId: j.Job.RunID, Attempt: j.Job.RunAttempt, JobToken: j.Token,
219 WorkflowPath: j.WorkflowPath, WorkflowYaml: j.WorkflowYAML, JobKey: j.Job.Key, Matrix: j.Job.MatrixJSON,
220 Contexts: j.Job.ContextsJSON, Labels: j.Job.Labels, TimeoutMinutes: j.Job.TimeoutMinutes,
221 Repo: j.Repo.String(), Sha: j.Sha, Ref: j.Ref, GithubToken: j.GithubToken,
222 }
223}
224
225func resolvedActionToPB(a ports.ResolvedAction) *pb.ResolvedAction {
226 out := &pb.ResolvedAction{Ref: a.Ref, Error: a.Error}
227 if a.Error != "" {
228 return out
229 }
230 out.NameWithOwner, out.Path, out.ResolvedSha, out.TarballUrl, out.Token = a.NameWithOwner, a.Path, a.ResolvedSHA, a.TarballURL, a.Token
231 switch a.Source {
232 case domain.CIActionGitHub:
233 out.Source = pb.ActionSource_ACTION_SOURCE_GITHUB
234 case domain.CIActionErmite:
235 out.Source = pb.ActionSource_ACTION_SOURCE_ERMITE
236 }
237 return out
238}
239
240func stepUpdatesFromPB(steps []*pb.StepUpdate) ([]ports.StepUpdate, error) {
241 out := make([]ports.StepUpdate, 0, len(steps))
242 for _, s := range steps {
243 concl, err := conclusionFromPB(s.GetConclusion())
244 if err != nil {
245 return nil, err
246 }
247 outcome, err := conclusionFromPB(s.GetOutcome())
248 if err != nil {
249 return nil, err
250 }
251 out = append(out, ports.StepUpdate{
252 Number: s.GetNumber(), Name: s.GetName(), State: stepStateFromPB(s.GetState()), Conclusion: concl, Outcome: outcome,
253 StartedAt: goTime(s.GetStartedAt()), CompletedAt: goTime(s.GetCompletedAt()),
254 })
255 }
256 return out, nil
257}
258
259func jobEndedToPB(e *domain.JobEndedError) *pb.JobEnded {
260 return &pb.JobEnded{Conclusion: conclusionToPB(e.Conclusion), Error: e.Message}
261}
262
263func runEventToPB(ev domain.RunUpdated) *pb.Event_RunUpdated {
264 return &pb.Event_RunUpdated{RunUpdated: &pb.RunUpdated{Run: runToPB(ev.Run)}}
265}
266
267func jobEventToPB(ev domain.JobUpdated) *pb.Event_JobUpdated {
268 return &pb.Event_JobUpdated{JobUpdated: &pb.JobUpdated{Job: jobToPB(ev.Job, ev.Lines)}}
269}
270
271var annotationLevelPairs = []struct {
272 d domain.AnnotationLevel
273 p pb.AnnotationLevel
274}{
275 {domain.AnnotationNotice, pb.AnnotationLevel_ANNOTATION_LEVEL_NOTICE},
276 {domain.AnnotationWarning, pb.AnnotationLevel_ANNOTATION_LEVEL_WARNING},
277 {domain.AnnotationFailure, pb.AnnotationLevel_ANNOTATION_LEVEL_FAILURE},
278}
279
280func annotationLevelToPB(l domain.AnnotationLevel) pb.AnnotationLevel {
281 for _, e := range annotationLevelPairs {
282 if e.d == l {
283 return e.p
284 }
285 }
286 return pb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED
287}
288
289func annotationToPB(a domain.CIAnnotation) *pb.Annotation {
290 return &pb.Annotation{
291 Level: annotationLevelToPB(a.Level), Title: a.Title, Message: a.Message, File: a.File,
292 StartLine: a.StartLine, EndLine: a.EndLine, StartColumn: a.StartColumn, EndColumn: a.EndColumn, StepNumber: a.Step,
293 }
294}
295
296// annotationsFromPB maps the annotations of a request; an unset or unknown
297// level is invalid_argument. The other rules are the use case's.
298func annotationsFromPB(anns []*pb.Annotation) ([]domain.CIAnnotation, error) {
299 out := make([]domain.CIAnnotation, 0, len(anns))
300 for i, a := range anns {
301 var level domain.AnnotationLevel
302 for _, e := range annotationLevelPairs {
303 if e.p == a.GetLevel() {
304 level = e.d
305 }
306 }
307 if level == "" {
308 return nil, fmt.Errorf("%w: annotation %d has no level", domain.ErrInvalid, i)
309 }
310 out = append(out, domain.CIAnnotation{
311 Level: level, Title: a.GetTitle(), Message: a.GetMessage(), File: a.GetFile(),
312 StartLine: a.GetStartLine(), EndLine: a.GetEndLine(), StartColumn: a.GetStartColumn(), EndColumn: a.GetEndColumn(),
313 Step: a.GetStepNumber(),
314 })
315 }
316 return out, nil
317}