1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6 "context"
7 "fmt"
8 "slices"
9 "unicode/utf8"
10
11 "bnema.dev/ermite/internal/domain"
12 "bnema.dev/ermite/internal/ports"
13)
14
15// The results of a job beyond its steps and its log: annotations and step
16// summaries (runner.proto, run.proto).
17
18// annotationWrites returns the replacements an UpdateJob or CompleteJob makes:
19// one per step the request names whose stored annotations differ, so that
20// repeating a call changes nothing. The request was validated.
21func (c *CI) annotationWrites(ctx context.Context, j domain.CIJob, anns []domain.CIAnnotation) ([]ports.CIStepAnnotations, error) {
22 if len(anns) == 0 {
23 return nil, nil
24 }
25 stored, err := c.cfg.Store.JobAnnotations(ctx, j.RunID, j.Attempt, j.ID)
26 if err != nil {
27 return nil, fmt.Errorf("read the annotations of job %s: %w", j.ID, err)
28 }
29 have, _ := domain.GroupAnnotations(stored)
30 want, steps := domain.GroupAnnotations(anns)
31 var out []ports.CIStepAnnotations
32 for _, step := range steps {
33 if slices.Equal(have[step], want[step]) {
34 continue
35 }
36 out = append(out, ports.CIStepAnnotations{Repo: j.Repo, Exec: logRef(j), Step: step, Items: want[step]})
37 }
38 return out, nil
39}
40
41// UploadSummary stores the summary of a step, replacing the one it has. An
42// empty Markdown removes it.
43func (c *CI) UploadSummary(ctx context.Context, secret, jobID string, step int32, markdown string) error {
44 j, err := c.jobFor(ctx, secret, jobID)
45 if err != nil {
46 return err
47 }
48 if j.Ended() {
49 return endedError(j)
50 }
51 switch {
52 case step < 1 || step > domain.CIMaxSteps:
53 return fmt.Errorf("%w: step %d is not between 1 and %d", domain.ErrInvalid, step, domain.CIMaxSteps)
54 case len(markdown) > domain.CIMaxSummaryBytes:
55 return fmt.Errorf("%w: the summary is larger than 1 MiB", domain.ErrInvalid)
56 case !utf8.ValidString(markdown):
57 return fmt.Errorf("%w: the summary is not valid UTF-8", domain.ErrInvalid)
58 }
59 // Under the lock of the run, so that the end of the job and the summary do
60 // not cross: after the end nothing is stored.
61 return c.lockedJob(ctx, j, func(cur *domain.CIJob) error {
62 if cur.Ended() {
63 return endedError(*cur)
64 }
65 return c.cfg.Summaries.Put(ctx, logRef(*cur), step, markdown)
66 })
67}
68
69// ListAnnotations returns the annotations of a run attempt (0 for the latest),
70// of one job when jobID is set, ordered by job, step and production order.
71// Each job gives those of the attempt in which it executed.
72func (c *CI) ListAnnotations(ctx context.Context, p domain.Principal, name domain.RepoName, runID int64, attempt int32, jobID string) (ports.AnnotationList, error) {
73 if _, err := c.readableRepo(ctx, p, name); err != nil {
74 return ports.AnnotationList{}, err
75 }
76 run, err := c.runOf(ctx, name, runID)
77 if err != nil {
78 return ports.AnnotationList{}, err
79 }
80 if attempt < 0 || attempt > run.Attempt {
81 return ports.AnnotationList{}, domain.ErrNotFound
82 }
83 if attempt == 0 {
84 attempt = run.Attempt
85 }
86 jobs, err := c.cfg.Store.ListJobs(ctx, runID, attempt)
87 if err != nil {
88 return ports.AnnotationList{}, err
89 }
90 if jobID != "" {
91 jobs = slices.DeleteFunc(jobs, func(j domain.CIJob) bool { return j.ID != jobID })
92 if len(jobs) == 0 {
93 return ports.AnnotationList{}, domain.ErrNotFound
94 }
95 }
96 out := ports.AnnotationList{Annotations: []ports.JobAnnotation{}}
97 for _, j := range jobs {
98 anns, err := c.cfg.Store.JobAnnotations(ctx, j.RunID, j.Attempt, j.ID)
99 if err != nil {
100 return ports.AnnotationList{}, err
101 }
102 for _, a := range anns {
103 if len(out.Annotations) >= domain.CIMaxListedAnnotations {
104 out.Truncated = true
105 return out, nil
106 }
107 out.Annotations = append(out.Annotations, ports.JobAnnotation{JobID: j.ID, Annotation: a})
108 }
109 }
110 return out, nil
111}
112
113// GetStepSummaries returns the summaries of a job at an attempt (0 for the
114// latest): the first 20 steps that have one, by step number. With maxBytes
115// above 0, no more than that many bytes of Markdown are read.
116func (c *CI) GetStepSummaries(ctx context.Context, p domain.Principal, name domain.RepoName, jobID string, attempt int32, maxBytes int64) (ports.SummaryList, error) {
117 if _, err := c.readableRepo(ctx, p, name); err != nil {
118 return ports.SummaryList{}, err
119 }
120 _, j, err := c.jobOf(ctx, name, jobID, attempt)
121 if err != nil {
122 return ports.SummaryList{}, err
123 }
124 list, more, err := c.cfg.Summaries.List(ctx, logRef(j), domain.CIMaxListedSummaries, max(maxBytes, 0))
125 if err != nil {
126 return ports.SummaryList{}, err
127 }
128 return ports.SummaryList{Summaries: list, Truncated: more}, nil
129}