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

feat: --discard-approval-on-plan to dismiss approvals when planning #2696

Merged
merged 9 commits into from
Dec 19, 2022
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 @@ -407,6 +408,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 on Github is supported",
secustor marked this conversation as resolved.
Show resolved Hide resolved
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
93 changes: 93 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,53 @@ func (g *GithubClient) HidePrevCommandComments(repo models.Repo, pullNum int, co
return nil
}

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), // Null after argument to get first page.
}

var allReviews []GithubReview
for {
secustor marked this conversation as resolved.
Show resolved Hide resolved
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 !query.Repository.PullRequest.Reviews.PageInfo.HasNextPage {
break
}
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 +338,34 @@ func (g *GithubClient) PullIsApproved(repo models.Repo, pull models.PullRequest)
return approvalStatus, nil
}

func (g *GithubClient) DiscardReviews(repo models.Repo, pull models.PullRequest) error {
reviewStatus, err := g.getPRReviews(repo, pull)
if err != nil {
return err
}

var mutation struct {
DismissPullRequestReview struct {
PullRequestReview struct {
ID githubv4.ID
}
} `graphql:"dismissPullRequestReview(input: $input)"`
}
for _, review := range reviewStatus.Reviews {
secustor marked this conversation as resolved.
Show resolved Hide resolved
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
Loading