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 Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe key is unknown
6 files · +36 -13
M go.mod +1 -1
| @@ -4,7 +4,7 @@ go 1.27.1 | |||
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | 6 | bnema.dev/ermite-actions v0.2.0 | |
| 7 | - | bnema.dev/ermite-api v0.3.0 | |
| 7 | + | bnema.dev/ermite-api v0.4.0 | |
| 8 | 8 | connectrpc.com/connect v1.21.0 | |
| 9 | 9 | github.com/bnema/zerowrap v1.4.1 | |
| 10 | 10 | 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.0 | 6 | bnema.dev/ermite-actions v0.2.0 | ||
| 7 | - | bnema.dev/ermite-api v0.3.0 | 7 | + | bnema.dev/ermite-api v0.4.0 |
| 8 | connectrpc.com/connect v1.21.0 | 8 | connectrpc.com/connect v1.21.0 | ||
| 9 | github.com/bnema/zerowrap v1.4.1 | 9 | github.com/bnema/zerowrap v1.4.1 | ||
| 10 | github.com/containerd/errdefs v1.0.0 | 10 | github.com/containerd/errdefs v1.0.0 | ||
M go.sum +2 -2 generated file: collapsed
M internal/executor/issues.go +1 -1
| @@ -20,7 +20,7 @@ const ( | |||
| 20 | 20 | maxCountPerIssueType = 10 | |
| 21 | 21 | // Limits of the contract (runner.proto, Annotation): the server refuses a | |
| 22 | 22 | // whole call that exceeds them, so the executor keeps within them. | |
| 23 | - | maxAnnotationTitle = 255 | |
| 23 | + | maxAnnotationTitle = 4096 | |
| 24 | 24 | maxAnnotationFile = 1024 | |
| 25 | 25 | ) | |
| 26 | 26 | ||
| @@ -20,7 +20,7 @@ const ( | |||||
| 20 | maxCountPerIssueType = 10 | 20 | maxCountPerIssueType = 10 | ||
| 21 | // Limits of the contract (runner.proto, Annotation): the server refuses a | 21 | // 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 = 255 | 23 | + | maxAnnotationTitle = 4096 |
| 24 | maxAnnotationFile = 1024 | 24 | maxAnnotationFile = 1024 | ||
| 25 | ) | 25 | ) | ||
| 26 | 26 | ||||
M internal/executor/issues_test.go +23 -0
| @@ -354,3 +354,26 @@ func TestMatcherAnnotationsWithoutFileHaveTheirLogLine(t *testing.T) { | |||
| 354 | 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 | + | } | |
| @@ -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 ( | |||
| 32 | 32 | const ( | |
| 33 | 33 | maxAnnotationsPerLevel = 10 | |
| 34 | 34 | maxAnnotationMessage = 4096 | |
| 35 | - | maxAnnotationTitle = 255 | |
| 35 | + | maxAnnotationTitle = 4096 | |
| 36 | 36 | maxAnnotationFile = 1024 | |
| 37 | 37 | maxSummaryBytes = 1 << 20 | |
| 38 | 38 | ) | |
| @@ -32,7 +32,7 @@ const ( | |||||
| 32 | const ( | 32 | const ( | ||
| 33 | maxAnnotationsPerLevel = 10 | 33 | maxAnnotationsPerLevel = 10 | ||
| 34 | maxAnnotationMessage = 4096 | 34 | maxAnnotationMessage = 4096 | ||
| 35 | - | maxAnnotationTitle = 255 | 35 | + | maxAnnotationTitle = 4096 |
| 36 | maxAnnotationFile = 1024 | 36 | maxAnnotationFile = 1024 | ||
| 37 | maxSummaryBytes = 1 << 20 | 37 | maxSummaryBytes = 1 << 20 | ||
| 38 | ) | 38 | ) | ||
M internal/host/results_test.go +8 -8
| @@ -154,18 +154,18 @@ func TestFeaturesUnreachableMeansNothingIsSent(t *testing.T) { | |||
| 154 | 154 | } | |
| 155 | 155 | ||
| 156 | 156 | // 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 | 158 | func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) { | |
| 159 | 159 | var many []*execpb.Annotation | |
| 160 | 160 | for range 12 { | |
| 161 | 161 | many = append(many, &execpb.Annotation{Level: lWarn, Message: "w"}) | |
| 162 | 162 | } | |
| 163 | 163 | 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 | |
| 169 | 169 | &execpb.Annotation{Level: lFail, Message: "end before", StartLine: 9, EndLine: 3}, | |
| 170 | 170 | ) | |
| 171 | 171 | sc := func(_ *testing.T, c *execstream.Conn, _ *execpb.Run, _ <-chan *execpb.Cancel) { | |
| @@ -196,7 +196,7 @@ func TestGuestAnnotationsAreBoundedByTheHost(t *testing.T) { | |||
| 196 | 196 | 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 | 200 | t.Errorf("title: %v", cut) | |
| 201 | 201 | } | |
| 202 | 202 | if neg == nil || neg.StartLine != 0 || neg.EndLine != 0 || neg.StartColumn != 5 { | |
| @@ -401,7 +401,7 @@ func TestCutStringsDoNotRetainTheGuestAllocation(t *testing.T) { | |||
| 401 | 401 | // within 4096 characters, a frame cannot make it retain more than that | |
| 402 | 402 | // through a substring because the host does not slice it. | |
| 403 | 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 | 405 | t.Errorf("title %d", len(a[0].Title)) | |
| 406 | 406 | } | |
| 407 | 407 | 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: ten | 156 | // 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.Annotation | 159 | 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: dropped | 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 | 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 | 166 | + | &execpb.Annotation{Level: lFail, Message: ""}, // empty: dropped |
| 167 | - | &execpb.Annotation{Level: execpb.AnnotationLevel_ANNOTATION_LEVEL_UNSPECIFIED, Message: "no level"}, // 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 | 168 | + | &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 = a | 196 | 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 that | 401 | // 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 != "" { | ||