cmd/releasebot: split the release log into multiple posts if necessary

This change splits the release log into parts that are equal to or
smaller than the maximum GitHub comment character limit. It attempts
to break the parts up by the last new line found per part.

Fixes golang/go#45998

Change-Id: Ica35ceff961e7c02a1e37fc1f928705c32007853
Reviewed-on: https://go-review.googlesource.com/c/build/+/318070
Trust: Carlos Amedee <carlos@golang.org>
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/cmd/releasebot/github.go b/cmd/releasebot/github.go
index 50a3ccf..4cd2f9a 100644
--- a/cmd/releasebot/github.go
+++ b/cmd/releasebot/github.go
@@ -235,9 +235,11 @@
 	}
 }
 
+const githubCommentCharacterLimit = 65536 // discovered in golang.org/issue/45998
+
 func postGithubComment(number int, body string) error {
 	if dryRun {
-		return errors.New("attemted write operation in dry-run mode")
+		return errors.New("attempted write operation in dry-run mode")
 	}
 	_, _, err := githubClient.Issues.CreateComment(context.TODO(), projectOwner, projectRepo, number, &github.IssueComment{
 		Body: &body,
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index bb70716..3dc83fd 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -567,9 +567,16 @@
 	if dryRun || w.Security {
 		return
 	}
-	err := postGithubComment(w.ReleaseIssue, body)
-	if err != nil {
-		fmt.Printf("error posting update comment: %v\n", err)
+
+	// Ensure that the entire body can be posted to the issue by splitting it into multiple
+	// GitHub comments if necesary.
+	// golang.org/issue/45998
+	bodyParts := splitLogMessage(body, githubCommentCharacterLimit)
+	for _, b := range bodyParts {
+		err := postGithubComment(w.ReleaseIssue, b)
+		if err != nil {
+			fmt.Printf("error posting update comment: %v\n", err)
+		}
 	}
 }
 
@@ -946,3 +953,25 @@
 		panic(fmt.Errorf("match: query %q is not supported", query))
 	}
 }
+
+// splitLogMessage splits a string into n number of strings of maximum size maxStrLen.
+// It naively attempts to split the string along the bounderies of new line characters in order
+// to make each individual string as readable as possible.
+func splitLogMessage(s string, maxStrLen int) []string {
+	sl := []string{}
+	for len(s) > maxStrLen {
+		end := strings.LastIndex(s[:maxStrLen], "\n")
+		if end == -1 {
+			end = maxStrLen
+		}
+		sl = append(sl, s[:end])
+
+		if string(s[end]) == "\n" {
+			s = s[end+1:]
+		} else {
+			s = s[end:]
+		}
+	}
+	sl = append(sl, s)
+	return sl
+}
diff --git a/cmd/releasebot/main_test.go b/cmd/releasebot/main_test.go
index 7aaefc0..f60716c 100644
--- a/cmd/releasebot/main_test.go
+++ b/cmd/releasebot/main_test.go
@@ -90,3 +90,75 @@
 		})
 	}
 }
+
+func TestSplitLogMessage(t *testing.T) {
+	testCases := []struct {
+		desc   string
+		str    string
+		maxLen int
+		want   []string
+	}{
+		{
+			desc:   "string matches max size",
+			str:    "the quicks",
+			maxLen: 10,
+			want:   []string{"the quicks"},
+		},
+		{
+			desc:   "string greater than max size",
+			str:    "the quick brown fox",
+			maxLen: 10,
+			want:   []string{"the quick ", "brown fox"},
+		},
+		{
+			desc:   "string smaller than max size",
+			str:    "the quick",
+			maxLen: 20,
+			want:   []string{"the quick"},
+		},
+		{
+			desc:   "string matches max size with return",
+			str:    "the quick\n",
+			maxLen: 10,
+			want:   []string{"the quick\n"},
+		},
+		{
+			desc:   "string greater than max size with return",
+			str:    "the quick\n brown fox",
+			maxLen: 10,
+			want:   []string{"the quick", " brown fox"},
+		},
+		{
+			desc:   "string smaller than max size with return",
+			str:    "the \nquick",
+			maxLen: 20,
+			want:   []string{"the \nquick"},
+		},
+		{
+			desc:   "string is multiples of max size",
+			str:    "000000000011111111112222222222",
+			maxLen: 10,
+			want:   []string{"0000000000", "1111111111", "2222222222"},
+		},
+		{
+			desc:   "string is multiples of max size with return",
+			str:    "000000000\n111111111\n222222222\n",
+			maxLen: 10,
+			want:   []string{"000000000", "111111111", "222222222\n"},
+		},
+		{
+			desc:   "string is multiples of max size with extra return",
+			str:    "000000000\n111111111\n222222222\n\n",
+			maxLen: 10,
+			want:   []string{"000000000", "111111111", "222222222", "\n"},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			got := splitLogMessage(tc.str, tc.maxLen)
+			if !cmp.Equal(tc.want, got) {
+				t.Errorf("splitStringToSlice(%q, %d) =\ngot  \t %#v\nwant \t %#v", tc.str, tc.maxLen, got, tc.want)
+			}
+		})
+	}
+}