Skip to content

Commit f8d5962

Browse files
authored
fix: validate and truncate characters exceeding limit (#2550)
1 parent f25a582 commit f8d5962

File tree

2 files changed

+73
-14
lines changed

2 files changed

+73
-14
lines changed

backend/controllers/projects_helpers.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"log/slog"
77
"os"
8+
"unicode/utf8"
89

910
"github.com/diggerhq/digger/backend/models"
1011
"github.com/diggerhq/digger/backend/utils"
@@ -13,12 +14,11 @@ import (
1314
orchestrator_scheduler "github.com/diggerhq/digger/libs/scheduler"
1415
)
1516

16-
17-
func GenerateChecksSummaryForBatch( batch *models.DiggerBatch) (string, error) {
17+
func GenerateChecksSummaryForBatch(batch *models.DiggerBatch) (string, error) {
1818
summaryEndpoint := os.Getenv("DIGGER_AI_SUMMARY_ENDPOINT")
1919
if summaryEndpoint == "" {
2020
slog.Error("DIGGER_AI_SUMMARY_ENDPOINT not set")
21-
return"", fmt.Errorf("could not generate AI summary, ai summary endpoint missing")
21+
return "", fmt.Errorf("could not generate AI summary, ai summary endpoint missing")
2222
}
2323
apiToken := os.Getenv("DIGGER_AI_SUMMARY_API_TOKEN")
2424

@@ -73,12 +73,12 @@ func GenerateChecksSummaryForBatch( batch *models.DiggerBatch) (string, error) {
7373
return summary, nil
7474
}
7575

76-
func GenerateChecksSummaryForJob( job *models.DiggerJob) (string, error) {
76+
func GenerateChecksSummaryForJob(job *models.DiggerJob) (string, error) {
7777
batch := job.Batch
7878
summaryEndpoint := os.Getenv("DIGGER_AI_SUMMARY_ENDPOINT")
7979
if summaryEndpoint == "" {
8080
slog.Error("AI summary endpoint not configured", "batch", batch.ID, "jobId", job.ID, "DiggerJobId", job.DiggerJobID)
81-
return"", fmt.Errorf("could not generate AI summary, ai summary endpoint missing")
81+
return "", fmt.Errorf("could not generate AI summary, ai summary endpoint missing")
8282
}
8383
apiToken := os.Getenv("DIGGER_AI_SUMMARY_API_TOKEN")
8484

@@ -100,7 +100,7 @@ func GenerateChecksSummaryForJob( job *models.DiggerJob) (string, error) {
100100
summary := ""
101101

102102
if job.WorkflowRunUrl != nil {
103-
summary += fmt.Sprintf(":link: <a href='%v'>CI job</a>\n\n", *job.WorkflowRunUrl )
103+
summary += fmt.Sprintf(":link: <a href='%v'>CI job</a>\n\n", *job.WorkflowRunUrl)
104104
}
105105

106106
if aiSummary != "FOUR_OH_FOUR" {
@@ -110,7 +110,6 @@ func GenerateChecksSummaryForJob( job *models.DiggerJob) (string, error) {
110110
return summary, nil
111111
}
112112

113-
114113
func UpdateCheckRunForBatch(gh utils.GithubClientProvider, batch *models.DiggerBatch) error {
115114
slog.Info("Updating PR status for batch",
116115
"batchId", batch.ID,
@@ -350,7 +349,6 @@ func UpdateCheckRunForBatch(gh utils.GithubClientProvider, batch *models.DiggerB
350349
return nil
351350
}
352351

353-
354352
// more modern check runs on github have their own page
355353
func UpdateCheckRunForJob(gh utils.GithubClientProvider, job *models.DiggerJob) error {
356354
batch := job.Batch
@@ -425,27 +423,27 @@ func UpdateCheckRunForJob(gh utils.GithubClientProvider, job *models.DiggerJob)
425423
"error", err)
426424
return fmt.Errorf("could not get conclusion for job: %v", err)
427425
}
428-
426+
429427
// Validate status and conclusion before sending to GitHub
430428
validStatuses := map[string]bool{"queued": true, "in_progress": true, "completed": true}
431429
validConclusions := map[string]bool{"": true, "success": true, "failure": true, "neutral": true, "cancelled": true, "timed_out": true, "action_required": true, "skipped": true}
432-
430+
433431
if !validStatuses[status] {
434432
slog.Warn("Invalid Check Run status detected",
435433
"jobId", job.DiggerJobID,
436434
"jobStatus", job.Status,
437435
"checkRunStatus", status,
438436
"validStatuses", []string{"queued", "in_progress", "completed"})
439437
}
440-
438+
441439
if !validConclusions[conclusion] {
442440
slog.Warn("Invalid Check Run conclusion detected",
443441
"jobId", job.DiggerJobID,
444442
"jobStatus", job.Status,
445443
"checkRunConclusion", conclusion,
446444
"validConclusions", []string{"", "success", "failure", "neutral", "cancelled", "timed_out", "action_required", "skipped"})
447445
}
448-
446+
449447
slog.Debug("Preparing to update Check Run for job",
450448
"jobId", job.DiggerJobID,
451449
"jobStatus", job.Status,
@@ -460,12 +458,20 @@ func UpdateCheckRunForJob(gh utils.GithubClientProvider, job *models.DiggerJob)
460458
conclusionPtr = &conclusion
461459
}
462460

461+
// Character limit check - GitHub check run text field has a 65535 character limit
462+
const maxCheckRunTextLength = 65535
463+
cutOffMsg := "\n[Character limit exceeded, output truncated]"
464+
if utf8.RuneCountInString(job.TerraformOutput) > maxCheckRunTextLength {
465+
runes := []rune(job.TerraformOutput)
466+
truncateAt := maxCheckRunTextLength - utf8.RuneCountInString(cutOffMsg)
467+
job.TerraformOutput = string(runes[:truncateAt]) + cutOffMsg
468+
}
469+
463470
text := "" +
464471
"```terraform\n" +
465472
job.TerraformOutput +
466473
"```\n"
467474

468-
469475
var summary = ""
470476
if job.Status == orchestrator_scheduler.DiggerJobSucceeded || job.Status == orchestrator_scheduler.DiggerJobFailed {
471477
summary, err = GenerateChecksSummaryForJob(job)
@@ -474,7 +480,6 @@ func UpdateCheckRunForJob(gh utils.GithubClientProvider, job *models.DiggerJob)
474480
}
475481
}
476482

477-
478483
slog.Debug("Updating PR status for job", "jobId", job.DiggerJobID, "status", status, "conclusion", conclusion)
479484
if isPlan {
480485
title := fmt.Sprintf("%v to create %v to update %v to delete", job.DiggerJobSummary.ResourcesCreated, job.DiggerJobSummary.ResourcesUpdated, job.DiggerJobSummary.ResourcesDeleted)

backend/controllers/projects_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package controllers
22

33
import (
44
"net/http"
5+
"strings"
56
"testing"
7+
"unicode/utf8"
68

79
"github.com/diggerhq/digger/backend/models"
810
"github.com/diggerhq/digger/backend/utils"
@@ -112,3 +114,55 @@ func TestAutomergeWhenBatchIsSuccessfulStatus(t *testing.T) {
112114
assert.NoError(t, err)
113115
assert.True(t, isMergeCalled)
114116
}
117+
118+
func TestCharacterLimit(t *testing.T) {
119+
tests := []struct {
120+
name string
121+
inputLength int
122+
expectTruncate bool
123+
}{
124+
{
125+
name: "under limit - no truncation",
126+
inputLength: 1000,
127+
expectTruncate: false,
128+
},
129+
{
130+
name: "at limit - no truncation",
131+
inputLength: 65535,
132+
expectTruncate: false,
133+
},
134+
{
135+
name: "over limit - truncation applied",
136+
inputLength: 70000,
137+
expectTruncate: true,
138+
},
139+
}
140+
141+
const maxCheckRunTextLength = 65535
142+
cutOffMsg := "\n[Character limit exceeded, output truncated]"
143+
144+
for _, tt := range tests {
145+
t.Run(tt.name, func(t *testing.T) {
146+
input := strings.Repeat("a", tt.inputLength)
147+
148+
result := input
149+
if utf8.RuneCountInString(result) > maxCheckRunTextLength {
150+
runes := []rune(result)
151+
truncateAt := maxCheckRunTextLength - utf8.RuneCountInString(cutOffMsg)
152+
result = string(runes[:truncateAt]) + cutOffMsg
153+
}
154+
155+
if tt.expectTruncate {
156+
assert.Equal(t, maxCheckRunTextLength, utf8.RuneCountInString(result),
157+
"truncated output should be exactly 65535 characters")
158+
assert.True(t, strings.HasSuffix(result, cutOffMsg),
159+
"truncated output should end with cutoff message")
160+
} else {
161+
assert.Equal(t, tt.inputLength, utf8.RuneCountInString(result),
162+
"non-truncated output should maintain original length")
163+
assert.False(t, strings.HasSuffix(result, cutOffMsg),
164+
"non-truncated output should not have cutoff message")
165+
}
166+
})
167+
}
168+
}

0 commit comments

Comments
 (0)