Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • seco-ne/yocto/infrastructure/gitlab-ci
1 result
Show changes
Commits on Source (5)
......@@ -19,8 +19,8 @@ default:
stages:
- analyze
- integrate
- check
- merge
- build
workflow:
rules:
......@@ -62,7 +62,6 @@ yamllint:
# ---------------------------------------------------------------------------------------
# Stage: integrate
# ---------------------------------------------------------------------------------------
.ci-test-projects:
variables:
PROJECT_ROOT:
......@@ -73,6 +72,7 @@ yamllint:
INTEGRATE_INTO:
${PROJECT_ROOT}/minimal-foo
${PROJECT_ROOT}/minimal-bar
${PROJECT_ROOT}/minimal-srcrev
.yocto-projects:
variables:
......@@ -104,6 +104,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 +115,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 +128,43 @@ 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]
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
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:
......@@ -192,38 +177,26 @@ merge-yocto:
- .yocto-projects
# --------------------------------------------------------------------------------------
# Stage: check
# Stage: build
# --------------------------------------------------------------------------------------
check:
stage: check
needs: [integrate-yocto, integrate-ci-test]
build-master-yocto:
stage: build
needs: [merge-yocto]
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}";
- if: $CI_COMMIT_BRANCH == "master"
when: manual
trigger:
project: SECO-Northern-Europe/yocto/manifest
branch: "dunfell"
strategy: depend
- 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}
build-master-ci-test:
stage: build
needs: [merge-ci-test]
rules:
- if: $CI_COMMIT_BRANCH == "master"
when: manual
trigger:
project: SECO-Northern-Europe/yocto/infrastructure/ci-test/minimal-manifest
branch: "master"
strategy: depend
......@@ -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
......
......@@ -51,6 +51,10 @@ yamllint:
extends: .infrastructure
stage: check
rules:
# Do not integration pipeline for merge requests for integrate/gitlab-ci/ branches
# The integration is done from the pipeline in gitlab-ci already
- if: $CI_COMMIT_REF_NAME =~ /^integrate\/gitlab-ci\/.*/
when: never
- if: $CI_MERGE_REQUEST_IID
# Explicitly allow externally triggered pipelines in every case
- if: $CI_PIPELINE_SOURCE == "pipeline" || $CI_PIPELINE_SOURCE == "api"
......
......@@ -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,206 @@ 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
)
logging.debug("Read manifest from: %s", manifest_file_abs)
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
)
logging.debug("Read manifest from: %s", srcrev_file_abs)
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_file = os.path.join(
integration["repo"].working_tree_dir, ".gitlab-ci.yml"
)
logging.debug("Read recipe name from %s", gitlab_ci_yml_file)
with open(gitlab_ci_yml_file, "r", encoding="utf8") as fp:
gitlab_ci_yml = fp.read()
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")
......
......@@ -38,10 +38,6 @@ workflow:
rules:
# Explicitly allow externally triggered pipelines in every case
- if: $CI_PIPELINE_SOURCE == "pipeline" || $CI_PIPELINE_SOURCE == "api"
# Do not run pipelines for merge requests for integrate/gitlab-ci/ branches
# These are trigger explicitly from the integration pipeline in gitlab-ci repo
- if: $CI_COMMIT_REF_NAME =~ /^integrate\/gitlab-ci\/.*/
when: never
# Do not run pipelines on forked projects.
# The pipelines would not work anyway because of the users permissions.
# There are two cases catched here:
......@@ -63,6 +59,10 @@ workflow:
integrate:
extends: .infrastructure
rules:
# Do not integration pipeline for merge requests for integrate/gitlab-ci/ branches
# The integration is done from the pipeline in gitlab-ci already
- if: $CI_COMMIT_REF_NAME =~ /^integrate\/gitlab-ci\/.*/
when: never
# We have to make sure that the pipeline runs for the current manifest
# master at the time a merge request is created. Otherwise we cannot
# guarantee a green master after merging.
......
......@@ -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()
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(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()
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(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()
......
......@@ -25,10 +25,6 @@ workflow:
rules:
# Explicitly allow externally triggered pipelines in every case
- if: $CI_PIPELINE_SOURCE == "pipeline" || $CI_PIPELINE_SOURCE == "api"
# Do not run pipelines for merge requests for integrate/gitlab-ci/ branches
# These are trigger explicitly from the integration pipeline in gitlab-ci repo
- if: $CI_COMMIT_REF_NAME =~ /^integrate\/gitlab-ci\/.*/
when: never
# Do not run pipelines on forked projects.
# The pipelines would not work anyway because of the users permissions.
# There are two cases catched here:
......@@ -50,6 +46,10 @@ workflow:
integrate:
extends: .infrastructure
rules:
# Do not integration pipeline for merge requests for integrate/gitlab-ci/ branches
# The integration is done from the pipeline in gitlab-ci already
- if: $CI_COMMIT_REF_NAME =~ /^integrate\/gitlab-ci\/.*/
when: never
# We have to make sure that the pipeline runs for the current manifest
# master at the time a merge request is created. Otherwise we cannot
# guarantee a green master after merging.
......
#!/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__":
......