From b9cb9a29a80973721956fb6c153c5bc9bfb51dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=B6ppner?= <jonas.hoeppner@garz-fricke.com> Date: Thu, 24 Mar 2022 16:38:22 +0100 Subject: [PATCH] CI: Add function to merge the gitlab-ci MRs --- .gitlab-ci.yml | 113 +++++++++++++++++--------- accept_merge_request.py | 2 + common.py | 48 ++++++++++- merge_gitlab_ci.py | 173 ++++++++++++++++++++++++++++++++++++++++ update_submodule.py | 54 +++++++++++++ 5 files changed, 350 insertions(+), 40 deletions(-) create mode 100755 merge_gitlab_ci.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c570c4c..5022ee43 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ stages: - analyze - deploy - check + - merge workflow: rules: @@ -57,12 +58,51 @@ yamllint: # --------------------------------------------------------------------------------------- # Stage: deploy-test # --------------------------------------------------------------------------------------- + +.foobar-manifest-projects: + variables: + PROJECT_ROOT: + ${CI_PROJECT_ROOT_NAMESPACE}/yocto/infrastructure/ci-test + MANIFEST_PROJECT: + ${PROJECT_ROOT}/minimal-manifest + + DEPLOY_TO: + ${PROJECT_ROOT}/minimal-foo + ${PROJECT_ROOT}/minimal-bar + +.manifest-projects: + variables: + PROJECT_ROOT: + ${CI_PROJECT_ROOT_NAMESPACE} + MANIFEST_PROJECT: + ${PROJECT_ROOT}/yocto/manifest + DEPLOY_TO: + ${PROJECT_ROOT}/3rd-party/kuk/uboot-imx-kuk + ${PROJECT_ROOT}/kernel/linux-guf + ${PROJECT_ROOT}/kernel/linux-imx-kuk + ${PROJECT_ROOT}/kernel/modules/egalaxi2c + ${PROJECT_ROOT}/kernel/modules/gfplatdetect + ${PROJECT_ROOT}/tools/gf-emc-test-suite + ${PROJECT_ROOT}/tools/gf-productiontests + ${PROJECT_ROOT}/tools/gfeeprom + ${PROJECT_ROOT}/tools/gfxml2dto + ${PROJECT_ROOT}/tools/guf-show-demo + ${PROJECT_ROOT}/tools/libmdb + ${PROJECT_ROOT}/tools/touchcal-conv + ${PROJECT_ROOT}/tools/xconfig + ${PROJECT_ROOT}/yocto/config + ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-bar + ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-foo + ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-manifest + ${PROJECT_ROOT}/yocto/layers/meta-guf-distro + ${PROJECT_ROOT}/yocto/layers/meta-guf-machine + .deploy: stage: deploy + rules: + - if: $CI_MERGE_REQUEST_IID script: - cd ${CI_PROJECT_DIR} - - if [[ "$CI_COMMIT_BRANCH" == "master" ]]; then - MERGE="--merge"; else MERGE=""; fi - ./deploy_gitlab_ci.py --gitlab-url=${CI_SERVER_URL} --token=${GITBOT_TOKEN} @@ -70,7 +110,6 @@ yamllint: --submodule=.gitlab-ci --revision=${CI_COMMIT_SHA} --verbose - ${MERGE} ${DEPLOY_TO} - ./generate_job_from_template.py @@ -88,51 +127,23 @@ yamllint: - integration.yml deploy: - extends: .deploy + extends: + - .deploy + - .manifest-projects rules: # For now to test the merge step - when: manual allow_failure: true - variables: - PROJECT_ROOT: - ${CI_PROJECT_ROOT_NAMESPACE} - MANIFEST_PROJECT: - ${PROJECT_ROOT}/yocto/manifest - DEPLOY_TO: - ${PROJECT_ROOT}/3rd-party/kuk/uboot-imx-kuk - ${PROJECT_ROOT}/kernel/linux-guf - ${PROJECT_ROOT}/kernel/linux-imx-kuk - ${PROJECT_ROOT}/kernel/modules/egalaxi2c - ${PROJECT_ROOT}/kernel/modules/gfplatdetect - ${PROJECT_ROOT}/tools/gf-emc-test-suite - ${PROJECT_ROOT}/tools/gf-productiontests - ${PROJECT_ROOT}/tools/gfeeprom - ${PROJECT_ROOT}/tools/gfxml2dto - ${PROJECT_ROOT}/tools/guf-show-demo - ${PROJECT_ROOT}/tools/libmdb - ${PROJECT_ROOT}/tools/touchcal-conv - ${PROJECT_ROOT}/tools/xconfig - ${PROJECT_ROOT}/yocto/config - ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-bar - ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-foo - ${PROJECT_ROOT}/yocto/infrastructure/ci-test/minimal-manifest - ${PROJECT_ROOT}/yocto/layers/meta-guf-distro - ${PROJECT_ROOT}/yocto/layers/meta-guf-machine deploy-foobar: - extends: .deploy - variables: - PROJECT_ROOT: - ${CI_PROJECT_ROOT_NAMESPACE}/yocto/infrastructure/ci-test - MANIFEST_PROJECT: - ${PROJECT_ROOT}/minimal-manifest - - DEPLOY_TO: - ${PROJECT_ROOT}/minimal-foo - ${PROJECT_ROOT}/minimal-bar + extends: + - .deploy + - .foobar-manifest-projects trigger: stage: deploy + rules: + - if: $CI_MERGE_REQUEST_IID needs: [deploy] trigger: include: @@ -142,12 +153,36 @@ trigger: trigger-foobar: stage: deploy + rules: + - if: $CI_MERGE_REQUEST_IID needs: [deploy-foobar] trigger: include: - artifact: integration.yml job: deploy-foobar strategy: depend +# -------------------------------------------------------------------------------------- +# Stage: merge +# -------------------------------------------------------------------------------------- +.merge: + stage: merge + rules: + - if: $CI_COMMIT_BRANCH == "master" + script: + - cd ${CI_PROJECT_DIR} + - ./merge_gitlab_ci.py + --gitlab-url=${CI_SERVER_URL} + --token=${GITBOT_TOKEN} + --manifest-project=${MANIFEST_PROJECT} + --submodule=.gitlab-ci + --revision=${CI_COMMIT_SHA} + --verbose + ${DEPLOY_TO} + +merge-foobar: + extends: + - .merge + - .foobar-manifest-projects # -------------------------------------------------------------------------------------- # Stage: check diff --git a/accept_merge_request.py b/accept_merge_request.py index b9df28d5..a193e7a1 100755 --- a/accept_merge_request.py +++ b/accept_merge_request.py @@ -2,6 +2,7 @@ import common import argparse +import logging import sys import time from gitlab import ( @@ -51,6 +52,7 @@ def accept_merge_request(project, mr, rebase=False): 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) diff --git a/common.py b/common.py index 8642798f..144c7f8d 100755 --- a/common.py +++ b/common.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 +import logging import requests import sys -import logging import time +from furl import furl from git import Actor, GitCommandError from git.repo.base import Repo from gitlab import GitlabAuthenticationError, GitlabGetError, GitlabMRRebaseError @@ -203,3 +204,48 @@ def get_merge_request(project: Project, merge_request): except GitlabGetError: return None return mr + + +def clone_project(project: Project, into, branch=None): + + gitlab = project.manager.gitlab + # If no branch is given, use project's default branch + if branch is None: + branch = project.default_branch + + # Construct clone url containing access token + clone_url = furl(project.http_url_to_repo) + clone_url.username = "gitlab-ci" + clone_url.password = gitlab.private_token + + # Checkout project + try: + repo = Repo.clone_from(clone_url.url, into, branch=branch, depth=1) + except GitCommandError as e: + raise Exception("could not clone repository\n" + str(e)) from e + except IndexError: + raise Exception("branch '%s' not found" % branch) from e + return repo + + +def get_repository_file_raw(project: Project, filename, ref=None): + # TODO tree objects are not supported + fileobj = get_repository_file_obj(project, filename, ref) + return project.repository_raw_blob( + fileobj["id"], retry_transient_errors=True + ).decode() + + +def get_repository_file_obj(project: Project, filename, ref=None): + # TODO tree objects are not supported + if ref is None: + ref = project.default_branch + logging.debug("Using default branch %s", ref) + repository_tree = project.repository_tree(ref=ref, retry_transient_errors=True) + logging.debug(repository_tree) + fileobj = [f for f in repository_tree if f["name"] == filename] + + if len(fileobj) == 0: + return None + fileobj = fileobj[0] + return fileobj diff --git a/merge_gitlab_ci.py b/merge_gitlab_ci.py new file mode 100755 index 00000000..bd380b7d --- /dev/null +++ b/merge_gitlab_ci.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +import common + +import argparse +import logging +import sys +from gitlab import Gitlab, GitlabGetError +from accept_merge_request import accept_merge_request +import update_submodule +from gitlab.v4.objects import Project + + +def find_integration_merge_request( + project: Project, submodule, revision, target_branch +): + + ( + submodule_path, + submodule_revision, + ) = update_submodule.get_submodule_project_path_and_revision( + project, submodule, target_branch + ) + logging.debug("Module: %s, Revision: %s", submodule_path, submodule_revision) + + # Get submodule project + gitlab = project.manager.gitlab + submodule_project = common.get_project(gitlab, submodule_path) + + # Get the integration branch name + integration_branch_suffix = ( + update_submodule.get_submodule_integration_branch_suffix( + submodule_project, revision + ) + ) + integration_branch_name = common.integration_branch_name( + submodule_project.name, integration_branch_suffix + ) + logging.debug(integration_branch_name) + + # Get the merge request for the branch TODO catch exception + try: + integration_branch = project.branches.get( + integration_branch_name, retry_transient_errors=True + ) + except GitlabGetError: + print("ERROR: Failed to find integration branch for {}".format(submodule)) + return None + + logging.debug(integration_branch) + commit = project.commits.get( + integration_branch.commit["id"], retry_transient_errors=True + ) + for mr in commit.merge_requests(retry_transient_errors=True): + if mr["target_branch"] == target_branch: + integration_mr = mr + break + if integration_mr is None: + print( + "ERROR: Failed to find integration merge request for %s", + integration_branch_name, + ) + return None + logging.debug(integration_mr["iid"]) + mr = project.mergerequests.get(integration_mr["iid"], retry_transient_errors=True) + + return mr + + +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", + "--manifest-project", + help="""name of the GitLab project""", + dest="project", + ) + parser.add_argument( + "--submodule", + help="""submodule to update""", + dest="submodule", + required=True, + ) + parser.add_argument( + "--revision", + help="""new revision for submodule""", + dest="revision", + required=True, + ) + parser.add_argument( + "--branch", + help="""project branch (if not default branch)""", + dest="branch", + required=False, + default=None, + ) + parser.add_argument( + "--merge", + help="""if set, perform merge after integration""", + dest="merge", + action="store_true", + required=False, + default=False, + ) + parser.add_argument( + "projects", + help="""List of projects the change should be deployed to additionally + to the manifest project given as named parameter.""", + nargs="*", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="""Increase verbosity.""", + ) + + args, _ = parser.parse_known_args() + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + + gitlab = Gitlab(args.gitlab_url, private_token=args.token) + + for p in args.projects + [args.project]: + project = common.get_project(gitlab, p) + branch = args.branch + + if branch is None: + branch = project.default_branch + + mr = find_integration_merge_request( + project, args.submodule, args.revision, branch + ) + + logging.debug("Merge %s!%d: %s", project.name, mr.iid, mr.title) + # Wait until GitLab has checked merge status + common.wait_until_merge_status_is_set(project, mr) + + # Attempt to merge + merged = accept_merge_request( + project, + mr, + # Only the file manifest commit may be rebased + # other rebase would require new integration commits + rebase=(p == args.project), + ) + + if not merged: + sys.exit( + "Integration MR could not be merged. You have two possibilities to fix " + "this:\n" + " 1. Checkout the MR and rebase it on the current master manually, or\n" + " 2. Delete the MR (Edit -> Delete in the MR UI)\n" + "In either case restart this job afterwards in order to get it merged." + ) + print("Successfully merged") + + exit() + + +if __name__ == "__main__": + main() diff --git a/update_submodule.py b/update_submodule.py index d749fa57..17167ef9 100755 --- a/update_submodule.py +++ b/update_submodule.py @@ -6,9 +6,63 @@ import logging import os import sys import tempfile +from configparser import ConfigParser from furl import furl from git import GitCommandError, Repo from gitlab import Gitlab +from gitlab.v4.objects import Project + + +def get_submodule_project_path_and_revision(project: Project, submodule, branch=None): + + gitmodules = common.get_repository_file_raw(project, ".gitmodules", ref=branch) + if gitmodules is None: + logging.error("Submodule %s not found in %s.", submodule, project.name) + return None, None + + logging.debug("Gitmodules: %s", gitmodules) + + cfgparse = ConfigParser() + cfgparse.read_string(gitmodules) + try: + section = cfgparse['submodule "{}"'.format(submodule)] + except KeyError: + logging.error("Submodule %s not found in %s.", submodule, project.name) + return None, None + + submodule_url = section["url"] + # absolut path to a relative submodule + # Check for relative path + if not submodule_url.startswith(".."): + logging.error("absolute submodule paths are not supported (%s)", submodule_url) + return None, None + + # Get absolute project path + # This cannot be done with gitpython directly due to issue: + # https://github.com/gitpython-developers/GitPython/issues/730 + relative_path = os.path.splitext(submodule_url)[0] # remove .git + project_path = project.path_with_namespace + while relative_path.startswith(".."): + relative_path = relative_path[3:] # strip off '../' + project_path, _ = os.path.split(project_path) # remove last part + submodule_project_path = os.path.join(project_path, relative_path) + + # Get current revision + gitmodule_rev = common.get_repository_file_obj(project, submodule, ref=branch) + + return submodule_project_path, gitmodule_rev["id"] + + +def get_submodule_integration_branch_suffix(submodule_project: Project, revision): + # Find out if top commit is part of a merge request + # If so, use source branch of this MR as integration branch name + # Else use commit sha instead + integration_branch_suffix = revision + for mr in submodule_project.commits.get(revision).merge_requests(): + if mr["target_branch"] == submodule_project.default_branch: + integration_branch_suffix = mr["source_branch"] + break + return integration_branch_suffix def update_submodule( -- GitLab