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: improving apply requirements for mergeable on Gitlab #1675

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 4 additions & 5 deletions runatlantis.io/docs/apply-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,11 @@ a pull request mergeable.
:::

#### GitLab
For GitLab, a merge request will be mergeable if it has no conflicts and if all
required approvers have approved the pull request.
For GitLab, a merge request will be merged if there are no conflicts, no unresolved discussions if it is a project requirement and if all necessary approvers have approved the pull request.

We **do not** check if there are [Unresolved Discussions](https://docs.gitlab.com/ee/user/discussions/#resolve-a-thread) because GitLab doesn't
provide that information in their API response. If you need this feature please
[open an issue](https://github.com/runatlantis/atlantis/issues/new).
For pipelines, if the project requires that pipelines must succeed, all builds except the apply command status will be checked.

For Jobs with allow_failure setting set to true, will be ignored. If the pipeline has been skipped and the project allows merging, it will be marked as mergeable.

#### Bitbucket.org (Bitbucket Cloud) and Bitbucket Server (Stash)
For Bitbucket, we just check if there is a conflict that is preventing a
Expand Down
78 changes: 71 additions & 7 deletions server/events/vcs/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
package vcs

import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"time"

"github.com/runatlantis/atlantis/server/events/yaml"

Expand Down Expand Up @@ -167,11 +169,9 @@ func (g *GitlabClient) PullIsApproved(repo models.Repo, pull models.PullRequest)
// PullIsMergeable returns true if the merge request can be merged.
// In GitLab, there isn't a single field that tells us if the pull request is
// mergeable so for now we check the merge_status and approvals_before_merge
// fields. We aren't checking if there are unresolved discussions or failing
// pipelines because those only block merges if the repo is set to require that.
// fields.
// In order to check if the repo required these, we'd need to make another API
// call to get the repo settings. For now I'm going to leave this as is and if
// some users require checking this as well then we can revisit.
// call to get the repo settings.
// It's also possible that GitLab implements their own "mergeable" field in
// their API in the future.
// See:
Expand All @@ -182,7 +182,33 @@ func (g *GitlabClient) PullIsMergeable(repo models.Repo, pull models.PullRequest
if err != nil {
return false, err
}
if mr.MergeStatus == "can_be_merged" && mr.ApprovalsBeforeMerge <= 0 {

// Get project configuration
project, _, err2 := g.Client.Projects.GetProject(mr.ProjectID, nil)
devlucasc marked this conversation as resolved.
Show resolved Hide resolved
if err2 != nil {
return false, err2
}

// Get Commit Statuses
statuses, _, err3 := g.Client.Commits.GetCommitStatuses(mr.ProjectID, mr.HeadPipeline.SHA, nil)
if err3 != nil {
return false, err3
}

for _, status := range statuses {
if !strings.HasSuffix(status.Name, fmt.Sprintf("/%s", models.ApplyCommand.String())) {
if !status.AllowFailure && project.OnlyAllowMergeIfPipelineSucceeds && status.Status != "success" {
return false, nil
}
}
}

isPipelineSkipped := mr.HeadPipeline.Status == "skipped"
allowSkippedPipeline := project.AllowMergeOnSkippedPipeline && isPipelineSkipped
if mr.MergeStatus == "can_be_merged" &&
mr.ApprovalsBeforeMerge <= 0 &&
mr.BlockingDiscussionsResolved &&
(allowSkippedPipeline || !isPipelineSkipped) {
return true, nil
}
return false, nil
Expand Down Expand Up @@ -213,17 +239,55 @@ func (g *GitlabClient) GetMergeRequest(repoFullName string, pullNum int) (*gitla
return mr, err
}

func (g *GitlabClient) WaitForSuccessPipeline(ctx context.Context, pull models.PullRequest) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

for wait := true; wait; {
select {
case <-ctx.Done():
// validation check time out
cancel()
return //ctx.Err()

default:
mr, _ := g.GetMergeRequest(pull.BaseRepo.FullName, pull.Num)
// check if pipeline has a success state to merge
if mr.HeadPipeline.Status == "success" {
return
}
time.Sleep(time.Second)
}
}
}

// MergePull merges the merge request.
func (g *GitlabClient) MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error {
commitMsg := common.AutomergeCommitMsg
_, _, err := g.Client.MergeRequests.AcceptMergeRequest(

mr, err := g.GetMergeRequest(pull.BaseRepo.FullName, pull.Num)
if err != nil {
return errors.Wrap(
err, "unable to merge merge request, it was not possible to retrieve the merge request")
}
project, _, err2 := g.Client.Projects.GetProject(mr.ProjectID, nil)
if err2 != nil {
return errors.Wrap(
err2, "unable to merge merge request, it was not possible to check the project requirements")
}

if project != nil && project.OnlyAllowMergeIfPipelineSucceeds {
g.WaitForSuccessPipeline(context.Background(), pull)
}

_, _, err3 := g.Client.MergeRequests.AcceptMergeRequest(
pull.BaseRepo.FullName,
pull.Num,
&gitlab.AcceptMergeRequestOptions{
MergeCommitMessage: &commitMsg,
ShouldRemoveSourceBranch: &pullOptions.DeleteSourceBranchOnMerge,
})
return errors.Wrap(err, "unable to merge merge request, it may not be in a mergeable state")
return errors.Wrap(err3, "unable to merge merge request, it may not be in a mergeable state")
}

// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.
Expand Down
8 changes: 8 additions & 0 deletions server/events/vcs/gitlab_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ func TestGitlabClient_MergePull(t *testing.T) {
case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1/merge":
w.WriteHeader(c.code)
w.Write([]byte(c.glResponse)) // nolint: errcheck
case "/api/v4/projects/runatlantis%2Fatlantis/merge_requests/1":
w.WriteHeader(http.StatusOK)
w.Write([]byte(pipelineSuccess)) // nolint: errcheck
case "/api/v4/projects/4580910":
w.WriteHeader(http.StatusOK)
w.Write([]byte(projectSuccess)) // nolint: errcheck
case "/api/v4/":
// Rate limiter requests.
w.WriteHeader(http.StatusOK)
Expand Down Expand Up @@ -254,3 +260,5 @@ func TestGitlabClient_MarkdownPullLink(t *testing.T) {
}

var mergeSuccess = `{"id":22461274,"iid":13,"project_id":4580910,"title":"Update main.tf","description":"","state":"merged","created_at":"2019-01-15T18:27:29.375Z","updated_at":"2019-01-25T17:28:01.437Z","merged_by":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"merged_at":"2019-01-25T17:28:01.459Z","closed_by":null,"closed_at":null,"target_branch":"patch-1","source_branch":"patch-1-merger","upvotes":0,"downvotes":0,"author":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"assignee":null,"source_project_id":4580910,"target_project_id":4580910,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha":"c9b336f1c71d3e64810b8cfa2abcfab232d6bff6","user_notes_count":0,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":false,"web_url":"https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"subscribed":true,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"diff_refs":{"base_sha":"67cb91d3f6198189f433c045154a885784ba6977","head_sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha":"67cb91d3f6198189f433c045154a885784ba6977"},"merge_error":null,"approvals_before_merge":null}`
var pipelineSuccess = `{"id": 22461274,"iid": 13,"project_id": 4580910,"title": "Update main.tf","description": "","state": "opened","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","merged_by": null,"merged_at": null,"closed_by": null,"closed_at": null,"target_branch": "patch-1","source_branch": "patch-1-merger","user_notes_count": 0,"upvotes": 0,"downvotes": 0,"author": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"assignee": null,"reviewers": [],"source_project_id": 4580910,"target_project_id": 4580910,"labels": [],"work_in_progress": false,"milestone": null,"merge_when_pipeline_succeeds": false,"merge_status": "can_be_merged","sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha": null,"squash_commit_sha": null,"discussion_locked": null,"should_remove_source_branch": null,"force_remove_source_branch": true,"reference": "!13","references": {"short": "!13","relative": "!13","full": "lkysow/atlantis-example!13"},"web_url": "https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats": {"time_estimate": 0,"total_time_spent": 0,"human_time_estimate": null,"human_total_time_spent": null},"squash": true,"task_completion_status": {"count": 0,"completed_count": 0},"has_conflicts": false,"blocking_discussions_resolved": true,"approvals_before_merge": null,"subscribed": false,"changes_count": "1","latest_build_started_at": "2019-01-15T18:27:29.375Z","latest_build_finished_at": "2019-01-25T17:28:01.437Z","first_deployed_to_production_at": null,"pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598"},"head_pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598","before_sha": "0000000000000000000000000000000000000000","tag": false,"yaml_errors": null,"user": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"started_at": "2019-01-15T18:27:29.375Z","finished_at": "2019-01-25T17:28:01.437Z","committed_at": null,"duration": 31,"coverage": null,"detailed_status": {"icon": "status_success","text": "passed","label": "passed","group": "success","tooltip": "passed","has_details": true,"details_path": "/lkysow/atlantis-example/-/pipelines/488598","illustration": null,"favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs": {"base_sha": "67cb91d3f6198189f433c045154a885784ba6977","head_sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha": "67cb91d3f6198189f433c045154a885784ba6977"},"merge_error": null,"first_contribution": false,"user": {"can_merge": true}}`
var projectSuccess = `{"id": 4580910,"description": "","name": "atlantis-example","name_with_namespace": "lkysow / atlantis-example","path": "atlantis-example","path_with_namespace": "lkysow/atlantis-example","created_at": "2018-04-30T13:44:28.367Z","default_branch": "patch-1","tag_list": [],"ssh_url_to_repo": "git@gitlab.com:lkysow/atlantis-example.git","http_url_to_repo": "https://gitlab.com/lkysow/atlantis-example.git","web_url": "https://gitlab.com/lkysow/atlantis-example","readme_url": "https://gitlab.com/lkysow/atlantis-example/-/blob/main/README.md","avatar_url": "https://gitlab.com/uploads/-/system/project/avatar/4580910/avatar.png","forks_count": 0,"star_count": 7,"last_activity_at": "2021-06-29T21:10:43.968Z","namespace": {"id": 1,"name": "lkysow","path": "lkysow","kind": "group","full_path": "lkysow","parent_id": 1,"avatar_url": "/uploads/-/system/group/avatar/1651/platform.png","web_url": "https://gitlab.com/groups/lkysow"},"_links": {"self": "https://gitlab.com/api/v4/projects/4580910","issues": "https://gitlab.com/api/v4/projects/4580910/issues","merge_requests": "https://gitlab.com/api/v4/projects/4580910/merge_requests","repo_branches": "https://gitlab.com/api/v4/projects/4580910/repository/branches","labels": "https://gitlab.com/api/v4/projects/4580910/labels","events": "https://gitlab.com/api/v4/projects/4580910/events","members": "https://gitlab.com/api/v4/projects/4580910/members"},"packages_enabled": false,"empty_repo": false,"archived": false,"visibility": "private","resolve_outdated_diff_discussions": false,"container_registry_enabled": false,"container_expiration_policy": {"cadence": "1d","enabled": false,"keep_n": 10,"older_than": "90d","name_regex": ".*","name_regex_keep": null,"next_run_at": "2021-05-01T13:44:28.397Z"},"issues_enabled": true,"merge_requests_enabled": true,"wiki_enabled": false,"jobs_enabled": true,"snippets_enabled": true,"service_desk_enabled": false,"service_desk_address": null,"can_create_merge_request_in": true,"issues_access_level": "private","repository_access_level": "enabled","merge_requests_access_level": "enabled","forking_access_level": "enabled","wiki_access_level": "disabled","builds_access_level": "enabled","snippets_access_level": "enabled","pages_access_level": "private","operations_access_level": "disabled","analytics_access_level": "enabled","emails_disabled": null,"shared_runners_enabled": true,"lfs_enabled": false,"creator_id": 818,"import_status": "none","import_error": null,"open_issues_count": 0,"runners_token": "1234456","ci_default_git_depth": 50,"ci_forward_deployment_enabled": true,"public_jobs": true,"build_git_strategy": "fetch","build_timeout": 3600,"auto_cancel_pending_pipelines": "enabled","build_coverage_regex": null,"ci_config_path": "","shared_with_groups": [],"only_allow_merge_if_pipeline_succeeds": false,"allow_merge_on_skipped_pipeline": false,"restrict_user_defined_variables": false,"request_access_enabled": true,"only_allow_merge_if_all_discussions_are_resolved": true,"remove_source_branch_after_merge": true,"printing_merge_request_link_enabled": true,"merge_method": "merge","suggestion_commit_message": "","auto_devops_enabled": false,"auto_devops_deploy_strategy": "continuous","autoclose_referenced_issues": true,"repository_storage": "default","approvals_before_merge": 0,"mirror": false,"external_authorization_classification_label": null,"marked_for_deletion_at": null,"marked_for_deletion_on": null,"requirements_enabled": false,"compliance_frameworks": [],"permissions": {"project_access": null,"group_access": {"access_level": 50,"notification_level": 3}}}`