detached fa0055a

fix(ci): annotation titles up to 4096 runes (#31)

commit
fa0055a7aad8eb4ffe29f09301bb3ca235cd1e0f
author
brice · · 2026-10-16 07:54 UTC
committer
bnema · · 2026-10-16 07:54 UTC
parent
9ebdf14
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
6 files · +36 -13
M go.mod +1 -1
@@ -4,7 +4,7 @@ go 1.27.1
44
55 require (
66 bnema.dev/ermite-actions v0.2.0
7- bnema.dev/ermite-api v0.3.0
7+ bnema.dev/ermite-api v0.4.0
88 connectrpc.com/connect v1.21.0
99 github.com/bnema/zerowrap v1.4.1
1010 github.com/containerd/errdefs v1.0.0
@@ -4,7 +4,7 @@ go 1.27.1
4 4
5 require (5 require (
6 bnema.dev/ermite-actions v0.2.06 bnema.dev/ermite-actions v0.2.0
7- bnema.dev/ermite-api v0.3.07+ bnema.dev/ermite-api v0.4.0
8 connectrpc.com/connect v1.21.08 connectrpc.com/connect v1.21.0
9 github.com/bnema/zerowrap v1.4.19 github.com/bnema/zerowrap v1.4.1
10 github.com/containerd/errdefs v1.0.010 github.com/containerd/errdefs v1.0.0
M go.sum +2 -2
@@ -1,7 +1,7 @@
11 bnema.dev/ermite-actions v0.2.0 h1:XJyGNCDfgiEv/svN7FJqSz2nX/nT2SZgYtxBAhz4e/Y=
22 bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4=
3-bnema.dev/ermite-api v0.3.0 h1:6mpinMuTGzcncG4ynt5b8bzRGW0No+VtAbJQsmCQ+HI=
4-bnema.dev/ermite-api v0.3.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8=
3+bnema.dev/ermite-api v0.4.0 h1:KEj5f0cr0qacuo9DMu4zgdKWyhjcFVb3is+gNO9hbUg=
4+bnema.dev/ermite-api v0.4.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8=
55 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 h1:z5+rVknD0AvorY/QV9cb3ohUXw4E7HvxU4+2JUH6EHU=
66 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc=
77 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo=
@@ -1,7 +1,7 @@
1 bnema.dev/ermite-actions v0.2.0 h1:XJyGNCDfgiEv/svN7FJqSz2nX/nT2SZgYtxBAhz4e/Y=1 bnema.dev/ermite-actions v0.2.0 h1:XJyGNCDfgiEv/svN7FJqSz2nX/nT2SZgYtxBAhz4e/Y=
2 bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4=2 bnema.dev/ermite-actions v0.2.0/go.mod h1:qoIB/Iz4xKdLUG4R6wuBDk6cemdugR+UEo5bpSJJYh4=
3-bnema.dev/ermite-api v0.3.0 h1:6mpinMuTGzcncG4ynt5b8bzRGW0No+VtAbJQsmCQ+HI=3+bnema.dev/ermite-api v0.4.0 h1:KEj5f0cr0qacuo9DMu4zgdKWyhjcFVb3is+gNO9hbUg=
4-bnema.dev/ermite-api v0.3.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8=4+bnema.dev/ermite-api v0.4.0/go.mod h1:L5smjt+IAigyAo3Ny2EnFB9IwvIGOPT1GoByZNgbDa8=
5 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 h1:z5+rVknD0AvorY/QV9cb3ohUXw4E7HvxU4+2JUH6EHU=5 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2 h1:z5+rVknD0AvorY/QV9cb3ohUXw4E7HvxU4+2JUH6EHU=
6 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc=6 buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.12-20260722160903-4d94f3df3a7b.2/go.mod h1:Owcuj2phVyvThVYCD74iSV8ki0dAXw/uxkdcdZScdCc=
7 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo=7 buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.12-20250109164928-1da0de137947.2 h1:tE4A2RVukQOSd4K+6ndK+8s65FbjiBt2k5CiddXxCCo=
M internal/executor/issues.go +1 -1
@@ -20,7 +20,7 @@ const (
2020 maxCountPerIssueType = 10
2121 // Limits of the contract (runner.proto, Annotation): the server refuses a
2222 // whole call that exceeds them, so the executor keeps within them.
23- maxAnnotationTitle = 255
23+ maxAnnotationTitle = 4096
2424 maxAnnotationFile = 1024
2525 )
2626
@@ -20,7 +20,7 @@ const (
20 maxCountPerIssueType = 1020 maxCountPerIssueType = 10
21 // Limits of the contract (runner.proto, Annotation): the server refuses a21 // Limits of the contract (runner.proto, Annotation): the server refuses a
22 // whole call that exceeds them, so the executor keeps within them.22 // whole call that exceeds them, so the executor keeps within them.
23- maxAnnotationTitle = 25523+ maxAnnotationTitle = 4096
24 maxAnnotationFile = 102424 maxAnnotationFile = 1024
25 )25 )
26 26
M internal/executor/issues_test.go +23 -0
@@ -354,3 +354,26 @@ func TestMatcherAnnotationsWithoutFileHaveTheirLogLine(t *testing.T) {
354354 t.Errorf("next step: %v\nwant %v\n%s", got, want4, logOf(rr.step(t, "the matcher stays removed in the next step")))
355355 }
356356 }
357+
358+// A title is bounded like the message, at 4096 characters (GitHub has no cap;
359+// ann-limits records a 600 character title): a 600 one is sent intact.
360+func TestAnnotationTitleIsSentIntactUpTo4096(t *testing.T) {
361+ needTools(t, "bash")
362+ rr, _ := runResults(t, wfHead+` steps:
363+ - run: |
364+ t600=$(printf 't%.0s' $(seq 600))
365+ t4100=$(printf 'u%.0s' $(seq 4100))
366+ echo "::warning title=$t600::m600"
367+ echo "::warning title=$t4100::m4100"
368+`, nil)
369+ got := map[string]string{}
370+ for _, a := range rr.results[2].Annotations {
371+ got[a.Message] = a.Title
372+ }
373+ if got["m600"] != strings.Repeat("t", 600) {
374+ t.Errorf("600 character title: %d characters", len([]rune(got["m600"])))
375+ }
376+ if got["m4100"] != strings.Repeat("u", 4096) {
377+ t.Errorf("4100 character title: %d characters", len([]rune(got["m4100"])))
378+ }
379+}
@@ -354,3 +354,26 @@ func TestMatcherAnnotationsWithoutFileHaveTheirLogLine(t *testing.T) {
354 t.Errorf("next step: %v\nwant %v\n%s", got, want4, logOf(rr.step(t, "the matcher stays removed in the next step")))354 t.Errorf("next step: %v\nwant %v\n%s", got, want4, logOf(rr.step(t, "the matcher stays removed in the next step")))
355 }355 }
356 }356 }
357+
358+// A title is bounded like the message, at 4096 characters (GitHub has no cap;
359+// ann-limits records a 600 character title): a 600 one is sent intact.
360+func TestAnnotationTitleIsSentIntactUpTo4096(t *testing.T) {
361+ needTools(t, "bash")
362+ rr, _ := runResults(t, wfHead+` steps:
363+ - run: |
364+ t600=$(printf 't%.0s' $(seq 600))
365+ t4100=$(printf 'u%.0s' $(seq 4100))
366+ echo "::warning title=$t600::m600"
367+ echo "::warning title=$t4100::m4100"
368+`, nil)
369+ got := map[string]string{}
370+ for _, a := range rr.results[2].Annotations {
371+ got[a.Message] = a.Title
372+ }
373+ if got["m600"] != strings.Repeat("t", 600) {
374+ t.Errorf("600 character title: %d characters", len([]rune(got["m600"])))
375+ }
376+ if got["m4100"] != strings.Repeat("u", 4096) {
377+ t.Errorf("4100 character title: %d characters", len([]rune(got["m4100"])))
378+ }
379+}
M internal/host/results.go +1 -1
@@ -32,7 +32,7 @@ const (
3232 const (
3333 maxAnnotationsPerLevel = 10
3434 maxAnnotationMessage = 4096
35- maxAnnotationTitle = 255
35+ maxAnnotationTitle = 4096
3636 maxAnnotationFile = 1024
3737 maxSummaryBytes = 1 << 20
3838 )
@@ -32,7 +32,7 @@ const (
32 const (32 const (
33 maxAnnotationsPerLevel = 1033 maxAnnotationsPerLevel = 10
34 maxAnnotationMessage = 409634 maxAnnotationMessage = 4096
35- maxAnnotationTitle = 25535+ maxAnnotationTitle = 4096
36 maxAnnotationFile = 102436 maxAnnotationFile = 1024
37 maxSummaryBytes = 1 << 2037 maxSummaryBytes = 1 << 20
38 )38 )
M internal/host/results_test.go +8 -8
@@ -154,18 +154,18 @@ func TestFeaturesUnreachableMeansNothingIsSent(t *testing.T) {
154154 }
155155
156156 // The host enforces the limits of the contract on what the guest sends: ten
157-// per level, 4096 characters, titles of 255, valid numbers.
157+// per level, 4096 characters, titles of 4096, valid numbers.
158158 func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) {
159159 var many []*execpb.Annotation
160160 for range 12 {
161161 many = append(many, &execpb.Annotation{Level: lWarn, Message: "w"})
162162 }
163163 many = append(many,
164- &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4097)}, // too long: dropped
165- &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4096), Title: strings.Repeat("t", 300)}, // kept, title cut
166- &execpb.Annotation{Level: lFail, Message: ""}, // empty: dropped
167- &execpb.Annotation{Level: execpb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED, Message: "no level"}, // dropped
168- &execpb.Annotation{Level: lFail, Message: "bad numbers", StartLine: -4, EndLine: -1, StartColumn: 5}, // numbers made valid
164+ &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4097)}, // too long: dropped
165+ &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4096), Title: strings.Repeat("t", 4100)}, // kept, title cut
166+ &execpb.Annotation{Level: lFail, Message: ""}, // empty: dropped
167+ &execpb.Annotation{Level: execpb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED, Message: "no level"}, // dropped
168+ &execpb.Annotation{Level: lFail, Message: "bad numbers", StartLine: -4, EndLine: -1, StartColumn: 5}, // numbers made valid
169169 &execpb.Annotation{Level: lFail, Message: "end before", StartLine: 9, EndLine: 3},
170170 )
171171 sc := func(_ *testing.T, c *execstream.Conn, _ *execpb.Run, _ <-chan *execpb.Cancel) {
@@ -196,7 +196,7 @@ func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) {
196196 end = a
197197 }
198198 }
199- if cut == nil || len([]rune(cut.Title)) != 255 {
199+ if cut == nil || len([]rune(cut.Title)) != 4096 {
200200 t.Errorf("title: %v", cut)
201201 }
202202 if neg == nil || neg.StartLine != 0 || neg.EndLine != 0 || neg.StartColumn != 5 {
@@ -401,7 +401,7 @@ func TestCutStringsDoNotRetainTheGuestAllocation(t *testing.T) {
401401 // within 4096 characters, a frame cannot make it retain more than that
402402 // through a substring because the host does not slice it.
403403 a := annotationsFrom(1, []*execpb.Annotation{{Level: lWarn, Message: "m", Title: big, File: strings.Repeat("f", 2000)}})
404- if len(a) != 1 || len(a[0].Title) != 255 || unsafe.StringData(a[0].Title) == unsafe.StringData(big) {
404+ if len(a) != 1 || len(a[0].Title) != 4096 || unsafe.StringData(a[0].Title) == unsafe.StringData(big) {
405405 t.Errorf("title %d", len(a[0].Title))
406406 }
407407 if a[0].File != "" {
@@ -154,18 +154,18 @@ func TestFeaturesUnreachableMeansNothingIsSent(t *testing.T) {
154 }154 }
155 155
156 // The host enforces the limits of the contract on what the guest sends: ten156 // The host enforces the limits of the contract on what the guest sends: ten
157-// per level, 4096 characters, titles of 255, valid numbers.157+// per level, 4096 characters, titles of 4096, valid numbers.
158 func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) {158 func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) {
159 var many []*execpb.Annotation159 var many []*execpb.Annotation
160 for range 12 {160 for range 12 {
161 many = append(many, &execpb.Annotation{Level: lWarn, Message: "w"})161 many = append(many, &execpb.Annotation{Level: lWarn, Message: "w"})
162 }162 }
163 many = append(many,163 many = append(many,
164- &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4097)}, // too long: dropped164+ &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4097)}, // too long: dropped
165- &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4096), Title: strings.Repeat("t", 300)}, // kept, title cut165+ &execpb.Annotation{Level: lFail, Message: strings.Repeat("é", 4096), Title: strings.Repeat("t", 4100)}, // kept, title cut
166- &execpb.Annotation{Level: lFail, Message: ""}, // empty: dropped166+ &execpb.Annotation{Level: lFail, Message: ""}, // empty: dropped
167- &execpb.Annotation{Level: execpb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED, Message: "no level"}, // dropped167+ &execpb.Annotation{Level: execpb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED, Message: "no level"}, // dropped
168- &execpb.Annotation{Level: lFail, Message: "bad numbers", StartLine: -4, EndLine: -1, StartColumn: 5}, // numbers made valid168+ &execpb.Annotation{Level: lFail, Message: "bad numbers", StartLine: -4, EndLine: -1, StartColumn: 5}, // numbers made valid
169 &execpb.Annotation{Level: lFail, Message: "end before", StartLine: 9, EndLine: 3},169 &execpb.Annotation{Level: lFail, Message: "end before", StartLine: 9, EndLine: 3},
170 )170 )
171 sc := func(_ *testing.T, c *execstream.Conn, _ *execpb.Run, _ <-chan *execpb.Cancel) {171 sc := func(_ *testing.T, c *execstream.Conn, _ *execpb.Run, _ <-chan *execpb.Cancel) {
@@ -196,7 +196,7 @@ func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) {
196 end = a196 end = a
197 }197 }
198 }198 }
199- if cut == nil || len([]rune(cut.Title)) != 255 {199+ if cut == nil || len([]rune(cut.Title)) != 4096 {
200 t.Errorf("title: %v", cut)200 t.Errorf("title: %v", cut)
201 }201 }
202 if neg == nil || neg.StartLine != 0 || neg.EndLine != 0 || neg.StartColumn != 5 {202 if neg == nil || neg.StartLine != 0 || neg.EndLine != 0 || neg.StartColumn != 5 {
@@ -401,7 +401,7 @@ func TestCutStringsDoNotRetainTheGuestAllocation(t *testing.T) {
401 // within 4096 characters, a frame cannot make it retain more than that401 // within 4096 characters, a frame cannot make it retain more than that
402 // through a substring because the host does not slice it.402 // through a substring because the host does not slice it.
403 a := annotationsFrom(1, []*execpb.Annotation{{Level: lWarn, Message: "m", Title: big, File: strings.Repeat("f", 2000)}})403 a := annotationsFrom(1, []*execpb.Annotation{{Level: lWarn, Message: "m", Title: big, File: strings.Repeat("f", 2000)}})
404- if len(a) != 1 || len(a[0].Title) != 255 || unsafe.StringData(a[0].Title) == unsafe.StringData(big) {404+ if len(a) != 1 || len(a[0].Title) != 4096 || unsafe.StringData(a[0].Title) == unsafe.StringData(big) {
405 t.Errorf("title %d", len(a[0].Title))405 t.Errorf("title %d", len(a[0].Title))
406 }406 }
407 if a[0].File != "" {407 if a[0].File != "" {

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 11

↑↓ moveenter openesc close