Repository: invenia/TagBotGitLab

Issue:
Auto-merge does not work sometimes
We observe this in our private PackageRegistry sometimes. 

Registrator/TagBot will open the PR, a `registrar` GitLab user will approve the PR, but then sometimes _not_ enabled an automatic merge when the pipeline succeeds. 

Since users take for granted that registry PRs will be auto-merged, this can often lead to PRs being left often (and versions not being registered) until someone notices and merges the PR manually.

It is relatively rarely... maybe 1% of the time at a rough guess.

Comments:

Patch:
diff --git a/tagbotgitlab/tagbot.py b/tagbotgitlab/tagbot.py
index 773e041..4d9241e 100644
--- a/tagbotgitlab/tagbot.py
+++ b/tagbotgitlab/tagbot.py
@@ -1,6 +1,7 @@
 import json
 import os
 import re
+import time
 import traceback
 
 import gitlab  # type: ignore
@@ -65,13 +66,25 @@ def handle_open(payload):
     p = client.projects.get(p_id, lazy=True)
     mr_id = get_in(payload, "changes", "iid", "current")
     mr = p.mergerequests.get(mr_id, lazy=True)
+
     print("Approving MR")
     mr.approve()
+
     # Add printing the MR state to assist in debugging cases where the mr.merge() below
     # returns an error
     mr = p.mergerequests.get(mr_id, lazy=False)
     print(mr)
-    print("Merging MR")
+
+    # Add timeout to wait for the head pipeline to be associated properly with the MR
+    # Avoids merge failures
+    timeout = 1
+    while timeout <= 3 and mr.head_pipeline is None:
+        print(f"The MR's head_pipeline is not set - sleeping for {timeout} seconds")
+        time.sleep(timeout)
+        timeout += 1
+        mr = p.mergerequests.get(mr_id, lazy=False)
+
+    print(f"Merging MR {mr}")
     mr.merge(merge_when_pipeline_succeeds=True, should_remove_source_branch=True)
     return "Approved and merged."
 


Test patch:
diff --git a/tests/test_tagbot.py b/tests/test_tagbot.py
index 6cc556d..4d159d8 100644
--- a/tests/test_tagbot.py
+++ b/tests/test_tagbot.py
@@ -1,5 +1,5 @@
 from os import environ as env
-from unittest.mock import Mock, patch
+from unittest.mock import Mock, call, patch
 
 import gitlab
 
@@ -246,8 +246,10 @@ def test_handle_merge():
     p.tags.create.assert_called_once_with({"tag_name": "v0.1.2", "ref": "abcdef"})
 
 
-def test_handle_open():
+@patch("time.sleep", return_value=None)
+def test_handle_open(patched_time_sleep):
     mr = Mock(spec=gitlab.v4.objects.ProjectMergeRequest)
+    mr.head_pipeline = {"id": 62299}
     p = Mock(spec=gitlab.v4.objects.Project)
     p.mergerequests = Mock(spec=gitlab.v4.objects.ProjectMergeRequestManager)
     p.mergerequests.get = Mock(return_value=mr)
@@ -267,6 +269,38 @@ def test_handle_open():
         == "Approved and merged."
     )
     tagbot.client.projects.get.assert_called_once_with(1, lazy=True)
+    # Assert we've gotten the MR twice (the second time is just for printing)
+    calls = [call(None, lazy=True), call(None, lazy=False)]
+    p.mergerequests.get.assert_has_calls(calls)
+    mr.approve.assert_called_once_with()
+    mr.merge.assert_called_once_with(
+        merge_when_pipeline_succeeds=True, should_remove_source_branch=True
+    )
+
+    # Test with head pipeline not set
+    # Note it should still approve and try merging, GitLab API will throw an error if
+    # the merge is not valid but this is mocked so it succeeds fine
+    mr = Mock(spec=gitlab.v4.objects.ProjectMergeRequest)
+    mr.head_pipeline = None
+    p = Mock(spec=gitlab.v4.objects.Project)
+    p.mergerequests = Mock(spec=gitlab.v4.objects.ProjectMergeRequestManager)
+    p.mergerequests.get = Mock(return_value=mr)
+    tagbot.client.projects.get = Mock(return_value=p)
+
+    assert (
+        tagbot.handle_open({"object_attributes": {"source_project_id": 1}})
+        == "Approved and merged."
+    )
+    tagbot.client.projects.get.assert_called_once_with(1, lazy=True)
+    # Assert we've gotten the MR 5 times because of the retries
+    calls = [
+        call(None, lazy=True),
+        call(None, lazy=False),
+        call(None, lazy=False),
+        call(None, lazy=False),
+        call(None, lazy=False),
+    ]
+    p.mergerequests.get.assert_has_calls(calls)
     mr.approve.assert_called_once_with()
     mr.merge.assert_called_once_with(
         merge_when_pipeline_succeeds=True, should_remove_source_branch=True