diff --git a/scripts/accept_layer_merge_request.py b/scripts/accept_layer_merge_request.py new file mode 100755 index 0000000000000000000000000000000000000000..ecc0453b9c6b792df645078a30396fac44fc944b --- /dev/null +++ b/scripts/accept_layer_merge_request.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +import common + +import argparse +import logging +import sys +import time +from gitlab import ( + Gitlab, + GitlabGetError, + GitlabMRClosedError, +) +from get_merge_requests import get_merge_requests + +critical_error = ( + "This is a critical error! Please make sure to:\n" + " 1. merge the above-mentioned merge request by hand as soon as possible\n" + " (this is important, otherwise following merge requests will get stuck, too)\n" + " 2. examine why this has happened and fix it in the CI pipeline" +) + + +def get_source_integration_requests( + project, state=None, target_branch=None, commit=None +): + """Get merge request by target branch and optionally commit sha""" + merge_requests = [] + try: + all_merge_requests = project.mergerequests.list( + target_branch=target_branch, + state=state if state else "all", + all=True, + retry_transient_errors=True, + order_by='updated_at' + ) + except GitlabGetError as e: + sys.exit( + "ERROR: could not list merge requests for project '%s': %s" + % (project.name, e) + ) + if commit: + for mr in all_merge_requests: + if mr.sha == commit or mr.squash_commit_sha == commit: + merge_requests.append(mr) + elif all_merge_requests: + merge_requests = all_merge_requests + + # Get complete objects + full_merge_requests = [] + for mr in merge_requests: + mr = project.mergerequests.get(mr.iid, retry_transient_errors=True) + full_merge_requests.append(mr) + + return full_merge_requests[0].source_branch + +def accept_merge_request(project, mr, rebase=False, should_remove_source_branch=True): + """Attempt to merge a merge request, rebase if necessary""" + merged = False + pipeline_pending = False + + while not merged: + # Update merge request before trying to merge it in order to get the latest + # pipeline status + try: + updated_mr = project.mergerequests.get( + id=mr.iid, retry_transient_errors=True + ) + mr = updated_mr + except GitlabGetError as e: + print("WARNING: Could not update merge request object: %s" % e) + + # Try to merge the merge request + try: + mr.merge(should_remove_source_branch=should_remove_source_branch) + if pipeline_pending: + print("") + if mr.state == "merged": + merged = True + else: + if mr.merge_error: + print("Merge error: %s" % mr.merge_error) + else: + print("Merge reported success, but MR state is '%s'" % mr.state) + return False, mr.sha + + except GitlabMRClosedError as e: + # See HTTP error codes for merge requests here: + # https://docs.gitlab.com/ce/api/merge_requests.html#accept-mr + logging.debug("Error from gitlab: %d", e.response_code) + + if e.response_code == 405: + # Not allowed (draft, closed, pipeline pending or failed) + # Contrary to the documentation, this response is also issued when the + # merge failed due to a merge conflict. See GitLab issue: + # https://gitlab.com/gitlab-org/gitlab/-/issues/364102 + + if mr.has_conflicts: + # Merge conflict, automatic rebase not possible + if pipeline_pending: + print("") + print("Merge not possible, has to be rebased manually") + return False, mr.sha + + # If pipeline is running, wait for completion + if not mr.head_pipeline: + # No pipeline created yet + print("No pipeline created yet") + time.sleep(1) + elif mr.head_pipeline["status"] in common.pending_states: + # Pipeline pending + if not pipeline_pending: + print("Waiting for pending pipeline", end="", flush=True) + pipeline_pending = True + print(".", end="", flush=True) + time.sleep(1) + else: + # Merge failed due to some other reason + if pipeline_pending: + print("") + print("Merge not possible for unkown reason") + return False, mr.sha + + elif e.response_code == 406: + # Merge conflict, automatic rebase is possible + if pipeline_pending: + print("") + pipeline_pending = False + print("Merge not possible, but branch can be automatically rebased") + if not rebase: + return False, mr.sha + print("Trying to rebase...") + mr = common.rebase_merge_request(project, mr) + if mr.merge_error: + print("ERROR: rebase not possible\n'%s'" % mr.merge_error) + sys.exit(critical_error) + print("Sucessfully rebased") + + else: + if pipeline_pending: + print("") + print("ERROR: merge not possible: %s" % e) + sys.exit(critical_error) + + return True, mr.sha + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--gitlab-url", + help="""URL to the GitLab instance""", + dest="gitlab_url", + required=True, + ) + parser.add_argument( + "--token", + help="""GitLab REST API private access token""", + dest="token", + required=True, + ) + parser.add_argument( + "--project", + help="""name of the GitLab project""", + dest="project", + required=True, + ) + parser.add_argument( + "--layer-project", + help="""name of the GitLab layer project""", + dest="layer_project", + required=True, + ) + parser.add_argument( + "--target-branch", + help="""target branch of the merge request""", + dest="target_branch", + required=True, + ) + parser.add_argument( + "--layer-target-branch", + help="""target branch of the layer merge request""", + dest="layer_target_branch", + required=True, + ) + parser.add_argument( + "--recipe-name", + help="""recipe name of the merge request""", + dest="recipe_name", + required=True, + ) + parser.add_argument( + "--rebase", + help="""attempt to automatically rebase merge request if necessary""", + dest="rebase", + action="store_true", + required=False, + ) + + args, _ = parser.parse_known_args() + + gitlab = Gitlab(args.gitlab_url, private_token=args.token) + project = common.get_project(gitlab, args.project) + layer_project = common.get_project(gitlab, args.layer_project) + + #Retrieving the source branch from the last MR merged in the project + try: + integration_branch_name = get_source_integration_requests( + project, + target_branch=args.target_branch, + state="merged", + ) + print("This is the source branch of the latest MR merged: ", integration_branch_name) + + #If the last merged branch is the gitlab-ci integration do nothing + if "integrate/gitlab-ci" in integration_branch_name: + print("The job was triggered by a merge of the gitlab-ci projects into the master branch, doing nothing!") + return "" + + except GitlabGetError as e: + sys.exit("Could not get integration branch name for latest project MR: %s" % e) + + integration_branch_name = args.recipe_name + "/" + integration_branch_name + + try: + merge_request = get_merge_requests( + layer_project, + source_branch=integration_branch_name, + target_branch=args.layer_target_branch, + state="opened", + ) + except GitlabGetError as e: + sys.exit("Could not get merge request: %s" % e) + + print("This job is going to merge MR %s in project %s", merge_request[0], layer_project) + if accept_merge_request(layer_project, merge_request[0], rebase=args.rebase): + print("Successfully merged") + else: + sys.exit(1) + + +if __name__ == "__main__": + main()