detached 27e453c

ci_annotation.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6	"fmt"
7	"slices"
8	"unicode/utf8"
9)
10
11// AnnotationLevel is the severity of an annotation, with the names of GitHub's
12// check run annotation levels.
13type AnnotationLevel string
14
15// Annotation levels.
16const (
17	AnnotationNotice  AnnotationLevel = "notice"
18	AnnotationWarning AnnotationLevel = "warning"
19	AnnotationFailure AnnotationLevel = "failure"
20)
21
22// Valid reports whether the level is one of the three.
23func (l AnnotationLevel) Valid() bool {
24	return l == AnnotationNotice || l == AnnotationWarning || l == AnnotationFailure
25}
26
27// Limits of annotations and summaries (runner.proto, run.proto). Lengths count
28// characters (code points), not bytes, except the summary size.
29const (
30	// CIMaxAnnotationsPerLevel bounds the annotations of one level in one step.
31	CIMaxAnnotationsPerLevel = 10
32	// CIMaxAnnotationMessage, CIMaxAnnotationTitle and CIMaxAnnotationFile bound
33	// the text fields of an annotation, in characters.
34	CIMaxAnnotationMessage = 4096
35	CIMaxAnnotationTitle   = 4096
36	CIMaxAnnotationFile    = 1024
37	// CIMaxListedAnnotations is the most annotations ListAnnotations returns.
38	CIMaxListedAnnotations = 1000
39	// CIMaxSummaryBytes bounds the summary of one step (1 MiB).
40	CIMaxSummaryBytes = 1 << 20
41	// CIMaxListedSummaries is the most summaries GetStepSummaries returns for a
42	// job, as GitHub does.
43	CIMaxListedSummaries = 20
44)
45
46// CIAnnotation is a message attached to a step, and optionally to a place in a
47// file, like an annotation of a GitHub check run.
48type CIAnnotation struct {
49	Level                                      AnnotationLevel
50	Title, Message, File                       string
51	StartLine, EndLine, StartColumn, EndColumn int32
52	// Step is the number of the step that produced the annotation.
53	Step int32
54}
55
56// CIStepSummary is the job summary (Markdown) of one step.
57type CIStepSummary struct {
58	Step     int32
59	Markdown string
60	// Omitted is set when the summary exists but was not read, because the
61	// caller's byte budget was used up; Markdown is then empty.
62	Omitted bool
63}
64
65// CheckAnnotations validates the annotations of a request: the limits of the
66// contract, whole or not at all.
67func CheckAnnotations(anns []CIAnnotation) error {
68	type key struct {
69		step  int32
70		level AnnotationLevel
71	}
72	count := map[key]int{}
73	for i, a := range anns {
74		switch {
75		case !a.Level.Valid():
76			return fmt.Errorf("%w: annotation %d has no level", ErrInvalid, i)
77		case a.Step < 1 || a.Step > CIMaxSteps:
78			return fmt.Errorf("%w: annotation %d: step number %d is not between 1 and %d", ErrInvalid, i, a.Step, CIMaxSteps)
79		case a.Message == "":
80			return fmt.Errorf("%w: annotation %d has no message", ErrInvalid, i)
81		case utf8.RuneCountInString(a.Message) > CIMaxAnnotationMessage:
82			return fmt.Errorf("%w: annotation %d: the message is longer than %d characters", ErrInvalid, i, CIMaxAnnotationMessage)
83		case utf8.RuneCountInString(a.Title) > CIMaxAnnotationTitle:
84			return fmt.Errorf("%w: annotation %d: the title is longer than %d characters", ErrInvalid, i, CIMaxAnnotationTitle)
85		case utf8.RuneCountInString(a.File) > CIMaxAnnotationFile:
86			return fmt.Errorf("%w: annotation %d: the file is longer than %d characters", ErrInvalid, i, CIMaxAnnotationFile)
87		case !utf8.ValidString(a.Message) || !utf8.ValidString(a.Title) || !utf8.ValidString(a.File):
88			return fmt.Errorf("%w: annotation %d is not valid UTF-8", ErrInvalid, i)
89		case a.StartLine < 0 || a.EndLine < 0 || a.StartColumn < 0 || a.EndColumn < 0:
90			return fmt.Errorf("%w: annotation %d: lines and columns are not negative", ErrInvalid, i)
91		case a.EndLine != 0 && a.EndLine < a.StartLine:
92			return fmt.Errorf("%w: annotation %d: end_line is before start_line", ErrInvalid, i)
93		}
94		k := key{a.Step, a.Level}
95		if count[k]++; count[k] > CIMaxAnnotationsPerLevel {
96			return fmt.Errorf("%w: step %d has more than %d annotations of level %s", ErrInvalid, a.Step, CIMaxAnnotationsPerLevel, a.Level)
97		}
98	}
99	return nil
100}
101
102// GroupAnnotations groups annotations by step number, each group in the order
103// of the input, and returns the step numbers in ascending order.
104func GroupAnnotations(anns []CIAnnotation) (map[int32][]CIAnnotation, []int32) {
105	by := map[int32][]CIAnnotation{}
106	var steps []int32
107	for _, a := range anns {
108		if _, ok := by[a.Step]; !ok {
109			steps = append(steps, a.Step)
110		}
111		by[a.Step] = append(by[a.Step], a)
112	}
113	slices.Sort(steps)
114	return by, steps
115}

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 4

Tags 45

↑↓ moveenter openesc close