v0.33.0 cf26365

ci_results.go RawHistoryFolder

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}

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