Skip to content
Snippets Groups Projects
Commit 9252e6ad authored by Jonas Höppner's avatar Jonas Höppner
Browse files

CI: deploy_gitlab_ci: Rework the deployment of gitlab-ci into the child projects

The deploy_gitlab_ci now creates the integration commit and branch in
each passed subproject and create an integration commit in the manifest
containing all these new revisions.
A build is then triggered on this commit to test the functionality.

Split the update_submodule functions to reuse them in different ways.
Remove some previously used files.

BCS 746-000740
parent 0a7d845e
No related branches found
No related tags found
No related merge requests found
Pipeline #9190 passed with stages
in 1 minute and 7 seconds
......@@ -19,7 +19,7 @@ default:
stages:
- analyze
- integrate
- check
- build
- merge
workflow:
......@@ -62,7 +62,6 @@ yamllint:
# ---------------------------------------------------------------------------------------
# Stage: integrate
# ---------------------------------------------------------------------------------------
.ci-test-projects:
variables:
PROJECT_ROOT:
......@@ -104,6 +103,8 @@ yamllint:
- if: $CI_MERGE_REQUEST_IID
when: manual
allow_failure: true
variables:
MERGE: ""
script:
- cd ${CI_PROJECT_DIR}
- ./deploy_gitlab_ci.py
......@@ -113,22 +114,9 @@ yamllint:
--submodule=.gitlab-ci
--revision=${CI_COMMIT_SHA}
--verbose
${MERGE}
${INTEGRATE_INTO}
- ./generate_job_from_template.py
--template=gitlab-ci-integration.jinja2
--image=${CI_IMAGE_PYTHON}
--branch="integrate/${CI_PROJECT_NAME}/${CI_COMMIT_REF_NAME}"
--manifest-project=${MANIFEST_PROJECT}
--parent_merge_request="${CI_MERGE_REQUEST_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
--verbose
${INTEGRATE_INTO}
> integration.yml
- cat integration.yml
artifacts:
paths:
- integration.yml
integrate-yocto:
extends:
- .integrate
......@@ -139,47 +127,47 @@ integrate-ci-test:
- .integrate
- .ci-test-projects
trigger-yocto:
stage: integrate
# --------------------------------------------------------------------------------------
# Stage: build
# --------------------------------------------------------------------------------------
build-yocto:
stage: build
needs: [integrate-yocto]
rules:
- if: $CI_MERGE_REQUEST_IID
allow_failure: true
needs: [integrate-yocto]
- if: $CI_COMMIT_BRANCH == "master"
when: manual
trigger:
include:
- artifact: integration.yml
job: integrate-yocto
project: SECO-Northern-Europe/yocto/manifest
branch: "integrate/${CI_PROJECT_NAME}/${CI_COMMIT_REF_NAME}"
strategy: depend
trigger-ci-test:
stage: integrate
build-ci-test:
stage: build
needs: [integrate-ci-test]
rules:
- if: $CI_MERGE_REQUEST_IID
needs: [integrate-ci-test]
allow_failure: true
- if: $CI_COMMIT_BRANCH == "master"
when: manual
trigger:
include:
- artifact: integration.yml
job: integrate-ci-test
project: SECO-Northern-Europe/yocto/infrastructure/ci-test/minimal-manifest
branch: "integrate/${CI_PROJECT_NAME}/${CI_COMMIT_REF_NAME}"
strategy: depend
# --------------------------------------------------------------------------------------
# Stage: merge
# --------------------------------------------------------------------------------------
.merge:
extends: .integrate
stage: merge
rules:
- if: $CI_COMMIT_BRANCH == "master"
when: manual
allow_failure: true
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}
${INTEGRATE_INTO}
variables:
MERGE: --merge
merge-ci-test:
extends:
......@@ -190,40 +178,3 @@ merge-yocto:
extends:
- .merge
- .yocto-projects
# --------------------------------------------------------------------------------------
# Stage: check
# --------------------------------------------------------------------------------------
check:
stage: check
needs: [integrate-yocto, integrate-ci-test]
rules:
# Probably this job gets removed
- when: never
- if: $CI_MERGE_REQUEST_IID
tags:
- infrastructure
timeout: 2m
script:
- cd ${CI_PROJECT_DIR}
- MERGE_REQUEST="${CI_MERGE_REQUEST_IID}";
- MASTER_BRANCH=dunfell
- ./check_if_integration_branch_is_up_to_date.py
--gitlab-url=${CI_SERVER_URL}
--token=${GITBOT_TOKEN}
--manifest-project=${CI_PROJECT_ROOT_NAMESPACE}/yocto/manifest
--integration-base=${MASTER_BRANCH}
--project=${CI_PROJECT_PATH}
--merge-request=${CI_MERGE_REQUEST_IID}
# The check is done for both manifests in one job as the retrigger
# looks for jobs named check, though there can only be one
- ./check_if_integration_branch_is_up_to_date.py
--gitlab-url=${CI_SERVER_URL}
--token=${GITBOT_TOKEN}
--manifest-project
${CI_PROJECT_ROOT_NAMESPACE}/yocto/infrastructure/ci-test/minimal-manifest
--integration-base=master
--project=${CI_PROJECT_PATH}
--merge-request=${CI_MERGE_REQUEST_IID}
......@@ -47,7 +47,7 @@ def accept_merge_request(project, mr, rebase=False, should_remove_source_branch=
print("Merge error: %s" % mr.merge_error)
else:
print("Merge reported success, but MR state is '%s'" % mr.state)
return False
return False, mr.sha
except GitlabMRClosedError as e:
# See HTTP error codes for merge requests here:
......@@ -73,7 +73,7 @@ def accept_merge_request(project, mr, rebase=False, should_remove_source_branch=
if pipeline_pending:
print("")
print("Merge not possible, has to be rebased manually")
return False
return False, mr.sha
elif e.response_code == 406:
# Merge conflict, automatic rebase is possible
......@@ -82,7 +82,7 @@ def accept_merge_request(project, mr, rebase=False, should_remove_source_branch=
pipeline_pending = False
print("Merge not possible, but branch can be automatically rebased")
if not rebase:
return False
return False, mr.sha
print("Trying to rebase...")
mr = common.rebase_merge_request(project, mr)
if mr.merge_error:
......@@ -96,7 +96,7 @@ def accept_merge_request(project, mr, rebase=False, should_remove_source_branch=
print("ERROR: merge not possible: %s" % e)
sys.exit(critical_error)
return True
return True, mr.sha
def main():
......
......@@ -150,7 +150,9 @@ def list_commits(commits):
return commit_list
def commit_and_push(project: Project, repo: Repo, branch, message, name, email):
def commit_and_push(
project: Project, repo: Repo, branch, message, name, email, less_verbose=False
):
"""Commit and push to a repo branch"""
author = Actor(name, email)
repo.index.commit(message, author=author, committer=author)
......@@ -168,7 +170,8 @@ def commit_and_push(project: Project, repo: Repo, branch, message, name, email):
revision = repo.head.commit.hexsha
print("Pushed new commit:")
print(project.web_url + "/-/commit/" + revision)
print(repo.git.show("--summary", "--decorate"))
if not less_verbose:
print(repo.git.show("--summary", "--decorate"))
return revision
......
......@@ -6,45 +6,24 @@ import logging
import sys
import os
from gitlab import Gitlab
from accept_merge_request import accept_merge_request
from create_merge_request import create_merge_request
from get_merge_requests import get_merge_requests
from update_submodule import update_submodule
from update_gitlab_ci import update_gitlab_ci_include
from update_submodule import (
update_submodule_and_include_ref,
get_submodule_project_path_and_revision,
)
from integrate_into_manifest import update_manifest, update_srcrev
from ruamel.yaml import YAML
def create_gitlab_ci_yml(repo, gitlab_ci_yml):
"""This code snippet was used to initially populate
all repos with the minial .gitlab-ci.yml that
includes the files from this repo.
Currently it is not used and only the revision is
changed.
It has also never been test as this function.
"""
logging.debug("Origin: %s", repo.remotes.origin.url)
# Contains the base file to be used in the subprojects
if "ci-test" in repo.remotes.origin.url:
if "manifest" in repo.remotes.origin.url:
include_file = "foobar-manifest.yml"
else:
include_file = "foobar-manifest-integration.yml"
else:
if "manifest" in repo.remotes.origin.url:
include_file = "manifest.yml"
else:
include_file = "manifest-integration.yml"
def read_keys_from_gitlab_ci_yml(gitlab_ci_yml):
# -----------------------------------
# Adapt content in the include file
# -----------------------------------
# Read values from existing file
yaml = YAML()
with open(gitlab_ci_yml, "r", encoding="utf8") as fp:
data = yaml.load(fp)
data = yaml.load(gitlab_ci_yml)
logging.debug("Yaml: %s", data)
try:
......@@ -57,103 +36,61 @@ def create_gitlab_ci_yml(repo, gitlab_ci_yml):
logging.debug("Recipe %s", recipe)
except KeyError:
recipe = None
return {"recipe": recipe, "masterbranch": masterbranch}
# -----------------------------------
with open(gitlab_ci_yml, "w", encoding="utf8") as fp:
fp.write(
"""---
# ---------------------------------------------------------------------------------------
# Include the default CI steps from the gitlab-ci repo
# ---------------------------------------------------------------------------------------
include:
- project: '${CI_PROJECT_ROOT_NAMESPACE}/yocto/infrastructure/gitlab-ci'
ref: 49cc4204323bca76190e5ffae1a7d5627157c073
"""
)
fp.write(" file: '{}'\n".format(include_file))
if masterbranch is not None or recipe is not None:
fp.write("\nvariables:\n")
if masterbranch is not None:
fp.write(" MASTER_BRANCH_PROJECT: {}\n".format(masterbranch))
if recipe is not None:
fp.write(" BB_RECIPE_NAME: {}\n".format(recipe))
# -----------------------------------
repo.git.add(gitlab_ci_yml)
# ======================================
def update_rev_in_gitlab_ci(repo, submodule_project, submodule_revision):
# Add changed revision also to .gitlab-ci.yml
gitlab_ci_yml = os.path.join(repo.working_tree_dir, ".gitlab-ci.yml")
if update_gitlab_ci_include(
gitlab_ci_yml,
submodule_project.web_url.split("//")[1].split("/", 1)[1],
submodule_revision,
):
repo.git.add(gitlab_ci_yml)
def integrate_submodule_into(
gitlab, project_name, submodule_name, new_revision, branch, commit_and_push=True
):
with open(gitlab_ci_yml, "r", encoding="utf8") as fp:
logging.debug(fp.read())
gitlab_project = common.get_project(gitlab, project_name)
def deploy_into(project, submodule, revision, branch, replace_existing_branch=False):
"""Update the submodule and include refs to the submodule in the given project.
Create mergerequest if needed.
Parameters:
project ( gitlab project): The project which's submodule should be updated
submodule_name (string): The name of the submodule to pull
submodule_revision (hex string): The sha hash of the commit to update the submodule to
branch (string): branch to update, if None, the projects default branch is used
replace_existing_branch: When an existing integration branch is found it is always replaced.
Returns: tuple of:
branch (string): Name of the newly created integration branch
merge_request (gitlab mr): Mergerequest for the integration branch
"""
# Update submodule
integration_branch, _, submodule_project = update_submodule(
project,
submodule,
revision,
(
project_repo,
integration_branch_name,
integration_commit,
message,
) = update_submodule_and_include_ref(
gitlab_project,
submodule_name,
new_revision,
branch,
pre_commit_hook=update_rev_in_gitlab_ci,
replace_existing_branch=replace_existing_branch,
commit_and_push=commit_and_push,
)
logging.debug("Integration branch: %s", integration_branch)
# If submodule is already at specified revision, return directly
if not integration_branch:
return None, submodule_project
# Get source merge request
mrs = get_merge_requests(
submodule_project,
# TODO should this be submodule_project's default branch?
target_branch="master",
commit=revision,
if integration_branch_name is None:
return None
# ======================================
# Store the references for creating the integration
# commit in the manifest later
# ======================================
ret = {
"project": gitlab_project,
"repo": project_repo,
"branch": integration_branch_name,
"commit": integration_commit,
"message": message,
}
logging.debug(
"Integration branch: %s (%s)",
integration_branch_name,
integration_commit,
)
if not mrs:
sys.exit(
"ERROR: could not determine source merge request for commit %s" % revision
)
source_mr = mrs[0]
return ret
def create_integration_merge_request(project, integration_branch_name, source_mr=None):
# Create merge request
# This should be optional
mr, created = create_merge_request(
project, integration_branch, project.default_branch
project, integration_branch_name, project.default_branch
)
if created:
common.crosslink_merge_requests(source_mr, mr)
if source_mr is not None:
common.crosslink_merge_requests(source_mr, mr)
print("Created new merge request:\n%s" % mr.web_url)
else:
print("Existing integration merge request:\n%s" % mr.web_url)
return integration_branch, mr
return mr
def main():
......@@ -203,6 +140,20 @@ def main():
required=False,
default=False,
)
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(
"projects",
help="""List of projects the change should be deployed to additionally
......@@ -221,66 +172,217 @@ def main():
logging.basicConfig(level=logging.DEBUG)
gitlab = Gitlab(args.gitlab_url, private_token=args.token)
project = common.get_project(gitlab, args.project)
logging.debug("Integrate into: %s", args.project)
# =======================================================
# Create integration branches and commits with updates
# submodule in all projects
# =======================================================
project_integration = {}
# Update submodule in all 'child' project
for p in args.projects:
logging.debug("Integrate into: %s", p)
res = integrate_submodule_into(
gitlab, p, args.submodule, args.revision, args.branch
)
if res is not None:
project_integration[p] = res
# Update submodule in this project, create MR
integration_branch, mr = deploy_into(
project,
# Update submodule in manifest project
manifest_project = integrate_submodule_into(
gitlab,
args.project,
args.submodule,
args.revision,
args.branch,
replace_existing_branch=len(args.projects) > 0,
commit_and_push=False,
)
merge_request_manifest = mr
merge_requests = []
if manifest_project is not None:
project_integration[args.project] = manifest_project
# If submodule is already at specified revision, exit successfully
if not integration_branch:
# If submodule is already at specified revision in all projects, exit successfully
if len(project_integration) == 0:
print("No integration done, changes are already included in all projects.")
sys.exit(0)
# =======================================================
# Create and merge merge_requests if needed
# =======================================================
if args.merge:
# Get source merge request ( the one in the gitlab-ci repo)
submodule_project_path, _ = get_submodule_project_path_and_revision(
manifest_project["project"], args.submodule, args.branch
)
submodule_project = common.get_project(gitlab, submodule_project_path)
mrs = get_merge_requests(
submodule_project,
# TODO should this be submodule_project's default branch?
target_branch="master",
commit=args.revision,
)
if not mrs:
sys.exit(
"ERROR: could not determine source merge request for commit %s"
% args.revision
)
source_mr = mrs[0]
for p in args.projects:
integration = project_integration[p]
logging.debug("Create MR in %s", integration["project"].name)
mr = create_integration_merge_request(
integration["project"], integration["branch"], source_mr
)
integration["mr"] = mr
# Now merge
logging.debug("Merge %s!%s", p, mr.iid)
# Wait until GitLab has checked merge status
common.wait_until_merge_status_is_set(integration["project"], mr)
# Attempt to merge
merged, integration_commit = accept_merge_request(
integration["project"], mr, rebase=True
)
# if this has rebased the integration commit needs to be adapted:
project_integration[p]["commit"] = integration_commit
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")
# =======================================================
# Now create the integration commit in the manifest
# for all subprojects at once
# =======================================================
manifest_file_abs = os.path.join(
manifest_project["repo"].working_tree_dir, args.manifest_file
)
with open(
manifest_file_abs,
"r",
encoding="utf8",
) as fp:
manifest = fp.read()
logging.debug(manifest)
srcrev_file_abs = os.path.join(
manifest_project["repo"].working_tree_dir, args.srcrev_file
)
with open(
srcrev_file_abs,
"r",
encoding="utf8",
) as fp:
srcrev = fp.read()
logging.debug(srcrev)
for p in args.projects:
gitlab_p = common.get_project(gitlab, p)
logging.debug("Integrate into: %s", p)
integration = project_integration[p]
logging.debug(
"Update %s to %s", integration["project"].name, integration["commit"]
)
integration_branch, mr = deploy_into(
gitlab_p,
args.submodule,
args.revision,
args.branch,
new_manifest = update_manifest(
manifest, integration["project"], integration["commit"]
)
merge_requests.append(mr)
if new_manifest is not None:
manifest = new_manifest
logging.debug(manifest)
continue
# get BB_RECIPE_NAME from the projects .gitlab-ci.yml
gitlab_ci_yml = common.get_repository_file_raw(
integration["project"], ".gitlab-ci.yml", ref=integration["branch"]
)
project_keys = read_keys_from_gitlab_ci_yml(gitlab_ci_yml)
new_srcrev = update_srcrev(
srcrev, project_keys["recipe"], integration["commit"]
)
if new_srcrev is not None:
srcrev = new_srcrev
logging.debug(srcrev)
else:
logging.debug("Project %s not found in xml or srcrev file", p)
# Write manifest
with open(
manifest_file_abs,
"w",
encoding="utf8",
) as fp:
fp.write(manifest)
manifest_project["repo"].git.add(args.manifest_file)
logging.debug(manifest)
with open(
srcrev_file_abs,
"w",
encoding="utf8",
) as fp:
fp.write(srcrev)
manifest_project["repo"].git.add(args.srcrev_file)
logging.debug(srcrev)
# ========================================================
# Now commit and push the changes to the manifest repo
# ========================================================
# Make an API request to create the gitlab.user object
gitlab = manifest_project["project"].manager.gitlab
gitlab.auth()
integration_commit = common.commit_and_push(
manifest_project["project"],
manifest_project["repo"],
manifest_project["branch"],
manifest_project["message"],
gitlab.user.username,
gitlab.user.email,
)
logging.debug("Integration branch: %s", integration_branch)
print(
"Successfully create integration commit {} in {}".format(
integration_commit, args.project
)
)
if not args.merge:
print(
"Skipping automatic merge in MR context. If you like to extend the "
"integration MR by hand, please do it now. Afterwards you can either merge "
"it by hand or re-run this job on the master branch after the source MR "
"has been merged."
)
sys.exit(0)
# ============================================
# Create merge requests for the manifest
# ============================================
logging.debug("Create MR in %s", manifest_project["project"].name)
manifest_project["mr"] = create_integration_merge_request(
manifest_project["project"], manifest_project["branch"], source_mr
)
# =================================================
# Now merge it
# =================================================
# The manifest needs to be merged at last
merge_requests.append(merge_request_manifest)
for mr in merge_requests:
logging.debug("Merge %s", mr)
# Wait until GitLab has checked merge status
common.wait_until_merge_status_is_set(project, mr)
mr = manifest_project["mr"]
logging.debug("Merge %s!%s", args.project, mr.iid)
# Attempt to merge
merged = accept_merge_request(project, mr, rebase=True)
# Wait until GitLab has checked merge status
common.wait_until_merge_status_is_set(manifest_project["project"], mr)
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."
)
# Attempt to merge
merged = accept_merge_request(manifest_project["project"], mr, rebase=True)
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")
......
......@@ -10,9 +10,66 @@ from pathlib import Path
from furl import furl
from git import GitCommandError, Repo
from gitlab import Gitlab
from gitlab.v4.objects import Project
from lxml import etree
def update_manifest(manifest, project: Project, new_revision):
"""Returns updated version of the manifest or None id project_name
was not found in the manifest
"""
# Parse manifest
try:
manifestxml = etree.fromstring(manifest.encode())
except etree.XMLSyntaxError:
sys.exit("ERROR: Failed to parse given manifest")
# 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 = manifestxml.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:
return None
# Get current project revision from manifest
old_revision = project_node.get("revision")
logging.debug("Replace %s with %s", old_revision, new_revision)
# 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.
manifest = manifest.replace(old_revision, new_revision)
return manifest
def update_srcrev(srcrev, recipe_name, new_revision):
# Check if project is referenced in SRCREV.conf
# Match "...RECIPE_NAME ="
pattern = re.compile("{}[ ,\t]{{0,}}?=".format(recipe_name))
project_line = None
for line in srcrev.splitlines():
if pattern.search(line):
project_line = line
break
if project_line is None:
return None
# Get current project revision from SRCREV file
# Assuming notation: <project> = "<hash>"
old_revision = project_line.split('"')[1]
# Update SRCREV file
srcrev = srcrev.replace(old_revision, new_revision)
return srcrev
def integrate_into_manifest(
manifest_project,
integration_base,
......@@ -34,6 +91,7 @@ def integrate_into_manifest(
clone_url.password = gitlab.private_token
# Checkout manifest
# TODO replace checkout with gitlab api access
try:
manifest_repo = Repo.clone_from(
clone_url.url, manifest_dir, branch=integration_base
......@@ -71,68 +129,42 @@ def integrate_into_manifest(
manifest_repo.head.set_reference(
manifest_repo.create_head(integration_branch)
)
# Get new project revision from merge request
new_revision = merge_request.sha
# Parse manifest file
try:
manifest = etree.parse(manifest_filepath.as_posix())
with open(manifest_filepath.as_posix(), "r", encoding="utf8") as fp:
manifest = fp.read_text()
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:
new_manifest = update_manifest(manifest, project, new_revision)
if new_manifest is not None:
# write file
with open(manifest_filepath.as_posix(), "w", encoding="utf8") as fp:
fp.write_text(new_manifest)
manifest_repo.index.add([manifest_file])
else:
# Look for project in SRCREV as it has not been found in the manifest
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:
with open(srcrev_filepath, "r", encoding="utf8") as fp:
srcrev = fp.read_text()
new_srcrev = update_srcrev(srcrev, recipe_name, new_revision)
# write file
if new_srcrev is None:
sys.exit(
"ERROR: project '%s' not found in manifest and "
"SRCREV file" % project.path
"no recipe name is specified" % 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)
with open(srcrev_filepath.as_posix(), "w", encoding="utf8") as fp:
fp.write_text(new_manifest)
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()
......
#!/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)
# Start with the manifest here, so the subproject
# can see that they are already integrated
for p in args.projects + [args.project]:
project = common.get_project(gitlab, p)
branch = args.branch
if branch is None:
branch = project.default_branch
print(
"Try to merge {}({}) into {} ({})".format(
args.submodule, args.revision, project.name, branch
)
)
mr = find_integration_merge_request(
project, args.submodule, args.revision, branch
)
if mr is None:
sys.exit("ERROR: Failed to find the integration MR.")
# TODO if this ever happens, why ever, we could call
# deploy_gitlab_ci again to create a new integration
# commit or make sure the change is already integrated.
print("Merge {}!{}: {}".format(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
# TODO implement rebase
rebase=(p == args.project),
)
if not merged:
print(
"Integration MR could not be merged."
"To fix this:\n"
" 1. Checkout the MR {}!{} and merge it manually.\n"
" 2. Manually merge the follow up MRs in the following project:".format(
project.name, mr.iid
)
)
found = False
for p2 in [args.project] + args.projects:
if p2 == p:
found = True
continue
if not found:
continue
print(" {}".format(p2))
sys.exit()
print("Successfully merged")
exit()
if __name__ == "__main__":
main()
......@@ -78,7 +78,7 @@ def merge_into_manifest(
common.wait_until_merge_status_is_set(manifest_project, mr)
# Attempt to merge
merged = accept_merge_request(manifest_project, mr)
merged, _ = accept_merge_request(manifest_project, mr)
if not merged:
# Merge failed, reintegrate the source merge request into the manifest.
......
#!/usr/bin/env python3
import argparse
import logging
import re
def update_gitlab_ci_include(filename, include_project, new_revision):
"""Update the include statement in a gitlab-ci yml to a given revision
Parameters:
filename( string): The path to the file to change.
include_project( string): The path used to reference the project the include points to.
new_revision (string): The hex sha to set the include to.
Returns: True if the file was changed.
"""
# Set the possible include in the local .gitlab.yml file
# to the new revision. The include needs to have the revision
# specified directly in the file, as it is parsed before the
# submodule checkout is done
# Use custom read write method as I didn't got ruamel yaml
# to keep all costum formating (linebreaks ...)
# This assumes following format of the include block
# include:
# - project: 'SECO-Northern-Europe/yocto/infrastructure/gitlab-ci'
# ref: c5a3793e783fcb364c7f3bda73e8cd7c08a08804
# file: 'manifest-childs.yml'
# Verify hash format:
if re.match(r"\A[0-9a-fA-F]{40}\Z", new_revision) is None:
raise TypeError("Format of specified revision is not correct")
parsestate = 0
changed = False
# Remove the SECO-Northern-Europe part from the priject filter
# as it is normally specified by $CI_PROJECT_ROOT_NAMESPACE
include_project = include_project.split("/", 1)[1]
logging.debug("Include project: %s", include_project)
logging.debug("New revision: %s", new_revision)
with open(filename, "r+", encoding="UTF-8") as fp:
while True:
linestart = fp.tell()
line = fp.readline()
parts = line.partition(":")
logging.debug("Splitted input line: %s", parts)
if len(line) == 0:
break # End of file
if parsestate == 0:
if parts[0] == "include":
# Found include block
parsestate = 1
logging.debug("Found 'include' block at %d", linestart)
elif parsestate == 1:
if parts[0] == "\n":
break # End of include block
if (
parts[0].endswith(" - project")
and parts[2].find(include_project) >= 0
):
# Found the correct project
parsestate = 2
logging.debug("Found 'project' entry at %d", linestart)
elif parsestate == 2:
if parts[0].endswith(" ref"):
# Found the ref: entry, compare the revision
logging.debug("Found 'ref' entry at %d", linestart)
parsestate = 1
if parts[2].find(new_revision) >= 0:
print(
"Revision in {} is already set to {}".format(
filename, new_revision
)
)
else:
print(
"Changed revision in {} to {}".format(
filename, new_revision
)
)
fp.seek(linestart)
fp.write("{}: {}".format(parts[0], new_revision))
fp.flush()
changed = True
elif parts[0].find("- ") >= 0 or not parts[0].startswith(" "):
# Format of the line is not 'name: value' assume end of block
# Block was not found
break
return changed
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--filename",
help="""File to change""",
required=True,
)
parser.add_argument(
"--include-project",
help="""The path to the included project as used in 'filename'""",
required=True,
)
parser.add_argument(
"--revision",
help="""new revision for submodule""",
required=True,
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="""Increase verbosity.""",
)
args, _ = parser.parse_known_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
update_gitlab_ci_include(args.filename, args.include_project, args.revision)
if __name__ == "__main__":
main()
......@@ -11,6 +11,7 @@ from furl import furl
from git import GitCommandError, Repo
from gitlab import Gitlab
from gitlab.v4.objects import Project
from ruamel.yaml import YAML
def get_submodule_project_path_and_revision(project: Project, submodule, branch=None):
......@@ -65,165 +66,235 @@ def get_submodule_integration_branch_suffix(submodule_project: Project, revision
return integration_branch_suffix
def update_submodule(
def clone_project_and_submodule(project: Project, submodule_name, branch=None):
"""Creates a clone of the given project including the submodule
return:
"""
gitlab = project.manager.gitlab
# If no branch is given, use project's default branch
if branch is None:
branch = project.default_branch
project_dir = tempfile.TemporaryDirectory()
# 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, project_dir, branch=branch, depth=1)
except GitCommandError as e:
sys.exit("ERROR: could not clone repository\n" + str(e))
except IndexError:
sys.exit("ERROR: branch '%s' not found" % branch)
# Find submodule
submodule = common.get_submodule(repo, submodule_name)
# Check for relative path
if not submodule.url.startswith(".."):
sys.exit(
"ERROR: absolute submodule paths are not supported (%s)" % submodule.url
)
# 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 submodule project
submodule_project = common.get_project(gitlab, submodule_project_path)
# Initialize submodule
# Hack due to issue above: change to absolute path and switch back afterwards
submodule_clone_url = furl(submodule_project.http_url_to_repo)
submodule_clone_url.username = "gitlab-ci"
submodule_clone_url.password = gitlab.private_token
submodule_relative_url = submodule.url
with submodule.config_writer() as writer:
writer.set("url", submodule_clone_url.url)
submodule.update(init=True)
with submodule.config_writer() as writer:
writer.set("url", submodule_relative_url)
return repo, submodule_project
def update_submodule_in_repo(repo: Repo, submodule_project: Project, new_revision):
"""Updates the given submodule to the given revision and adds it to the
staging of repo
"""
# Update submodule
try:
submodule_project.module().git.checkout(new_revision)
except GitCommandError as e:
sys.exit("ERROR: could not checkout commit\n" + str(e))
repo.git.add(submodule_project.path)
def update_gitlab_ci_include(content, include_project, new_revision):
# Remove the SECO-Northern-Europe part from the project filter
# as it is normally specified by $CI_PROJECT_ROOT_NAMESPACE
include_project = include_project.split("/", 1)[1]
yaml = YAML()
data = yaml.load(content)
logging.debug("Yaml: %s", data)
try:
includes = data["include"]
except KeyError:
logging.debug("No include statement found")
return None
current_revision = None
for entry in includes:
try:
if include_project in entry["project"]:
current_revision = entry["ref"]
break
except KeyError:
logging.debug("Failed to parse include statement")
return None
if current_revision is None:
logging.debug("Failed to find %s in include statement", include_project)
return None
# Use plain replacement to keep the content of the file
# Yes, this may fail if the 'current_revision' is used multiple
# time is this fail. But probably this will not ever happen
logging.debug("Replace %s with %s", current_revision, new_revision)
return content.replace(current_revision, new_revision)
def update_submodule_and_include_ref(
project,
submodule_name,
submodule_revision,
new_revision,
branch=None,
pre_commit_hook=None,
replace_existing_branch=False,
commit_and_push=True,
):
"""Update submodule of gitlab project to given revision
"""Update the submodule and include refs to the submodule in the given project.
Create mergerequest if needed.
Parameters:
project (gitlab project): The project which's submodule should be updated
project ( gitlab project): The project which's submodule should be updated
submodule_name (string): The name of the submodule to pull
submodule_revision (hex string): The sha hash of the commit to update the submodule to
new_revision (hex string): The sha hash of the commit to update the submodule to
branch (string): branch to update, if None, the projects default branch is used
pre_commit_hook: Function to be called before the actual commit is done, to add additional changes.
Arguments passed: ( repo, submodule_project, submodule_revision)
replace_existing_branch: When an existing integration branch is found it is always replaced.
Returns: tuple of:
branch (string): Name of the newly created integration branch
revision (string): hexsha of the new commit
submodule_project ( gitlab project): The submodule as gitlab priject instance
commit_and_push: Set to false if no commit should be created. Changes are left in staging.
Returns: tuple of:
project_repo (Repo): GitPython repo with the cloned project
integration_branch (string): Name of the newly created integration branch
integration_commit (hexsha): Hash of the newly created commit
message: Commit message based on the integrated changes.
"""
gitlab = project.manager.gitlab
# If no branch is given, use project's default branch
if branch is None:
branch = project.default_branch
logging.debug("Branch: %s", branch)
with tempfile.TemporaryDirectory() as project_dir:
_, submodule_current_rev = get_submodule_project_path_and_revision(
project, submodule_name, 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
# Check if revisions are different
if submodule_current_rev == new_revision:
print("Submodule is already at %s" % new_revision)
return (None, None, None, None)
# Checkout project
try:
repo = Repo.clone_from(clone_url.url, project_dir, branch=branch, depth=1)
except GitCommandError as e:
sys.exit("ERROR: could not clone repository\n" + str(e))
except IndexError:
sys.exit("ERROR: branch '%s' not found" % branch)
# Find submodule
submodule = common.get_submodule(repo, submodule_name)
# Check if revisions are different
if submodule.hexsha == submodule_revision:
print("Submodule is already at %s" % submodule_revision)
if not replace_existing_branch:
# TODO test this
return (None, None, None)
# Check for relative path
if not submodule.url.startswith(".."):
sys.exit(
"ERROR: absolute submodule paths are not supported (%s)" % submodule.url
)
# 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 submodule project
submodule_project = common.get_project(gitlab, submodule_project_path)
# Get commits between current and new revision
revision_range = submodule.hexsha + ".." + submodule_revision
commits = submodule_project.commits.list(ref_name=revision_range)
if not commits:
sys.exit("ERROR: no commits found in range %s" % revision_range)
# 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 = submodule_revision
for mr in commits[0].merge_requests():
if mr["target_branch"] == submodule_project.default_branch:
integration_branch_suffix = mr["source_branch"]
break
project_repo, submodule_project = clone_project_and_submodule(
project, submodule_name, branch
)
# Initialize submodule
# Hack due to issue above: change to absolute path and switch back afterwards
submodule_clone_url = furl(submodule_project.http_url_to_repo)
submodule_clone_url.username = "gitlab-ci"
submodule_clone_url.password = gitlab.private_token
submodule_relative_url = submodule.url
with submodule.config_writer() as writer:
writer.set("url", submodule_clone_url.url)
submodule.update(init=True)
with submodule.config_writer() as writer:
writer.set("url", submodule_relative_url)
# Check if integration branch already exists and if it is up to date
integration_branch = common.integration_branch_name(
submodule_project.name, integration_branch_suffix
)
existing_branch = None
for ref in repo.references:
if "origin/" + integration_branch == ref.name:
existing_branch = ref
if existing_branch:
repo.head.set_reference(existing_branch)
submodule = common.get_submodule(repo, submodule_name)
if replace_existing_branch or submodule.hexsha != submodule_revision:
print("Replacing outdated integration branch %s" % integration_branch)
repo.head.set_reference(branch)
submodule = common.get_submodule(repo, submodule_name)
else:
print(
"Submodule is already at %s on branch %s"
% (submodule_revision, integration_branch)
)
return (integration_branch, existing_branch.commit, submodule_project)
else:
print("Creating integration branch %s" % integration_branch)
# Create integration branch
repo.head.set_reference(repo.create_head(integration_branch))
# Update submodule
try:
submodule.module().git.checkout(submodule_revision)
except GitCommandError as e:
sys.exit("ERROR: could not checkout commit\n" + str(e))
repo.git.add(submodule.path)
# Get commits between current and new revision
revision_range = submodule_current_rev + ".." + new_revision
commits = submodule_project.commits.list(
ref_name=revision_range, retry_transient_errors=True
)
if not commits:
sys.exit("ERROR: no commits found in range %s" % revision_range)
logging.debug("New commits: %s", commits)
if pre_commit_hook is not None:
pre_commit_hook(repo, submodule_project, submodule_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 = new_revision
for mr in commits[0].merge_requests():
if mr["target_branch"] == submodule_project.default_branch:
integration_branch_suffix = mr["source_branch"]
break
logging.debug("Integration branch suffix: %s", integration_branch_suffix)
# Setup integration branch
integration_branch_name = common.integration_branch_name(
submodule_project.name, integration_branch_suffix
)
# Create integration branch
print("Creating/replacing integration branch %s" % integration_branch_name)
project_repo.head.set_reference(project_repo.create_head(integration_branch_name))
# Update submodule to new revision
submodule_repo = common.get_submodule(project_repo, submodule_name)
update_submodule_in_repo(project_repo, submodule_repo, new_revision)
# Update the gitlab-ci.yml file to the new revision
# Now also update the project '.gitlab-ci.yml' file
gitlab_ci_yml_filename = os.path.join(
project_repo.working_tree_dir, ".gitlab-ci.yml"
)
with open(gitlab_ci_yml_filename, "r", encoding="utf8") as fp:
gitlab_ci_yml = fp.read()
logging.debug(gitlab_ci_yml)
new_gitlab_ci_yml = update_gitlab_ci_include(
gitlab_ci_yml,
submodule_project.web_url.split("//")[1].split("/", 1)[1],
new_revision,
)
if new_gitlab_ci_yml is None:
print("Failed to update the include revision in '.gitlab-ci.yml'")
else:
logging.debug(new_gitlab_ci_yml)
with open(gitlab_ci_yml_filename, "w", encoding="utf8") as fp:
fp.write(new_gitlab_ci_yml)
project_repo.git.add(os.path.basename(gitlab_ci_yml_filename))
# Construct commit message and commit the change
message = "Integrate %s/%s%s\n%s" % (
submodule_project.name,
integration_branch_suffix,
" and %d more" % (len(commits) - 1) if len(commits) > 1 else "",
common.list_commits(commits),
)
# Commit the changes
if commit_and_push:
# Make an API request to create the gitlab.user object
gitlab = project.manager.gitlab
gitlab.auth()
# Construct commit message and commit the change
message = "Integrate %s/%s%s\n%s" % (
submodule_project.name,
integration_branch_suffix,
" and %d more" % (len(commits) - 1) if len(commits) > 1 else "",
common.list_commits(commits),
)
project_revision = common.commit_and_push(
integration_commit = common.commit_and_push(
project,
repo,
integration_branch,
project_repo,
integration_branch_name,
message,
gitlab.user.username,
gitlab.user.email,
less_verbose=True,
)
else:
integration_commit = None
return (integration_branch, project_revision, submodule_project)
return project_repo, integration_branch_name, integration_commit, message
def main():
......@@ -279,7 +350,9 @@ def main():
gitlab = Gitlab(args.gitlab_url, private_token=args.token)
project = common.get_project(gitlab, args.project)
update_submodule(project, args.submodule, args.revision, args.branch)
update_submodule_and_include_ref(
project, args.submodule, args.revision, args.branch
)
if __name__ == "__main__":
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment