Repository: runatlantis/atlantis (language: go)

Issue:
Atlantis Apply Fails on GitLab v16.10 When the Merge Request Branch Needs Rebasing
<!--- Please keep this note for the community --->

### Community Note

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request. Searching for pre-existing feature requests helps us consolidate datapoints for identical requirements into a single place, thank you!
* Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request.
* If you are interested in working on this issue or have submitted a pull request, please leave a comment.

<!--- Thank you for keeping this note for the community --->

---

<!---
When filing a bug, please include the following headings if possible.
Any example text in this template can be deleted.
--->

### Overview of the Issue

In Gitlab if:
- The Gitlab version is `16.10.0` or above
- The repo has the `merge commit with semi-linear history` or `fast-forward` merge methods enabled
- The merge request branch is behind the target branch by one or more commits

Then the `atlantis apply` command on the MR will fail with the following error:

```
Apply Failed: Pull request must be mergeable before running apply.
```

### Logs

<details>
  <summary>Logs</summary>

```
[DBUG] Getting GitLab merge request 31
[DBUG] GET /projects/sheather/test/merge_requests/31 returned: 200
[DBUG] Updating GitLab commit status for 'atlantis/apply' to 'pending'
[DBUG] Getting GitLab merge request 31
[DBUG] GET /projects/sheather/test/merge_requests/31 returned: 200
[DBUG] Head pipeline found for merge request 31, source 'merge_request_event'. refTarget 'refs/merge-requests/31/head'
[DBUG] POST /projects/sheather/test/statuses/91a7ea6756cee6e9e62ab72665556c363bbf9af7 returned: 201
[DBUG] Checking if GitLab merge request 31 is approved
[DBUG] GET /projects/sheather/test/merge_requests/31/approvals returned: 200
[DBUG] Checking if GitLab merge request 31 is mergeable
[DBUG] GET /projects/sheather/test/merge_requests/31 returned: 200
[DBUG] GET /projects/2 returned: 200
[DBUG] GET /projects/2/commits/91a7ea6756cee6e9e62ab72665556c363bbf9af7/statuses returned: 200
[DBUG] Checking if GitLab supports detailed merge status
[DBUG] Getting GitLab version
[DBUG] GET /version returned: 200
[DBUG] building config based on server-side config
[DBUG] setting apply_requirements: [mergeable] from repos[1], id: /.*/
[DBUG] setting workflow: "default" from default server config
[DBUG] setting allowed_overrides: [] from default server config
[DBUG] setting custom_policy_check: false from default server config
[DBUG] setting repo_locking: true from default server config
[DBUG] setting policy_check: false from default server config
[DBUG] setting plan_requirements: [] from default server config
[DBUG] setting import_requirements: [] from default server config
[DBUG] setting allow_custom_workflows: false from default server config
[DBUG] setting delete_source_branch_on_merge: false from default server config
[DBUG] Building project command context for apply
[DBUG] Found required_version setting of ">=1.3.9"
[DBUG] Listing Terraform versions available at: https://releases.hashicorp.com/terraform
[INFO] Detected module requires version: 1.7.5
[DBUG] Updating GitLab commit status for 'atlantis/apply: live/aws/123456789012/develop/eu-west-2/stack11/default' to 'pending'
[DBUG] Getting GitLab merge request 31
[DBUG] GET /projects/sheather/test/merge_requests/31 returned: 200
[DBUG] Head pipeline found for merge request 31, source 'merge_request_event'. refTarget 'refs/merge-requests/31/head'
[DBUG] POST /projects/sheather/test/statuses/91a7ea6756cee6e9e62ab72665556c363bbf9af7 returned: 201
[DBUG] Updating GitLab commit status for 'atlantis/apply: live/aws/123456789012/develop/eu-west-2/stack11/default' to 'failed'
[DBUG] Getting GitLab merge request 31
[DBUG] GET /projects/sheather/test/merge_requests/31 returned: 200
[DBUG] Head pipeline found for merge request 31, source 'merge_request_event'. refTarget 'refs/merge-requests/31/head'
[DBUG] POST /projects/sheather/test/statuses/91a7ea6756cee6e9e62ab72665556c363bbf9af7 returned: 201
[EROR] Failure running apply operation: Pull request must be mergeable before running apply.
[DBUG] Hiding previous plan comments for command: 'Apply', directory: ''
[DBUG] Hiding previous command comments on GitLab merge request 31
[DBUG] /projects/sheather/test/merge_requests/31/notes
[DBUG] GET /projects/sheather/test/merge_requests/31/notes returned: 200
[DBUG] Updating merge request note: Repo: 'sheather/test', MR: '31', comment ID: '749'
[DBUG] PUT /projects/sheather/test/merge_requests/31/notes/749 returned: 200
```

</details>

### Environment details

- Atlantis version: `0.27.2`

### Additional Context

Gitlab have introduced the following new detailed merge statuses in MR [!144729](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144729) which was released in `v16.10.0`: 

- `conflict`
- `need_rebase`.

Reference: [GitLab MR Merge Status](https://docs.gitlab.com/ee/api/merge_requests.html#merge-status)

In previous versions of GitLab, merge requests in this state return `mergeable` in the detailed merge status.

The `need_rebase` status needs adding to the list of acceptable merge statuses that Atlantis checks for in the GitLab client `PullIsMergeable` function here: https://github.com/runatlantis/atlantis/blob/890df735887b6dbf19ea50241c378c34c617c15d/server/events/vcs/gitlab_client.go#L358-L361


Discussion / hints:

Fix patch:
diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go
index c4cb837a4e..0a85353adc 100644
--- a/server/events/vcs/gitlab_client.go
+++ b/server/events/vcs/gitlab_client.go
@@ -355,18 +355,28 @@ func (g *GitlabClient) PullIsMergeable(logger logging.SimpleLogging, repo models
 		return false, err
 	}
 
+	if supportsDetailedMergeStatus {
+		logger.Debug("Detailed merge status: '%s'", mr.DetailedMergeStatus)
+	} else {
+		logger.Debug("Merge status: '%s'", mr.MergeStatus) //nolint:staticcheck // Need to reference deprecated field for backwards compatibility
+	}
+
 	if ((supportsDetailedMergeStatus &&
 		(mr.DetailedMergeStatus == "mergeable" ||
 			mr.DetailedMergeStatus == "ci_still_running" ||
-			mr.DetailedMergeStatus == "ci_must_pass")) ||
+			mr.DetailedMergeStatus == "ci_must_pass" ||
+			mr.DetailedMergeStatus == "need_rebase")) ||
 		(!supportsDetailedMergeStatus &&
 			mr.MergeStatus == "can_be_merged")) && //nolint:staticcheck // Need to reference deprecated field for backwards compatibility
 		mr.ApprovalsBeforeMerge <= 0 &&
 		mr.BlockingDiscussionsResolved &&
 		!mr.WorkInProgress &&
 		(allowSkippedPipeline || !isPipelineSkipped) {
+
+		logger.Debug("Merge request is mergeable")
 		return true, nil
 	}
+	logger.Debug("Merge request is not mergeable")
 	return false, nil
 }
 


Test patch:
diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go
index 5c463e85cf..3853698823 100644
--- a/server/events/vcs/gitlab_client_test.go
+++ b/server/events/vcs/gitlab_client_test.go
@@ -358,6 +358,7 @@ func TestGitlabClient_PullIsMergeable(t *testing.T) {
 	noHeadPipelineMR := 2
 	ciMustPassSuccessMR := 3
 	ciMustPassFailureMR := 4
+	needRebaseMR := 5
 
 	pipelineSuccess, err := os.ReadFile("testdata/gitlab-pipeline-success.json")
 	Ok(t, err)
@@ -368,6 +369,9 @@ func TestGitlabClient_PullIsMergeable(t *testing.T) {
 	detailedMergeStatusCiMustPass, err := os.ReadFile("testdata/gitlab-detailed-merge-status-ci-must-pass.json")
 	Ok(t, err)
 
+	detailedMergeStatusNeedRebase, err := os.ReadFile("testdata/gitlab-detailed-merge-status-need-rebase.json")
+	Ok(t, err)
+
 	headPipelineNotAvailable, err := os.ReadFile("testdata/gitlab-head-pipeline-not-available.json")
 	Ok(t, err)
 
@@ -427,6 +431,13 @@ func TestGitlabClient_PullIsMergeable(t *testing.T) {
 			ciMustPassFailureMR,
 			false,
 		},
+		{
+			fmt.Sprintf("%s/apply", vcsStatusName),
+			models.FailedCommitStatus,
+			gitlabServerVersions,
+			needRebaseMR,
+			true,
+		},
 		{
 			fmt.Sprintf("%s/apply: resource/default", vcsStatusName),
 			models.FailedCommitStatus,
@@ -491,6 +502,9 @@ func TestGitlabClient_PullIsMergeable(t *testing.T) {
 						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", ciMustPassFailureMR):
 							w.WriteHeader(http.StatusOK)
 							w.Write(detailedMergeStatusCiMustPass) // nolint: errcheck
+						case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%v", needRebaseMR):
+							w.WriteHeader(http.StatusOK)
+							w.Write(detailedMergeStatusNeedRebase) // nolint: errcheck
 						case fmt.Sprintf("/api/v4/projects/%v", projectID):
 							w.WriteHeader(http.StatusOK)
 							w.Write(projectSuccess) // nolint: errcheck
diff --git a/server/events/vcs/testdata/gitlab-detailed-merge-status-need-rebase.json b/server/events/vcs/testdata/gitlab-detailed-merge-status-need-rebase.json
new file mode 100644
index 0000000000..a37f0e8577
--- /dev/null
+++ b/server/events/vcs/testdata/gitlab-detailed-merge-status-need-rebase.json
@@ -0,0 +1,124 @@
+{
+  "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&d=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",
+  "detailed_merge_status": "need_rebase",
+  "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&d=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
+  }
+}