Skip to content
Snippets Groups Projects
  • Felix Gerking's avatar
    CI: Modifications to enable reproducible builds · 610e4a46
    Felix Gerking authored
    Added default source revision file name to common.py.
    Modified the integration_into_manifest function to set the
    corresponding subproject hash in the SRCREV.conf file if the
    repository is not found in the default.xml file. The
    merge_into_manifest function now works even if the names of
    the master branches in the manifest and in the project
    repository are different.
    
    BCS 746-000016
    610e4a46
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
integrate_into_manifest.py 7.67 KiB
#!/usr/bin/env python3
import common

import argparse
import sys
import tempfile
import re
from pathlib import Path
from furl import furl
from git import GitCommandError, Repo
from gitlab import Gitlab, GitlabGetError
from lxml import etree


def integrate_into_manifest(
    manifest_project,
    integration_base,
    manifest_file,
    srcrev_file,
    recipe_name,
    project,
    merge_request,
):
    gitlab = manifest_project.manager.gitlab

    with tempfile.TemporaryDirectory() as manifest_dir:
        manifest_filepath = Path(manifest_dir) / manifest_file
        srcrev_filepath = Path(manifest_dir) / srcrev_file

        # Construct clone url containing access token
        clone_url = furl(manifest_project.http_url_to_repo)
        clone_url.username = "gitlab-ci"
        clone_url.password = gitlab.private_token

        # Checkout manifest
        try:
            manifest_repo = Repo.clone_from(
                clone_url.url, manifest_dir, branch=integration_base
            )
        except GitCommandError as e:
            sys.exit("ERROR: could not clone manifest repository\n" + str(e))
        except IndexError:
            sys.exit("ERROR: branch '%s' not found" % integration_base)

        # Create integration branch (delete former one if already exists)
        integration_branch = common.integration_branch_name(
            project.name, merge_request.source_branch
        )
        for ref in manifest_repo.references:
            if integration_branch == ref.name:
                manifest_repo.delete_head(ref)
        manifest_repo.head.set_reference(manifest_repo.create_head(integration_branch))

        # Parse manifest file
        try:
            manifest = etree.parse(manifest_filepath.as_posix())
        except FileNotFoundError:
            sys.exit("ERROR: file '%s' not found in manifest repo" % manifest_file)

        # Find project reference in manifest
        # We are using str.endswith() for this in order to support sub-projects as well
        # (e.g."mygroup/myproject", when only "myproject" is given)
        project_node = None
        project_nodes = manifest.findall("project")
        for node in project_nodes:
            name = node.get("name")
            if name is not None and name.endswith(project.path):
                project_node = node
        if project_node is None:
            if recipe_name is None:
                sys.exit(
                    "ERROR: project '%s' not found in manifest and "
                    "no recipe name is specified" % project.path
                )
            # Check if project is referenced in SRCREV.conf
            project_line = None
            content = srcrev_filepath.read_text()
            # Match "...RECIPE_NAME ="
            pattern = re.compile("{}[ ,\t]{{0,}}?=".format(recipe_name))
            for line in content.splitlines():
                if pattern.search(line):
                    project_line = line
            if project_line is None:
                sys.exit(
                    "ERROR: project '%s' not found in manifest and "
                    "SRCREV file" % project.path
                )

            # Get current project revision from SRCREV file
            # Assuming notation: <project> = "<hash>"
            old_revision = project_line.split('"')[1]

            # Get new project revision from merge request
            new_revision = merge_request.sha

            # Update SRCREV file
            content = content.replace(old_revision, new_revision)
            srcrev_filepath.write_text(content)
            manifest_repo.index.add([srcrev_file])
        else:
            # Get current project revision from manifest
            old_revision = project_node.get("revision")

            # Get new project revision from merge request
            new_revision = merge_request.sha

            # Update manifest file
            # We are doing this using a plain text replace action. Unfortunately
            # all python libraries for handling XML data are not able to preserve
            # the file layout, and we want a minimal diff.
            content = manifest_filepath.read_text()
            content = content.replace(old_revision, new_revision)
            manifest_filepath.write_text(content)
            manifest_repo.index.add([manifest_file])

        # Make an API request to create the gitlab.user object
        gitlab.auth()

        # Construct commit message and commit the change
        message = "Integrate %s/%s\n%s" % (
            project.path,
            merge_request.source_branch,
            common.list_commits(merge_request.commits()),
        )
        manifest_revision = common.commit_and_push(
            manifest_project,
            manifest_repo,
            integration_branch,
            message,
            gitlab.user.username,
            gitlab.user.email,
        )

        return manifest_revision


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(
        "--manifest-project",
        help="""name of the manifest project""",
        dest="manifest_project",
        required=True,
    )
    parser.add_argument(
        "--integration-base",
        help="""manifest branch to branch off from""",
        dest="integration_base",
        required=True,
    )
    parser.add_argument(
        "--manifest-file",
        help="""manifest file name (default: 'default.xml')""",
        dest="manifest_file",
        default=common.manifest_file,
        required=False,
    )
    parser.add_argument(
        "--srcrev-file",
        help="""source revision file name (default: 'SRCREV.conf')""",
        dest="srcrev_file",
        default=common.srcrev_file,
        required=False,
    )
    parser.add_argument(
        "--recipe-name",
        help="""recipe name to resolve project in 'SRCREV.conf'""",
        dest="recipe_name",
        default=None,
        required=False,
    )
    parser.add_argument(
        "--project",
        help="""name of the project, as specified in the manifest""",
        dest="project",
        required=True,
    )
    parser.add_argument(
        "--merge-request",
        help="""project merge request IID containing the changes to be integrated""",
        dest="merge_request",
        required=True,
    )
    parser.add_argument(
        "--save-revision-to",
        help="""path to a file where the new manifest revision is stored""",
        dest="revision_file",
        required=False,
    )

    args, _ = parser.parse_known_args()

    gitlab = Gitlab(args.gitlab_url, private_token=args.token)

    manifest_project = common.get_project(gitlab, args.manifest_project)
    project = common.get_project(gitlab, args.project)
    try:
        merge_request = project.mergerequests.get(
            args.merge_request, retry_transient_errors=True
        )
    except GitlabGetError as e:
        sys.exit(
            "ERROR: could not get %s!%s: %s"
            % (project.name, args.merge_request, e.error_message)
        )

    manifest_revision = integrate_into_manifest(
        manifest_project=manifest_project,
        integration_base=args.integration_base,
        manifest_file=args.manifest_file,
        srcrev_file=args.srcrev_file,
        recipe_name=args.recipe_name,
        project=project,
        merge_request=merge_request,
    )

    if args.revision_file:
        with open(args.revision_file, "w", encoding="utf-8") as file:
            file.write(manifest_revision)


if __name__ == "__main__":
    main()