Skip to content

Commit

Permalink
feat: dismiss approvals when planning (#2696)
Browse files Browse the repository at this point in the history
* feat: dismiss approvals when planning

* feat: add pagination and move query in separate method

* tests: add test for dismissing

* refactor: fix linting issue

* implement change requests

* Update cmd/server.go

Co-authored-by: PePe Amengual <jose.amengual@gmail.com>

Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com>
Co-authored-by: PePe Amengual <jose.amengual@gmail.com>
  • Loading branch information
3 people committed Dec 19, 2022
1 parent c00cfa4 commit 01a9a5f
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 0 deletions.
5 changes: 5 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
DisableAutoplanFlag = "disable-autoplan"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
DisableRepoLockingFlag = "disable-repo-locking"
DiscardApprovalOnPlanFlag = "discard-approval-on-plan"
EnablePolicyChecksFlag = "enable-policy-checks"
EnableRegExpCmdFlag = "enable-regexp-cmd"
EnableDiffMarkdownFormat = "enable-diff-markdown-format"
Expand Down Expand Up @@ -414,6 +415,10 @@ var boolFlags = map[string]boolFlag{
DisableRepoLockingFlag: {
description: "Disable atlantis locking repos",
},
DiscardApprovalOnPlanFlag: {
description: "Enables the discarding of approval if a new plan has been executed. Currently only Github is supported",
defaultValue: false,
},
EnablePolicyChecksFlag: {
description: "Enable atlantis to run user defined policy checks. This is explicitly disabled for TFE/TFC backends since plan files are inaccessible.",
defaultValue: false,
Expand Down
1 change: 1 addition & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var testFlags = map[string]interface{}{
DisableApplyFlag: true,
DisableMarkdownFoldingFlag: true,
DisableRepoLockingFlag: true,
DiscardApprovalOnPlanFlag: true,
GHHostnameFlag: "ghhostname",
GHTokenFlag: "token",
GHUserFlag: "user",
Expand Down
4 changes: 4 additions & 0 deletions server/events/plan_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ func (p *PlanCommandRunner) run(ctx *command.Context, cmd *CommentCommand) {
baseRepo := ctx.Pull.BaseRepo
pull := ctx.Pull

if err = p.pullUpdater.VCSClient.DiscardReviews(baseRepo, pull); err != nil {
ctx.Log.Err("failed to remove approvals: %s", err)
}

if err = p.commitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, command.Plan); err != nil {
ctx.Log.Warn("unable to update commit status: %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 @@ -166,6 +166,11 @@ func (g *AzureDevopsClient) PullIsApproved(repo models.Repo, pull models.PullReq
return approvalStatus, nil
}

func (g *AzureDevopsClient) DiscardReviews(repo models.Repo, pull models.PullRequest) error {
// TODO implement
return nil
}

// PullIsMergeable returns true if the merge request can be merged.
func (g *AzureDevopsClient) PullIsMergeable(repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
owner, project, repoName := SplitAzureDevopsRepoFullName(repo.FullName)
Expand Down
5 changes: 5 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ func (b *Client) prepRequest(method string, path string, body io.Reader) (*http.
return req, nil
}

func (b *Client) DiscardReviews(repo models.Repo, pull models.PullRequest) error {
// TODO implement
return nil
}

func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]byte, error) {
req, err := b.prepRequest(method, path, reqBody)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ func (b *Client) PullIsApproved(repo models.Repo, pull models.PullRequest) (appr
return approvalStatus, nil
}

func (b *Client) DiscardReviews(repo models.Repo, pull models.PullRequest) error {
// TODO implement
return nil
}

// PullIsMergeable returns true if the merge request has no conflicts and can be merged.
func (b *Client) PullIsMergeable(repo models.Repo, pull models.PullRequest, vcsstatusname string) (bool, error) {
projectKey, err := b.GetProjectKey(repo.Name, repo.SanitizedCloneURL)
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 @@ -36,6 +36,7 @@ type Client interface {
// url is an optional link that users should click on for more information
// about this status.
UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error
DiscardReviews(repo models.Repo, pull models.PullRequest) error
MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error
MarkdownPullLink(pull models.PullRequest) (string, error)
GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error)
Expand Down
103 changes: 103 additions & 0 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ import (
// by GitHub.
const maxCommentLength = 65536

var (
clientMutationID = githubv4.NewString("atlantis")
pullRequestDismissalMessage = *githubv4.NewString("Dismissing reviews because of plan changes")
)

// GithubClient is used to perform GitHub actions.
type GithubClient struct {
user string
Expand All @@ -59,6 +64,19 @@ type GithubAppTemporarySecrets struct {
URL string
}

type GithubReview struct {
ID githubv4.ID
SubmittedAt githubv4.DateTime
Author struct {
Login githubv4.String
}
}

type GithubPRReviewSummary struct {
ReviewDecision githubv4.String
Reviews []GithubReview
}

// NewGithubClient returns a valid GitHub client.
func NewGithubClient(hostname string, credentials GithubCredentials, config GithubConfig, logger logging.SimpleLogging) (*GithubClient, error) {
transport, err := credentials.Client()
Expand Down Expand Up @@ -241,6 +259,58 @@ func (g *GithubClient) HidePrevCommandComments(repo models.Repo, pullNum int, co
return nil
}

// getPRReviews Retrieves PR reviews for a pull request on a specific repository.
// The reviews are being retrieved using pages with the size of 10 reviews.
func (g *GithubClient) getPRReviews(repo models.Repo, pull models.PullRequest) (GithubPRReviewSummary, error) {
var query struct {
Repository struct {
PullRequest struct {
ReviewDecision githubv4.String
Reviews struct {
Nodes []GithubReview
// contains pagination information
PageInfo struct {
EndCursor githubv4.String
HasNextPage githubv4.Boolean
}
} `graphql:"reviews(first: $entries, after: $reviewCursor, states: $reviewState)"`
} `graphql:"pullRequest(number: $number)"`
} `graphql:"repository(owner: $owner, name: $name)"`
}

variables := map[string]interface{}{
"owner": githubv4.String(repo.Owner),
"name": githubv4.String(repo.Name),
"number": githubv4.Int(pull.Num),
"entries": githubv4.Int(10),
"reviewState": []githubv4.PullRequestReviewState{githubv4.PullRequestReviewStateApproved},
"reviewCursor": (*githubv4.String)(nil), // initialize the reviewCursor with null
}

var allReviews []GithubReview
for {
err := g.v4Client.Query(g.ctx, &query, variables)
if err != nil {
return GithubPRReviewSummary{
query.Repository.PullRequest.ReviewDecision,
allReviews,
}, errors.Wrap(err, "getting reviewDecision")
}

allReviews = append(allReviews, query.Repository.PullRequest.Reviews.Nodes...)
// if we don't have a NextPage pointer, we have requested all pages
if !query.Repository.PullRequest.Reviews.PageInfo.HasNextPage {
break
}
// set the end cursor, so the next batch of reviews is going to be requested and not the same again
variables["reviewCursor"] = githubv4.NewString(query.Repository.PullRequest.Reviews.PageInfo.EndCursor)
}
return GithubPRReviewSummary{
query.Repository.PullRequest.ReviewDecision,
allReviews,
}, nil
}

// PullIsApproved returns true if the pull request was approved.
func (g *GithubClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (approvalStatus models.ApprovalStatus, err error) {
nextPage := 0
Expand Down Expand Up @@ -273,6 +343,39 @@ func (g *GithubClient) PullIsApproved(repo models.Repo, pull models.PullRequest)
return approvalStatus, nil
}

// DiscardReviews dismisses all reviews on a pull request
func (g *GithubClient) DiscardReviews(repo models.Repo, pull models.PullRequest) error {
reviewStatus, err := g.getPRReviews(repo, pull)
if err != nil {
return err
}

// https://docs.github.com/en/graphql/reference/input-objects#dismisspullrequestreviewinput
var mutation struct {
DismissPullRequestReview struct {
PullRequestReview struct {
ID githubv4.ID
}
} `graphql:"dismissPullRequestReview(input: $input)"`
}

// dismiss every review one by one.
// currently there is no way to dismiss them in one mutation.
for _, review := range reviewStatus.Reviews {
input := githubv4.DismissPullRequestReviewInput{
PullRequestReviewID: review.ID,
Message: pullRequestDismissalMessage,
ClientMutationID: clientMutationID,
}
mutationResult := &mutation
err := g.v4Client.Mutate(g.ctx, mutationResult, input, nil)
if err != nil {
return errors.Wrap(err, "dismissing reviewDecision")
}
}
return nil
}

// isRequiredCheck is a helper function to determine if a check is required or not
func isRequiredCheck(check string, required []string) bool {
//in go1.18 can prob replace this with slices.Contains
Expand Down

0 comments on commit 01a9a5f

Please sign in to comment.