Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide "plan" comments #994

Merged
merged 14 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
GitlabTokenFlag = "gitlab-token"
GitlabUserFlag = "gitlab-user"
GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec
HidePrevPlanComments = "hide-prev-plan-comments"
LogLevelFlag = "log-level"
PortFlag = "port"
RepoConfigFlag = "repo-config"
Expand Down Expand Up @@ -252,6 +253,11 @@ var boolFlags = map[string]boolFlag{
description: "Disable \"atlantis apply\" command so a specific project/workspace/directory has to be specified for applies.",
defaultValue: false,
},
HidePrevPlanComments: {
description: "Hide previous plan comments to reduce clutter in the PR. " +
"VCS support is limited to: GitHub.",
defaultValue: false,
},
RequireApprovalFlag: {
description: "Require pull requests to be \"Approved\" before allowing the apply command to be run.",
defaultValue: false,
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/runatlantis/atlantis
go 1.14

require (
github.com/Laisky/graphql v1.0.4
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Masterminds/sprig v2.15.0+incompatible
github.com/aokoli/goutils v1.0.1 // indirect
Expand Down Expand Up @@ -41,6 +42,8 @@ require (
github.com/pelletier/go-toml v1.0.0 // indirect
github.com/petergtz/pegomock v2.7.0+incompatible
github.com/pkg/errors v0.8.0
github.com/shurcooL/githubv4 v0.0.0-20191127044304-8f68eb5628d0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
github.com/sirupsen/logrus v1.2.0 // indirect
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1 // indirect
github.com/spf13/cast v1.1.0 // indirect
Expand Down
32 changes: 8 additions & 24 deletions go.sum

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,13 @@ Values are chosen in this order:
```
View help.

* ### `--hide-prev-plan-comments`
```bash
atlantis server --hide-prev-plan-comments
```
Hide previous plan comments to declutter PRs. This is only supported in
GitHub currently.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lkysow I'm trying to figure out how to set a boolean flag like this via environment variable - before I go testing random settings (1, true, etc) maybe there could be some docs added to show how to do this?

Maybe a more generic bit in the "environment variable" section of the config docs would handle it for any boolean flags?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can set it to the string false or true, e.g. ATLANTIS_HIDE_PREV_PLAN_COMMENTS=true. Yeah I think adding this to https://www.runatlantis.io/docs/server-configuration.html#environment-variables would be great. PRs welcome please!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lkysow opened a PR for the docs: #1017


* ### `--log-level`
```bash
atlantis server --log-level="<debug|info|warn|error>"
Expand Down
11 changes: 11 additions & 0 deletions server/events/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ type DefaultCommandRunner struct {
// this in our error message back to the user on a forked PR so they know
// how to enable this functionality.
AllowForkPRsFlag string
// HidePrevPlanComments will hide previous plan comments to declutter PRs.
HidePrevPlanComments bool
// SilenceForkPRErrors controls whether to comment on Fork PRs when AllowForkPRs = False
SilenceForkPRErrors bool
// SilenceForkPRErrorsFlag is the name of the flag that controls fork PR's. We use
Expand Down Expand Up @@ -429,6 +431,15 @@ func (c *DefaultCommandRunner) updatePull(ctx *CommandContext, command PullComma
ctx.Log.Warn(res.Failure)
}

// HidePrevPlanComments will hide old comments left from previous plan runs to reduce
// clutter in a pull/merge request. This will not delete the comment, since the
// comment trail may be useful in auditing or backtracing problems.
if c.HidePrevPlanComments {
if err := c.VCSClient.HidePrevPlanComments(ctx.BaseRepo, ctx.Pull.Num); err != nil {
ctx.Log.Err("unable to hide old comments: %s", err)
}
}

comment := c.MarkdownRenderer.Render(res, command.CommandName(), ctx.Log.History.String(), command.IsVerbose(), ctx.BaseRepo.VCSHost.Type)
if err := c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment); err != nil {
ctx.Log.Err("unable to comment: %s", err)
Expand Down
5 changes: 5 additions & 0 deletions server/events/vcs/azuredevops_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,12 @@ func (g *AzureDevopsClient) CreateComment(repo models.Repo, pullNum int, comment
return nil
}

func (g *AzureDevopsClient) HidePrevPlanComments(repo models.Repo, pullNum int) error {
return nil
}

// PullIsApproved returns true if the merge request was approved by another reviewer.
// https://docs.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops#require-a-minimum-number-of-reviewers
func (g *AzureDevopsClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) {
owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)

Expand Down
4 changes: 4 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) er
return err
}

func (b *Client) HidePrevPlanComments(repo models.Repo, pullNum int) error {
return nil
}

// PullIsApproved returns true if the merge request was approved.
func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) {
path := fmt.Sprintf("%s/2.0/repositories/%s/pullrequests/%d", b.BaseURL, repo.FullName, pull.Num)
Expand Down
4 changes: 4 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) er
return nil
}

func (b *Client) HidePrevPlanComments(repo models.Repo, pullNum int) error {
return nil
}

// postComment actually posts the comment. It's a helper for CreateComment().
func (b *Client) postComment(repo models.Repo, pullNum int, comment string) error {
bodyBytes, err := json.Marshal(map[string]string{"text": comment})
Expand Down
1 change: 1 addition & 0 deletions server/events/vcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Client interface {
// relative to the repo root, e.g. parent/child/file.txt.
GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error)
CreateComment(repo models.Repo, pullNum int, comment string) error
HidePrevPlanComments(repo models.Repo, pullNum int) error
PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error)
PullIsMergeable(repo models.Repo, pull models.PullRequest) (bool, error)
// UpdateStatus updates the commit status to state for pull. src is the
Expand Down
97 changes: 92 additions & 5 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import (
"net/url"
"strings"

"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs/common"

"github.com/Laisky/graphql"
"github.com/google/go-github/v28/github"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/shurcooL/githubv4"
)

// maxCommentLength is the maximum number of chars allowed in a single comment
Expand All @@ -32,8 +34,10 @@ const maxCommentLength = 65536

// GithubClient is used to perform GitHub actions.
type GithubClient struct {
client *github.Client
ctx context.Context
user string
client *github.Client
v4MutateClient *graphql.Client
ctx context.Context
}

// NewGithubClient returns a valid GitHub client.
Expand All @@ -43,6 +47,8 @@ func NewGithubClient(hostname string, user string, pass string) (*GithubClient,
Password: strings.TrimSpace(pass),
}
client := github.NewClient(tp.Client())
graphqlURL := "https://api.github.com/graphql"

// If we're using github.com then we don't need to do any additional configuration
// for the client. It we're using Github Enterprise, then we need to manually
// set the base url for the API.
Expand All @@ -53,11 +59,31 @@ func NewGithubClient(hostname string, user string, pass string) (*GithubClient,
return nil, errors.Wrapf(err, "Invalid github hostname trying to parse %s", baseURL)
}
client.BaseURL = base
graphqlURL = fmt.Sprintf("https://%s/graphql", hostname)
_, err = url.Parse(graphqlURL)
if err != nil {
return nil, errors.Wrapf(err, "Invalid GraphQL github hostname trying to parse %s", graphqlURL)
}
}

// shurcooL's githubv4 library has a client ctor, but it doesn't support schema
// previews, which need custom Accept headers (https://developer.github.com/v4/previews)
// So for now use the graphql client, since the githubv4 library was basically
// a simple wrapper around it. And instead of using shurcooL's graphql lib, use
// Laisky's, since shurcooL's doesn't support custom headers.
// Once the Minimize Comment schema is official, this can revert back to using
// shurcooL's libraries completely.
v4MutateClient := graphql.NewClient(
graphqlURL,
tp.Client(),
graphql.WithHeader("Accept", "application/vnd.github.queen-beryl-preview+json"),
)

return &GithubClient{
client: client,
ctx: context.Background(),
user: user,
client: client,
v4MutateClient: v4MutateClient,
ctx: context.Background(),
}, nil
}

Expand Down Expand Up @@ -113,6 +139,67 @@ func (g *GithubClient) CreateComment(repo models.Repo, pullNum int, comment stri
return nil
}

func (g *GithubClient) HidePrevPlanComments(repo models.Repo, pullNum int) error {
var allComments []*github.IssueComment
nextPage := 0
for {
comments, resp, err := g.client.Issues.ListComments(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueListCommentsOptions{
Sort: "created",
Direction: "asc",
ListOptions: github.ListOptions{Page: nextPage},
})
if err != nil {
return err
}
allComments = append(allComments, comments...)
if resp.NextPage == 0 {
break
}
nextPage = resp.NextPage
}

for _, comment := range allComments {
// Using a case insensitive compare here because usernames aren't case
// sensitive and users may enter their atlantis users with different
// cases.
if comment.User != nil && !strings.EqualFold(comment.User.GetLogin(), g.user) {
continue
}
// Crude filtering: The comment templates typically include the command name
// somewhere in the first line. It's a bit of an assumption, but seems like
// a reasonable one, given we've already filtered the comments by the
// configured Atlantis user.
body := strings.Split(comment.GetBody(), "\n")
if len(body) == 0 {
continue
}
firstLine := strings.ToLower(body[0])
if !strings.Contains(firstLine, models.PlanCommand.String()) {
continue
}
var m struct {
MinimizeComment struct {
MinimizedComment struct {
IsMinimized githubv4.Boolean
MinimizedReason githubv4.String
ViewerCanMinimize githubv4.Boolean
}
} `graphql:"minimizeComment(input:$input)"`
}
input := map[string]interface{}{
"input": githubv4.MinimizeCommentInput{
Classifier: githubv4.ReportedContentClassifiersOutdated,
SubjectID: comment.GetNodeID(),
},
}
if err := g.v4MutateClient.Mutate(g.ctx, &m, input); err != nil {
return errors.Wrapf(err, "minimize comment %s", comment.GetNodeID())
}
}

return nil
}

// PullIsApproved returns true if the pull request was approved.
func (g *GithubClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) {
nextPage := 0
Expand Down
Loading