Repository: easybuilders/easybuild-framework

Issue:
Better handling of status checks required in --merge-pr
I saw a traceback when running `--merge-pr` on a PR that needed to be re-synced with `develop`:
```
Review OK, merging pull request!

Adding comment to easybuild-easyconfigs issue #10401: 'Going in, thanks @boegel!'
Traceback (most recent call last):
  File "/usr/lib64/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/usr/lib64/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/main.py", line 502, in <module>
    main()
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/main.py", line 246, in main
    merge_pr(options.merge_pr)
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/tools/github.py", line 1269, in merge_pr
    github_api_put_request(merge_url, github_user, body=body)
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/tools/github.py", line 275, in github_api_put_request
    status, data = url.put(**kwargs)
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/base/rest.py", line 145, in put
    return self.request(self.PUT, url, json.dumps(body), headers, content_type='application/json')
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/base/rest.py", line 174, in request
    conn = self.get_connection(method, url, body, headers)
  File "/eb/maint/software/EasyBuild/4.1.2/lib/python2.7/site-packages/easybuild/base/rest.py", line 215, in get_connection
    connection = self.opener.open(request)
  File "/usr/lib64/python2.7/urllib2.py", line 437, in open
    response = meth(req, response)
  File "/usr/lib64/python2.7/urllib2.py", line 550, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib64/python2.7/urllib2.py", line 475, in error
    return self._call_chain(*args)
  File "/usr/lib64/python2.7/urllib2.py", line 409, in _call_chain
    result = func(*args)
  File "/usr/lib64/python2.7/urllib2.py", line 558, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 405: Method Not Allowed
```


Discussion:

Resolving patch:
diff --git a/easybuild/tools/github.py b/easybuild/tools/github.py
--- a/easybuild/tools/github.py
+++ b/easybuild/tools/github.py
@@ -1136,11 +1136,20 @@ def not_eligible(msg):
         if not test_report_found:
             res = not_eligible(msg_tmpl % "(no test reports found)")
 
-    # check for approved review
+    # check for requested changes and approved review
     approved_review_by = []
+    changes_requested_by = []
     for review in pr_data['reviews']:
         if review['state'] == 'APPROVED':
             approved_review_by.append(review['user']['login'])
+        if review['state'] == 'CHANGES_REQUESTED':
+            changes_requested_by.append(review['user']['login'])
+
+    msg_tmpl = "* no pending change requests: %s"
+    if changes_requested_by:
+        res = not_eligible(msg_tmpl % 'FAILED (changes requested by %s)' % ', '.join(changes_requested_by))
+    else:
+        print_msg(msg_tmpl % 'OK', prefix=False)
 
     msg_tmpl = "* approved review: %s"
     if approved_review_by:
@@ -1155,6 +1164,16 @@ def not_eligible(msg):
     else:
         res = not_eligible(msg_tmpl % 'no milestone found')
 
+    # check github mergeable state
+    msg_tmpl = "* mergeable state is clean: %s"
+    if pr_data['merged']:
+        print_msg(msg_tmpl % "PR is already merged", prefix=False)
+    elif pr_data['mergeable_state'] == GITHUB_MERGEABLE_STATE_CLEAN:
+        print_msg(msg_tmpl % "OK", prefix=False)
+    else:
+        reason = "FAILED (mergeable state is '%s')" % pr_data['mergeable_state']
+        res = not_eligible(msg_tmpl % reason)
+
     return res
 
 


Test patch:
diff --git a/test/framework/github.py b/test/framework/github.py
--- a/test/framework/github.py
+++ b/test/framework/github.py
@@ -42,7 +42,8 @@
 from easybuild.tools.config import build_option, module_classes
 from easybuild.tools.configobj import ConfigObj
 from easybuild.tools.filetools import read_file, write_file
-from easybuild.tools.github import GITHUB_EASYCONFIGS_REPO, GITHUB_EASYBLOCKS_REPO, VALID_CLOSE_PR_REASONS
+from easybuild.tools.github import GITHUB_EASYCONFIGS_REPO, GITHUB_EASYBLOCKS_REPO, GITHUB_MERGEABLE_STATE_CLEAN
+from easybuild.tools.github import VALID_CLOSE_PR_REASONS
 from easybuild.tools.github import pick_default_branch
 from easybuild.tools.testing import post_pr_test_report, session_state
 from easybuild.tools.py2vs3 import HTTPError, URLError, ascii_letters
@@ -654,7 +655,9 @@ def run_check(expected_result=False):
             'issue_comments': [],
             'milestone': None,
             'number': '1234',
-            'reviews': [],
+            'merged': False,
+            'mergeable_state': 'unknown',
+            'reviews': [{'state': 'CHANGES_REQUESTED', 'user': {'login': 'boegel'}}],
         }
 
         test_result_warning_template = "* test suite passes: %s => not eligible for merging!"
@@ -714,10 +717,12 @@ def run_check(expected_result=False):
         pr_data['issue_comments'].insert(2, {'body': 'lgtm'})
         run_check()
 
-        pr_data['reviews'].append({'state': 'CHANGES_REQUESTED', 'user': {'login': 'boegel'}})
+        expected_warning = "* no pending change requests: FAILED (changes requested by boegel)"
+        expected_warning += " => not eligible for merging!"
         run_check()
 
-        pr_data['reviews'].append({'state': 'APPROVED', 'user': {'login': 'boegel'}})
+        pr_data['reviews'] = [{'state': 'APPROVED', 'user': {'login': 'boegel'}}]
+        expected_stdout += "* no pending change requests: OK\n"
         expected_stdout += "* approved review: OK (by boegel)\n"
         expected_warning = ''
         run_check()
@@ -729,6 +734,13 @@ def run_check(expected_result=False):
         pr_data['milestone'] = {'title': '3.3.1'}
         expected_stdout += "* milestone is set: OK (3.3.1)\n"
 
+        # mergeable state must be clean
+        expected_warning = "* mergeable state is clean: FAILED (mergeable state is 'unknown')"
+        run_check()
+
+        pr_data['mergeable_state'] = GITHUB_MERGEABLE_STATE_CLEAN
+        expected_stdout += "* mergeable state is clean: OK\n"
+
         # all checks pass, PR is eligible for merging
         expected_warning = ''
         self.assertEqual(run_check(True), '')
diff --git a/test/framework/options.py b/test/framework/options.py
--- a/test/framework/options.py
+++ b/test/framework/options.py
@@ -4435,7 +4435,9 @@ def test_merge_pr(self):
             "Checking eligibility of easybuilders/easybuild-easyconfigs PR #4781 for merging...",
             "* test suite passes: OK",
             "* last test report is successful: OK",
+            "* no pending change requests: OK",
             "* milestone is set: OK (3.3.1)",
+            "* mergeable state is clean: PR is already merged",
         ])
         expected_stderr = '\n'.join([
             "* targets some_branch branch: FAILED; found 'develop' => not eligible for merging!",
@@ -4457,8 +4459,10 @@ def test_merge_pr(self):
             "* targets develop branch: OK",
             "* test suite passes: OK",
             "* last test report is successful: OK",
+            "* no pending change requests: OK",
             "* approved review: OK (by wpoely86)",
             "* milestone is set: OK (3.3.1)",
+            "* mergeable state is clean: PR is already merged",
             '',
             "Review OK, merging pull request!",
             '',
@@ -4483,8 +4487,10 @@ def test_merge_pr(self):
             "Checking eligibility of easybuilders/easybuild-easyblocks PR #1206 for merging...",
             "* targets develop branch: OK",
             "* test suite passes: OK",
+            "* no pending change requests: OK",
             "* approved review: OK (by migueldiascosta)",
             "* milestone is set: OK (3.3.1)",
+            "* mergeable state is clean: PR is already merged",
             '',
             "Review OK, merging pull request!",
         ])