Skip to content

Commit cf12f97

Browse files
committed
Add --max-comments-per-command configuration
Allow Atlantis administrators to cap the number of VCS comments that will be created for a single command. (The default value is 0, which means unlimited comments.) If you're trying something like `TF_LOG=debug`, Atlantis can produce so many comments that it becomes challenging to read your PR, or worse still, your VCS might rate-limit Atlantis, effectively breaking your ability to use it for an extended period of time. While this PR does not change the default behavior, it's probably a good idea to set this flag to something like 10. Fixes #416.
1 parent febed33 commit cf12f97

15 files changed

+111
-50
lines changed

cmd/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const (
106106
LockingDBType = "locking-db-type"
107107
LogLevelFlag = "log-level"
108108
MarkdownTemplateOverridesDirFlag = "markdown-template-overrides-dir"
109+
MaxCommentsPerCommand = "max-comments-per-command"
109110
ParallelPoolSize = "parallel-pool-size"
110111
StatsNamespace = "stats-namespace"
111112
AllowDraftPRs = "allow-draft-prs"
@@ -591,6 +592,10 @@ var intFlags = map[string]intFlag{
591592
" If merge base is further behind than this number of commits from any of branches heads, full fetch will be performed.",
592593
defaultValue: DefaultCheckoutDepth,
593594
},
595+
MaxCommentsPerCommand: {
596+
description: "If non-zero, the maximum number of comments to split command output into before truncating.",
597+
defaultValue: 0,
598+
},
594599
ParallelPoolSize: {
595600
description: "Max size of the wait group that runs parallel plans and applies (if enabled).",
596601
defaultValue: DefaultParallelPoolSize,

runatlantis.io/docs/server-configuration.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,18 @@ This is useful when you have many projects and want to keep the pull request cle
693693

694694
Defaults to the atlantis home directory `/home/atlantis/.markdown_templates/` in `/$HOME/.markdown_templates`.
695695

696+
### `--max-comments-per-command`
697+
```bash
698+
atlantis server --max-comments-per-command=10
699+
# or
700+
ATLANTIS_MAX_COMMENTS_PER_COMMAND=10
701+
```
702+
When a command's output is large, Atlantis automatically splits it into multiple comments with a maximum size based
703+
on the specific VCS. By default, Atlantis will post enough comments to include the full command output. If your
704+
command has a huge amount of output (eg, from setting `TF_LOG=debug`), the amount of comments may be enough to make
705+
it challenging to read your PR, or it may even cause your VCS to rate limit Atlantis. Set this option to a non-zero
706+
number to make Atlantis truncate output after this many comments.
707+
696708
### `--parallel-apply`
697709
```bash
698710
atlantis server --parallel-apply

server/controllers/github_app_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ func (g *GithubAppController) ExchangeCode(w http.ResponseWriter, r *http.Reques
5656
g.Logger.Debug("Exchanging GitHub app code for app credentials")
5757
creds := &vcs.GithubAnonymousCredentials{}
5858
config := vcs.GithubConfig{}
59-
client, err := vcs.NewGithubClient(g.GithubHostname, creds, config, g.Logger)
59+
// This client does not post comments, so we don't need to configure it with maxCommentsPerCommand.
60+
client, err := vcs.NewGithubClient(g.GithubHostname, creds, config, 0, g.Logger)
6061
if err != nil {
6162
g.respond(w, logging.Error, http.StatusInternalServerError, "Failed to exchange code for github app: %s", err)
6263
return

server/events/vcs/azuredevops_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (g *AzureDevopsClient) CreateComment(repo models.Repo, pullNum int, comment
106106
// or tested limit in Azure DevOps.
107107
const maxCommentLength = 150000
108108

109-
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart)
109+
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart, 0, "")
110110
owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)
111111

112112
for i := range comments {

server/events/vcs/bitbucketserver/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (b *Client) GetProjectKey(repoName string, cloneURL string) (string, error)
136136
func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string, command string) error {
137137
sepEnd := "\n```\n**Warning**: Output length greater than max comment size. Continued in next comment."
138138
sepStart := "Continued from previous comment.\n```diff\n"
139-
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart)
139+
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart, 0, "")
140140
for _, c := range comments {
141141
if err := b.postComment(repo, pullNum, c); err != nil {
142142
return err

server/events/vcs/common/common.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,32 @@ func AutomergeCommitMsg(pullNum int) string {
1515
// SplitComment splits comment into a slice of comments that are under maxSize.
1616
// It appends sepEnd to all comments that have a following comment.
1717
// It prepends sepStart to all comments that have a preceding comment.
18-
func SplitComment(comment string, maxSize int, sepEnd string, sepStart string) []string {
18+
// If maxCommentsPerCommand is non-zero, it never returns more than maxCommentsPerCommand
19+
// comments, and it appends truncationFooter to the final comment if it would have
20+
// produced more comments.
21+
func SplitComment(comment string, maxSize int, sepEnd string, sepStart string, maxCommentsPerCommand int, truncationFooter string) []string {
1922
if len(comment) <= maxSize {
2023
return []string{comment}
2124
}
2225

23-
maxWithSep := maxSize - len(sepEnd) - len(sepStart)
26+
// No comment contains both sepEnd and truncationFooter, so we only have to count their max.
27+
maxWithSep := maxSize - max(len(sepEnd), len(truncationFooter)) - len(sepStart)
2428
var comments []string
25-
numComments := int(math.Ceil(float64(len(comment)) / float64(maxWithSep)))
29+
numPotentialComments := int(math.Ceil(float64(len(comment)) / float64(maxWithSep)))
30+
var numComments int
31+
if maxCommentsPerCommand == 0 {
32+
numComments = numPotentialComments
33+
} else {
34+
numComments = min(numPotentialComments, maxCommentsPerCommand)
35+
}
36+
2637
for i := 0; i < numComments; i++ {
2738
upTo := min(len(comment), (i+1)*maxWithSep)
2839
portion := comment[i*maxWithSep : upTo]
2940
if i < numComments-1 {
3041
portion += sepEnd
42+
} else if i == numComments-1 && numComments < numPotentialComments {
43+
portion += truncationFooter
3144
}
3245
if i > 0 {
3346
portion = sepStart + portion
@@ -43,3 +56,10 @@ func min(a, b int) int {
4356
}
4457
return b
4558
}
59+
60+
func max(a, b int) int {
61+
if a > b {
62+
return a
63+
}
64+
return b
65+
}

server/events/vcs/common/common_test.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424
// If under the maximum number of chars, we shouldn't split the comments.
2525
func TestSplitComment_UnderMax(t *testing.T) {
2626
comment := "comment under max size"
27-
split := common.SplitComment(comment, len(comment)+1, "sepEnd", "sepStart")
27+
split := common.SplitComment(comment, len(comment)+1, "sepEnd", "sepStart", 0, "")
2828
Equals(t, []string{comment}, split)
2929
}
3030

@@ -34,7 +34,7 @@ func TestSplitComment_TwoComments(t *testing.T) {
3434
comment := strings.Repeat("a", 1000)
3535
sepEnd := "-sepEnd"
3636
sepStart := "-sepStart"
37-
split := common.SplitComment(comment, len(comment)-1, sepEnd, sepStart)
37+
split := common.SplitComment(comment, len(comment)-1, sepEnd, sepStart, 0, "")
3838

3939
expCommentLen := len(comment) - len(sepEnd) - len(sepStart) - 1
4040
expFirstComment := comment[:expCommentLen]
@@ -51,7 +51,7 @@ func TestSplitComment_FourComments(t *testing.T) {
5151
sepEnd := "-sepEnd"
5252
sepStart := "-sepStart"
5353
max := (len(comment) / 4) + len(sepEnd) + len(sepStart)
54-
split := common.SplitComment(comment, max, sepEnd, sepStart)
54+
split := common.SplitComment(comment, max, sepEnd, sepStart, 0, "")
5555

5656
expMax := len(comment) / 4
5757
Equals(t, []string{
@@ -61,6 +61,23 @@ func TestSplitComment_FourComments(t *testing.T) {
6161
sepStart + comment[expMax*3:]}, split)
6262
}
6363

64+
func TestSplitComment_Limited(t *testing.T) {
65+
comment := strings.Repeat("a", 1000)
66+
sepEnd := "-sepEnd"
67+
sepStart := "-sepStart"
68+
truncationFooter := "-truncated"
69+
max := (len(comment) / 8) + max(len(sepEnd), len(truncationFooter)) + len(sepStart)
70+
split := common.SplitComment(comment, max, sepEnd, sepStart, 5, truncationFooter)
71+
72+
expMax := len(comment) / 8
73+
Equals(t, []string{
74+
comment[:expMax] + sepEnd,
75+
sepStart + comment[expMax:expMax*2] + sepEnd,
76+
sepStart + comment[expMax*2:expMax*3] + sepEnd,
77+
sepStart + comment[expMax*3:expMax*4] + sepEnd,
78+
sepStart + comment[expMax*4:expMax*5] + truncationFooter}, split)
79+
}
80+
6481
func TestAutomergeCommitMsg(t *testing.T) {
6582
tests := []struct {
6683
name string

server/events/vcs/gh_app_creds_rotator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func Test_githubAppTokenRotator_GenerateJob(t *testing.T) {
1919
Ok(t, err)
2020

2121
anonCreds := &vcs.GithubAnonymousCredentials{}
22-
anonClient, err := vcs.NewGithubClient(testServer, anonCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t))
22+
anonClient, err := vcs.NewGithubClient(testServer, anonCreds, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
2323
Ok(t, err)
2424
tempSecrets, err := anonClient.ExchangeCode("good-code")
2525
Ok(t, err)

server/events/vcs/github_client.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ var (
4141

4242
// GithubClient is used to perform GitHub actions.
4343
type GithubClient struct {
44-
user string
45-
client *github.Client
46-
v4Client *githubv4.Client
47-
ctx context.Context
48-
logger logging.SimpleLogging
49-
config GithubConfig
44+
user string
45+
client *github.Client
46+
v4Client *githubv4.Client
47+
ctx context.Context
48+
logger logging.SimpleLogging
49+
config GithubConfig
50+
maxCommentsPerCommand int
5051
}
5152

5253
// GithubAppTemporarySecrets holds app credentials obtained from github after creation.
@@ -77,7 +78,7 @@ type GithubPRReviewSummary struct {
7778
}
7879

7980
// NewGithubClient returns a valid GitHub client.
80-
func NewGithubClient(hostname string, credentials GithubCredentials, config GithubConfig, logger logging.SimpleLogging) (*GithubClient, error) {
81+
func NewGithubClient(hostname string, credentials GithubCredentials, config GithubConfig, maxCommentsPerCommand int, logger logging.SimpleLogging) (*GithubClient, error) {
8182
transport, err := credentials.Client()
8283
if err != nil {
8384
return nil, errors.Wrap(err, "error initializing github authentication transport")
@@ -107,12 +108,13 @@ func NewGithubClient(hostname string, credentials GithubCredentials, config Gith
107108
return nil, errors.Wrap(err, "getting user")
108109
}
109110
return &GithubClient{
110-
user: user,
111-
client: client,
112-
v4Client: v4Client,
113-
ctx: context.Background(),
114-
logger: logger,
115-
config: config,
111+
user: user,
112+
client: client,
113+
v4Client: v4Client,
114+
ctx: context.Background(),
115+
logger: logger,
116+
config: config,
117+
maxCommentsPerCommand: maxCommentsPerCommand,
116118
}, nil
117119
}
118120

@@ -187,7 +189,10 @@ func (g *GithubClient) CreateComment(repo models.Repo, pullNum int, comment stri
187189
"```diff\n"
188190
}
189191

190-
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart)
192+
truncationFooter := "\n```\n</details>" +
193+
"\n<br>\n\n**Warning**: Command output is larger than the maximum number of comments per command. Output truncated."
194+
195+
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart, g.maxCommentsPerCommand, truncationFooter)
191196
for i := range comments {
192197
_, resp, err := g.client.Issues.CreateComment(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueComment{Body: &comments[i]})
193198
g.logger.Debug("POST /repos/%v/%v/issues/%d/comments returned: %v", repo.Owner, repo.Name, pullNum, resp.StatusCode)

server/events/vcs/github_client_internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import (
2222

2323
// If the hostname is github.com, should use normal BaseURL.
2424
func TestNewGithubClient_GithubCom(t *testing.T) {
25-
client, err := NewGithubClient("github.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, logging.NewNoopLogger(t))
25+
client, err := NewGithubClient("github.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, 0, logging.NewNoopLogger(t))
2626
Ok(t, err)
2727
Equals(t, "https://api.github.com/", client.client.BaseURL.String())
2828
}
2929

3030
// If the hostname is a non-github hostname should use the right BaseURL.
3131
func TestNewGithubClient_NonGithub(t *testing.T) {
32-
client, err := NewGithubClient("example.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, logging.NewNoopLogger(t))
32+
client, err := NewGithubClient("example.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, 0, logging.NewNoopLogger(t))
3333
Ok(t, err)
3434
Equals(t, "https://example.com/api/v3/", client.client.BaseURL.String())
3535
// If possible in the future, test the GraphQL client's URL as well. But at the

0 commit comments

Comments
 (0)