diff --git a/CI/CMakeLists.txt b/CI/CMakeLists.txt deleted file mode 100644 index e7116b2f2f5bb620cceedc2a5300945c3209b03d..0000000000000000000000000000000000000000 --- a/CI/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ - -# Declare the package's name to the build. This is necessary for it -# to show up nicely in the build results. -atlas_subdir( CI ) - -# Declare tests for the "package": -add_test (NAME DomainMapTests COMMAND python -m test.test_domain_map WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) -add_test (NAME WatchListTests COMMAND python -m test.test_watch_list WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) -set_property (TEST DomainMapTests WatchListTests APPEND PROPERTY LABELS CI) - -# install executables -atlas_install_scripts( sweep_MR.py ) diff --git a/CI/domain_map.py b/CI/domain_map.py deleted file mode 100644 index fd5cc81edafd5dbc954833f31e04df39c397cdd8..0000000000000000000000000000000000000000 --- a/CI/domain_map.py +++ /dev/null @@ -1,39 +0,0 @@ -# dictionary for mapping package names to software domains -# keys ... must be strings for the name of the software domain -# values ... must be sets containing valid regular expression strings -DOMAIN_MAP = {} - -DOMAIN_MAP['Analysis'] = set(['^PhysicsAnalysis/']) -DOMAIN_MAP['BTagging'] = set(['JetTagging', 'ParticleJetTools', 'FlavourTag']) -DOMAIN_MAP['Build'] = set(['^Build$','^Projects/']) -DOMAIN_MAP['Calorimeter'] = set(['^Calorimeter/']) -DOMAIN_MAP['CI'] = set(['^CI']) -DOMAIN_MAP['Core'] = set(['^Control/']) -DOMAIN_MAP['Database'] = set(['^Database/']) -DOMAIN_MAP['Digitization'] = set(['Digitization','TileSimAlgs','PileUpComps','PileUpTools','RunDependentSim']) -DOMAIN_MAP['DQ'] = set(['^DataQuality/','^LumiBlock/']) -DOMAIN_MAP['EDM'] = set(['^Event/']) -DOMAIN_MAP['Egamma'] = set(['^egamma']) -DOMAIN_MAP['EventDisplay'] = set(['^graphics/']) -DOMAIN_MAP['Externals'] = set(['^External/']) -DOMAIN_MAP['ForwardDetectors'] = set(['^ForwardDetectors/']) -DOMAIN_MAP['Generators'] = set(['^Generators/']) -DOMAIN_MAP['Geometry'] = set(['^AtlasGeometryCommon/','^DetectorDescription/']) -DOMAIN_MAP['InnerDetector'] = set(['^InnerDetector/']) -DOMAIN_MAP['JetEtmiss'] = set(['Jet','(?<!spectro)(?<!geo)MET','MissingEt','EventShape']) -DOMAIN_MAP['LAr'] = set(['^LArCalorimeter/']) -DOMAIN_MAP['Magnets'] = set(['^MagneticField/']) -DOMAIN_MAP['MuonSpectrometer'] = set(['^MuonSpectrometer/']) -DOMAIN_MAP['Other'] = set(['^Commission/','^LCG_Interfaces/','^PackDist/','^ReleaseTests/']) -DOMAIN_MAP['Overlay'] = set(['Overlay']) -DOMAIN_MAP['Reconstruction'] = set(['^Reconstruction/']) -DOMAIN_MAP['Simulation'] = set(['^Simulation/','G4','InDetSimUtils','SubDetectorEnvelopes','SimEvent']) -DOMAIN_MAP['Tau'] = set(['Tau']) -DOMAIN_MAP['Test'] = set(['^AtlasTest/','^TestPolicy/']) -DOMAIN_MAP['TestBeam'] = set(['^TestBeam/']) -DOMAIN_MAP['Tile'] = set(['^TileCalorimeter/']) -DOMAIN_MAP['Tools'] = set(['^Tools/']) -DOMAIN_MAP['Tracking'] = set(['^Tracking/']) -DOMAIN_MAP['Trigger'] = set(['^Trigger/','^HLT/','Trig','^DetectorDescription/.?Reg','^DetectorDescription/RoiDescriptor','RegionSelector']) -DOMAIN_MAP['TriggerMenu'] = set(['^Trigger/TriggerCommon/TriggerMenu']) - diff --git a/CI/get_srl_domain_information.py b/CI/get_srl_domain_information.py deleted file mode 100644 index 852aff382c8dbddf8a6b791de6cd18ad85cf2a3b..0000000000000000000000000000000000000000 --- a/CI/get_srl_domain_information.py +++ /dev/null @@ -1,93 +0,0 @@ -import argparse, glob, logging, os, pickle, re, sys - -def get_srl_package_mapping(directory): - domains = {} - logging.debug("get SRL domain <--> package mapping from directory '%s'" % directory) - srl_files = glob.glob(os.path.join(directory,"atlas-srl-*")) - for f in srl_files: - logging.debug("reading file '%s'" % f) - m = re.match("atlas-srl-(\w+)",os.path.basename(f)) - domain = m.group(1) - logging.info("extracted domain '%s' from filename '%s'" % (domain,os.path.basename(f))) - fh = open(f) - pkgs = [package.strip() for package in fh.readlines() if package.strip()] - fh.close() - logging.debug("found packages %s in file '%s'" % (str(pkgs),f)) - if not domain in domains: - domains[domain] = set(pkgs) - else: - domains[domain].update(pkgs) - - for domain,packages in domains.items(): - logging.debug("domain '%s': packages = %s" % (domain,str(packages))) - - return domains - -def get_srl_group_members(group_def_file): - experts = {} - logging.debug("read SRL group definitions from '%s'" % group_def_file) - fh = open(group_def_file) - groups = [l.strip() for l in fh.readlines()] - fh.close() - for g in groups: - m = re.match("atlas-srl-(\w+).*=([\w ,]+)",g) - domain = m.group(1) - expert_list = m.group(2).split(',') - members = set([e.strip() for e in expert_list]) - logging.info("group '%s' consists of members %s" % (domain,members)) - if not domain in experts: - experts[domain] = members - else: - experts[domain].update(members) - - for domain,expert_list in experts.items(): - logging.debug("domain '%s': experts = %s" % (domain,str(expert_list))) - - return experts - -def merge_srl_packages_experts(pkg_map,expert_map): - domains = {} - for domain,pkgs in pkg_map.items(): - if domain in expert_map: - domains[domain] = {"packages": pkgs, "experts": expert_map[domain]} - else: - logging.error("found package mapping for domain '%s' but no experts" % domain) - - missing = set(expert_map.keys()) - set(domains.keys()) - if len(missing) > 0: - logging.error("following domains have experts but no packages: %s" % str(missing)) - - return domains - -def main(): - parser = argparse.ArgumentParser(description="ATLAS SRL mapper", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("indir",type=str,help="directory with ATLAS SRL definitions") - parser.add_argument("outfile",type=str,help="output pickle file with dictionary") - parser.add_argument("-v","--verbose",default="INFO",choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],help="verbosity level") - - # get command line arguments - args = parser.parse_args() - args.indir = os.path.abspath(args.indir) - - # configure log output - logging.basicConfig(format='%(asctime)s %(levelname)-10s %(message)s', - datefmt='%H:%M:%S', - level=logging.getLevelName(args.verbose)) - - # delegate - packages = get_srl_package_mapping(os.path.join(args.indir,"done")) - experts = get_srl_group_members(os.path.join(args.indir,"atlas-srl_group_defs.txt")) - domains = merge_srl_packages_experts(packages,experts) - - if os.path.isfile(args.outfile): - var = "" - while not (var == 'y' or var == 'n'): - var = raw_input("The output file '%s' already exists. Do you want to overwrite it? y|[n] " % args.outfile) - if var == 'n': - sys.exit(0) - - pickle.dump(domains,open(args.outfile,'w')) - -if __name__ == "__main__": - main() diff --git a/CI/gitlab_mr_helpers.py b/CI/gitlab_mr_helpers.py deleted file mode 100644 index edf627f84c60cb9ea854e1937e4386a11555d9f1..0000000000000000000000000000000000000000 --- a/CI/gitlab_mr_helpers.py +++ /dev/null @@ -1,80 +0,0 @@ -import logging, os, re -from domain_map import DOMAIN_MAP - -DOMAINS = DOMAIN_MAP.keys() - -def print_collection(c): - return " - " + "\n - ".join(c) - -def map_filename_to_package(fname): - """ - fname ... full path of filename - - note: only works if this file resides in <ATHENA_ROOT>/CI - - return: package path for given full file path - """ - # get Athena root directory (specific to current layout) which is one level up - athena_root = os.path.dirname(os.path.abspath(os.path.join(os.path.realpath(__file__),'../'))) - logging.debug("found Athena root directory '%s'",athena_root) - - # start from directory name - pkg_name = os.path.dirname(fname) - - # recursively move up in directory hierarchy and look for CMakeLists.txt - while pkg_name and pkg_name != '/': - if os.path.isfile(os.path.join(athena_root,pkg_name,'CMakeLists.txt')): - break - pkg_name = os.path.dirname(pkg_name) - - logging.debug("mapped file '%s' to package '%s'" % (fname,pkg_name)) - return pkg_name - -def map_package_domain(pkg): - """ - map the package name to a domain - - pkg ... package path - - return: domain(s) of package - """ - domains = set() - for domain,pattern_list in DOMAIN_MAP.items(): - for pattern in pattern_list: - if re.search(pattern,pkg,re.I): - domains.add(domain) - break - - return domains - -def list_changed_packages(mr): - """ - mr ... Gitlab merge request object - - return: sorted set of package names affected by this merge request - """ - changed_files = set([c[p] for c in mr.changes()['changes'] for p in ['old_path','new_path']]) - logging.debug("changed files:\n" + print_collection(changed_files)) - - return sorted(set([map_filename_to_package(f) for f in changed_files])) - -def list_affected_domains(packages): - """ - packages ... set of Athena packages - - return: sorted set of domains responsible for the given packages - and a set of all packages which could not be mapped - """ - domains = set() - unmapped_pkgs = set() - for pkg in packages: - this_domain = map_package_domain(pkg) - logging.debug("mapped package '%s' to domain '%s'" % (pkg,this_domain)) - if not this_domain: - unmapped_pkgs.add(pkg) - else: - domains.update(this_domain) - logging.debug("affected domains:\n" + print_collection(domains)) - logging.debug("unmapped packages:\n" + print_collection(unmapped_pkgs)) - - return domains,unmapped_pkgs diff --git a/CI/handle_new_mr.py b/CI/handle_new_mr.py deleted file mode 100644 index 4e1d032065c47b346e1f1e0c8c7dfb1afe48fb04..0000000000000000000000000000000000000000 --- a/CI/handle_new_mr.py +++ /dev/null @@ -1,174 +0,0 @@ -import argparse, gitlab, logging, re, sys -from gitlab.exceptions import GitlabGetError -from gitlab_mr_helpers import print_collection, list_changed_packages, list_affected_domains -from watch_list import WATCH_LIST - -def comment_affected_packages(packages): - """ - prepare initial comment to merge request listing affected packages - - packages ... list of affected packages - - return: comment text - """ - n_packages = len(packages) - if n_packages == 0: - comment = "This merge request affects no known packages. Consult an expert!" - elif n_packages == 1: - comment = "This merge request affects 1 package: \n- " + list(packages)[0] - elif n_packages <= 20: - comment = "This merge request affects %d packages: \n- " % n_packages - comment += " \n- ".join(sorted(packages)) - else: - comment = "This merge request affects %d packages. Since this is a long list, I will not print it here." % n_packages - - return comment - -def warn_unmapped_packages(packages): - """ - prepare warning for packages which could not be mapped to domains - - packages ... set of packages which could not be mapped - - return: comment text - """ - if not packages: - return "" - - comment = " \n \nCould not map the following packages to a domain: \n \n" - for p in packages: - comment += "- %s \n" % p - return comment - -def add_watchers(packages): - """ - prepare comment to add watchers for the modified packages - - packages ... set of affected packages - - return: comment text - """ - watchers = set() - for pkg in packages: - for pattern,users in WATCH_LIST.items(): - if re.search(pattern,pkg): - logging.debug("package '%s' matches watch list pattern '%s'" % (pkg,pattern)) - logging.debug("subscribing users:\n" + print_collection(users)) - watchers.update(users) - - logging.debug("list of watchers for this MR:\n" + print_collection(watchers)) - - if watchers: - watchers = ['@' + w for w in watchers] - comment = " \n \nAdding " - comment += " ,".join(watchers) - comment += " as watcher%s" % ('' if len(watchers) == 1 else 's') - else: - comment = "" - - return comment - -def add_labels(mr,domains): - """ - add review and domain labels to merge request - - mr ... Gitlab merge request object - domains ... set of affected domains - - return: updated list of labels - """ - # labels assigned during MR creation - labels = set(mr.labels) - logging.debug("labels assigned during MR creation:\n" + print_collection(labels)) - # assign labels for all affected domains - for d in domains: - labels.add(d) - # remove possible complete review labels - for label in list(labels): - if re.match("review-",label): - labels.discard(label) - # add review-pending flag - labels.add("review-pending-level-1") - # add label for target branch - labels.add(mr.target_branch) - logging.debug("updated labels:\n" + print_collection(labels)) - mr.labels = list(labels) - mr.save() - - return labels - -def handle_new_merge_request(args): - """ - handle new merge request in Gitlab - - This involves the following steps: - - get a list of packages affected by these changes - - post a comment about the affected packages - - determine domain experts and mention those in a comment - - assign labels for sign-off process by domain experts - """ - # get GitLab API handler - gl = gitlab.Gitlab(args.url,args.token) - try: - # get Gitlab project object - project = gl.projects.get(args.project_name) - logging.debug("retrieved Gitlab project handle") - # get Gitlab merge request object - mr = project.mergerequests.get(args.mr_id) - logging.debug("retrieved Gitlab merge request handle") - except GitlabGetError as e: - logging.critical("error communication with Gitlab API '%s'" % (e.error_message)) - sys.exit(1) - - handled_mr_states = ["opened","reopened","merged"] - if not mr.state in handled_mr_states: - logging.debug("ignore merge request in '%s' state" % mr.state) - sys.exit(0) - - # get list of affected packages - affected_packages = filter(None,list_changed_packages(mr)) - # get list of domains - affected_domains,unmapped_pkgs = list_affected_domains(affected_packages) - # add CI as fallback check if no responsible domain was found - if not affected_domains: - logging.debug("no responsible domain found -> please check that this is expected") - affected_domains = set(['no-domain']) - # assemble atlasbot comment - comment = "" - # add comment listing affected packages - comment += comment_affected_packages(affected_packages) - # warn about unmapped packages - comment += warn_unmapped_packages(unmapped_pkgs) - # add watchers for affected packages - comment += add_watchers(affected_packages) - - # publish comment - logging.debug("add comment to merge request:\n\n" + comment + "\n") - mr.notes.create({'body':comment}) - - # add labels - add_labels(mr,affected_domains) - -def main(): - parser = argparse.ArgumentParser(description="GitLab merge request handler",formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-m","--merge-request-id",dest="mr_id",required=True,type=int,help="(unique) ID of merge request (not the project specific IID)") - parser.add_argument("-p","--project-name",dest="project_name",required=True,help="GitLab project with namespace (e.g. user/my-project)") - parser.add_argument("-t","--token",required=True,help="private GitLab user token") - parser.add_argument("-u","--url",default="https://gitlab.cern.ch",help="URL of GitLab instance") - parser.add_argument("-v","--verbose",default="INFO",choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],help="verbosity level") - - # get command line arguments - args = parser.parse_args() - - # configure log output - logging.basicConfig(format='%(asctime)s %(levelname)-10s %(message)s', - datefmt='%H:%M:%S', - level=logging.getLevelName(args.verbose)) - - logging.debug("parsed arguments:\n" + repr(args)) - - # delegate - handle_new_merge_request(args) - -if __name__ == "__main__": - main() diff --git a/CI/post_ci_result.py b/CI/post_ci_result.py deleted file mode 100644 index 910fa9710a24624544c387edb450bedc84df97d5..0000000000000000000000000000000000000000 --- a/CI/post_ci_result.py +++ /dev/null @@ -1,93 +0,0 @@ -import argparse, gitlab, logging, sys -from gitlab.exceptions import GitlabGetError, GitlabCreateError - -def compose_result_text(status): - """ - generate comment line describing global result of CI job - - status ... Jenkins exit code as string - """ - text = ":question: **CI Result UNSET**" - if status == "SUCCESS": - text = ":white_check_mark: **CI Result SUCCESS**" - elif status == "FAILURE": - text = ":negative_squared_cross_mark: **CI Result FAILURE**" - elif status == "ABORT": - text = ":point_up: **CI Result ABORTED**" - - return text - -def compose_stage_text(stage,result): - """ - generate comment line describing result of CI job stage - - result ... Jenkin exit code as string - """ - if result == "SUCCESS": - text = ":white_check_mark: " - elif result == "FAILURE": - text = ":o: " - # everything else means that this stage was not run - else: - text = ":white_circle: " - - return text + stage - -def main(): - parser = argparse.ArgumentParser(description="GitLab merge request commentator",formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--externals",required=True,type=str,help="result of external building step") - parser.add_argument("--cmake",required=True,type=str,help="result of cmake configuration step") - parser.add_argument("--make",required=True,type=str,help="result of build step") - parser.add_argument("--test",required=True,type=str,help="result of test step") - parser.add_argument("--status",required=True,choices=["SUCCESS","FAILURE","ABORT"],help="final result of CI job") - parser.add_argument("-i","--ci-job-id",dest="ci_id",required=True,type=int,help="ID of CI job") - parser.add_argument("-m","--merge-request-id",dest="mr_id",required=True,type=int,help="(unique) ID of merge request (not the project specific IID)") - parser.add_argument("-n","--nicos-project-relname",dest="nicos_name",required=True,type=str,help="NICOS project release name for this build") - parser.add_argument("-p","--project-name",dest="project_name",required=True,help="GitLab project with namespace (e.g. user/my-project)") - parser.add_argument("-t","--token",required=True,help="private GitLab user token") - parser.add_argument("-u","--url",default="https://gitlab.cern.ch",help="URL of GitLab instance") - parser.add_argument("-v","--verbose",default="INFO",choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],help="verbosity level") - - # get command line arguments - args = parser.parse_args() - - # configure log output - logging.basicConfig(format='%(asctime)s %(levelname)-10s %(message)s', - datefmt='%H:%M:%S', - level=logging.getLevelName(args.verbose)) - - logging.debug("parsed arguments:\n" + repr(args)) - - # get GitLab API handler - gl = gitlab.Gitlab(args.url,args.token) - try: - # get Gitlab project object - project = gl.projects.get(args.project_name) - logging.debug("retrieved Gitlab project handle") - # get Gitlab merge request object - mr = project.mergerequests.get(args.mr_id) - logging.debug("retrieved Gitlab merge request handle") - except GitlabGetError as e: - logging.critical("error communication with Gitlab API '%s'" % (e.error_message)) - sys.exit(1) - - # prepare individual parts of the CI comment - comment = compose_result_text(args.status) - comment += " \n" - comment += compose_stage_text("externals",args.externals) + " \n" - comment += compose_stage_text("cmake",args.cmake) + " \n" - comment += compose_stage_text("make",args.make) + " \n" - comment += compose_stage_text("test",args.test) + " \n" - comment += " \n" - comment += "Full details available at [NICOS {0}](http://atlas-nightlies-browser.cern.ch/~platinum/nightlies/info?tp=g&nightly=MR-CI-builds&rel={0}&ar=*) \n".format(args.nicos_name) - comment += "For experts only: Jenkins output [[CI-MERGE-REQUEST {0}]](http://aibuild080.cern.ch:8080/job/CI-MERGE-REQUEST/{0}/) (for remote access see instructions in Jenkins section [here](https://atlassoftwaredocs.web.cern.ch/MRtutorial/tools/))".format(args.ci_id) - - logging.debug(comment) - try: - mr.notes.create({'body':comment}) - except GitlabCreateError as e: - logging.critical("error creating MR comment '%s'" % (e.error_message)) - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/CI/run_unit_tests_for_mr.py b/CI/run_unit_tests_for_mr.py deleted file mode 100644 index 551e1ef318422bc2c0f9322ee5b28436e666d933..0000000000000000000000000000000000000000 --- a/CI/run_unit_tests_for_mr.py +++ /dev/null @@ -1,71 +0,0 @@ -import argparse, gitlab, logging, os, subprocess, sys -from gitlab.exceptions import GitlabGetError -from gitlab_mr_helpers import print_collection, map_filename_to_package - -def run_unit_tests(args): - # get GitLab API handler - gl = gitlab.Gitlab(args.url,args.token) - try: - # get Gitlab project object - project = gl.projects.get(args.project_name) - logging.debug("retrieved Gitlab project handle") - # get Gitlab merge request object - mr = project.mergerequests.get(args.mr_id) - logging.debug("retrieved Gitlab merge request handle") - except GitlabGetError as e: - logging.critical("error communication with Gitlab API '%s'" % (e.error_message)) - sys.exit(1) - - changes = mr.changes()['changes'] - changed_files = set([c[p] for c in changes for p in ['old_path','new_path']]) - logging.debug("changed files:\n" + print_collection(changed_files)) - affected_packages = sorted(set([map_filename_to_package(f) for f in changed_files])) - - # construct list of patterns for matching test labels - pattern_list = [] - for package_path in affected_packages: - # ignore empty package paths which would trigger all tests - # (empty package paths may appear due to failed mapping) - if not package_path: - continue - - # label is package name and not full package path - pattern_list.append("^" + os.path.basename(package_path) + "$") - - # only run tests if we found some patterns for test labels - if pattern_list: - # assemble ctest command - ctest_cmd = "ctest --output-on-failure -O ctest.log " - ctest_cmd += "-L \"" + "|".join(pattern_list) + "\"" - - # execute - logging.debug("ctest command = '%s'" % ctest_cmd) - status = subprocess.call(ctest_cmd,shell=True) - return status - # no tests -> return success - else: - return 0 - -def main(): - parser = argparse.ArgumentParser(description="ATLAS unit test runner", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-m","--merge-request-id",dest="mr_id",required=True,type=int,help="(unique) ID of merge request (not the project specific IID)") - parser.add_argument("-p","--project-name",dest="project_name",required=True,help="GitLab project with namespace (e.g. user/my-project)") - parser.add_argument("-t","--token",required=True,help="private GitLab user token") - parser.add_argument("-u","--url",default="https://gitlab.cern.ch",help="URL of GitLab instance") - parser.add_argument("-v","--verbose",default="INFO",choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],help="verbosity level") - - # get command line arguments - args = parser.parse_args() - - # configure log output - logging.basicConfig(format='%(asctime)s %(levelname)-10s %(message)s', - datefmt='%H:%M:%S', - level=logging.getLevelName(args.verbose)) - - # delegate - status = run_unit_tests(args) - sys.exit(status) - -if __name__ == "__main__": - main() diff --git a/CI/sweep_MR.py b/CI/sweep_MR.py deleted file mode 100644 index 63aeefb4f039ddcf8b6ff1621b16916c1988fc94..0000000000000000000000000000000000000000 --- a/CI/sweep_MR.py +++ /dev/null @@ -1,287 +0,0 @@ -import argparse, gitlab, logging, os, re, subprocess, sys, yaml -from gitlab.exceptions import GitlabGetError, GitlabCreateError, GitlabCherryPickError -from gitlab_mr_helpers import list_changed_packages - -def execute_command_with_retry(cmd,max_attempts=3): - logging.debug("running command '%s' with max attempts %d",cmd,max_attempts) - attempt = 0 - while attempt < max_attempts: - attempt += 1 - logging.debug('running attempt %d',attempt) - process = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True) - out,err = process.communicate() - status = process.returncode - out = out.strip() - err = err.strip() - logging.debug('command returned %d',status) - if out: - logging.debug('stdout:\n%s',out) - if err: - logging.debug('stderr:\n%s',err) - - # break loop if execution was successfull - if status == 0: - break - - return status,out,err - -def get_list_of_merge_commits(branch,since): - logging.info("looking for merge commits on '%s' since '%s'",branch,since) - git_cmd = 'git log --merges --first-parent --oneline --since="{0}" {1}'.format(since,branch) - status,out,_ = execute_command_with_retry(git_cmd) - - # bail out in case of errors - if status != 0: - logging.critical("failed to retrieve merge commits") - return None - - # extract hashes of merge commits - hash_list = set() - for line in out.split('\n'): - # skip empty lines - if not line: - continue - match = re.match(r"[0-9a-f]{6,}",line) - if not match: - logging.critical("could not extract merge commit hash from '%s'",line) - continue - hash_list.add(match.group()) - logging.info("found %d merge commits",len(hash_list)) - logging.debug("hashes : %s",repr(hash_list)) - - return hash_list - -def get_sweep_target_branch_rules(src_branch): - git_cmd = "git show {0}:CI/config.yaml".format(src_branch) - status,out,_ = execute_command_with_retry(git_cmd) - - # bail out in case of errors - if status != 0: - logging.critical("failed to retrieve CI configuration") - return None - - try: - CI_config = yaml.load(out) - except: - logging.critical("failed to interpret the following text as YAML:\n%s",out) - return None - - if not 'sweep-targets' in CI_config or not CI_config['sweep-targets']: - logging.info("no sweep targets for branch '%s' configured",src_branch) - return None - - target_branch_rules = CI_config['sweep-targets'] - logging.info("read %d sweeping rules for MRs to '%s",len(target_branch_rules),src_branch) - logging.debug("sweeping rules: %r",target_branch_rules) - - return target_branch_rules - -def cherry_pick_mr(merge_commit,source_branch,target_branch_rules,project): - # keep track of successful and failed cherry-picks - good_branches = set() - failed_branches = set() - - # get merge commit object - try: - commit = project.commits.get(merge_commit) - except GitlabGetError as e: - logging.critical("failed to get merge commit '%s' with\n%s",merge_commit,e.error_message) - return - - # get merge request IID from commit message - _,out,_ = execute_command_with_retry("git show {0}".format(merge_commit)) - match = re.search("See merge request !(\d+)",out) - if match: - MR_IID = int(match.group(1)) - logging.debug("merge commit '%s' corresponds to MR IID %d",merge_commit,MR_IID) - - # retrieve gitlab API object - try: - mr_handle = project.mergerequests.list(iid=MR_IID)[0] - except: - logging.critical("failed to retrieve Gitlab merge request handle") - return - else: - logging.debug("retrieved Gitlab merge request handle") - else: - logging.critical("failed to determine MR IID") - return - - # save original author so that we can add as watcher - original_mr_author = mr_handle.author.username - - # handle sweep labels - labels = set(mr_handle.labels) - if "sweep:done" in labels: - logging.info("merge commit '%s' was already swept -> skipping",merge_commit) - return - if "sweep:ignore" in labels: - logging.info("merge commit '%s' is marked as ignore -> skipping",merge_commit) - return - - labels.add("sweep:done") - mr_handle.labels = list(labels) - mr_handle.save() - - # get list of affected packages for this MR - affected_packages = list_changed_packages(mr_handle) - logging.debug("MR %d affects the following packages: %r",MR_IID,affected_packages) - - # determine set of target branches from rules and affected packages - target_branches = set() - for rule,branches in target_branch_rules.items(): - # get pattern expression for affected packages - pkg_pattern = re.compile(rule) - # only add target branches if ALL affected packages match the given pattern - matches = [pkg_pattern.match(pkg_name) for pkg_name in affected_packages] - if all(matches): - logging.debug("add branches for rule '%s'",rule) - target_branches.update(branches) - else: - logging.debug("skip branches for rule '%s'",rule) - - logging.info("MR %d is swept to %d branches",MR_IID,len(target_branches)) - logging.debug("sweep target branches: %r",target_branches) - - # get initial MR commit title and description - _,mr_title,_ = execute_command_with_retry('git show {0} --pretty=format:"%s"'.format(merge_commit)) - _,mr_desc,_ = execute_command_with_retry('git show {0} --pretty=format:"%b"'.format(merge_commit)) - - # perform cherry-pick to all target branches - for tbranch in target_branches: - failed = False - - # create remote branch for containing the cherry-pick commit - cherry_pick_branch = "cherry-pick-{0}-{1}".format(merge_commit,tbranch) - try: - project.branches.create({'branch_name': cherry_pick_branch, 'ref': tbranch}) - except GitlabCreateError as e: - logging.critical("failed to create remote branch '%s' with\n%s",cherry_pick_branch,e.error_message) - failed = True - else: - # perform cherry-pick - try: - commit.cherry_pick(cherry_pick_branch) - except GitlabCherryPickError as e: - logging.critical("failed to cherry pick merge commit '%s' with\n%s",merge_commit,e.error_message) - failed = True - - # only create MR if cherry-pick succeeded - if failed: - logging.critical("failed to cherry-pick '%s' into '%s'",merge_commit,tbranch) - failed_branches.add(tbranch) - else: - logging.info("cherry-picked '%s' into '%s'",merge_commit,tbranch) - - # create merge request - mr_data = {} - mr_data['source_branch'] = cherry_pick_branch - mr_data['target_branch'] = tbranch - mr_data['title'] = mr_title - mr_data['description'] = mr_desc - mr_data['labels'] = "sweep:from {0}".format(os.path.basename(source_branch)) - mr_data['remove_source_branch'] = True - try: - new_mr = project.mergerequests.create(mr_data) - except GitlabCreateError as e: - logging.critical("failed to create merge request for '%s' into '%s' with\n%s",cherry_pick_branch,tbranch,e.error_message) - failed_branches.add(tbranch) - else: - good_branches.add(tbranch) - # adding original author as watcher - notification_text = "Adding original author @{0:s} as watcher.".format(original_mr_author) - new_mr.notes.create({'body': notification_text}) - - # compile comment about sweep results - comment = "**Sweep summary** \n" - if good_branches: - comment += "successful: \n* " + "\n* ".join(sorted(good_branches)) + " \n \n" - if failed_branches: - comment += "failed: \n* " + "\n* ".join(sorted(failed_branches)) - # add label to original MR indicating cherry-pick problem - mr_handle.labels = list(set(mr_handle.labels) | {"sweep:failed"}) - mr_handle.save() - - # add sweep summary to MR in Gitlab - try: - mr_handle.notes.create({'body': comment}) - except GitlabCreateError as e: - logging.critical("failed to add comment with sweep summary with\n{0:s}".format(e.error_message)) - -def main(): - parser = argparse.ArgumentParser(description="GitLab merge request commentator",formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("-b","--branch",required=True,help="remote branch whose merge commits should be swept (e.g. origin/master)") - parser.add_argument("-p","--project-name",dest="project_name",required=True,help="GitLab project with namespace (e.g. user/my-project)") - parser.add_argument("-s","--since",default="1 day",help="time interval for sweeping MR (e.g. 1 week)") - parser.add_argument("-t","--token",required=True,help="private GitLab user token") - parser.add_argument("-u","--url",default="https://gitlab.cern.ch",help="URL of GitLab instance") - parser.add_argument("-v","--verbose",default="INFO",choices=["DEBUG","INFO","WARNING","ERROR","CRITICAL"],help="verbosity level") - parser.add_argument("--repository-root",dest="root",default=os.path.dirname(os.path.abspath(os.path.join(os.path.realpath(__file__),'../'))),help="path to root directory of git repository") - - # get command line arguments - args = parser.parse_args() - - # configure log output - logging.basicConfig(format='%(asctime)s %(levelname)-10s %(message)s', - datefmt='%H:%M:%S', - level=logging.getLevelName(args.verbose)) - - logging.debug("parsed arguments:\n" + repr(args)) - - # we only support porting merge commits from remote branches since we expect - # them to be created through the Gitlab web interface - # -> branch must contain the name of the remote repository (e.g. upstream/master) - # -> infer it - tokens = args.branch.split('/') - if len(tokens) < 2: - logging.critical("expect branch to specify a remote branch (e.g. 'upstream/master')") - logging.critical("received branch '%s' which does not look like a remote branch",args.branch) - logging.critical("--> aborting") - sys.exit(1) - - # set name of remote repository - args.remote_name = tokens[0] - - # get GitLab API handler - gl = gitlab.Gitlab(args.url,args.token) - try: - # get Gitlab project object - project = gl.projects.get(args.project_name) - logging.debug("retrieved Gitlab project handle") - except GitlabGetError as e: - logging.critical("error communication with Gitlab API '%s'" % (e.error_message)) - sys.exit(1) - - # get top-level directory of git repository (specific to current directory structure) - workdir = os.path.abspath(args.root) - - logging.info("changing to root directory of git repository '%s'",workdir) - current_dir = os.getcwd() - os.chdir(workdir) - - # fetch latest changes - status,_,_ = execute_command_with_retry("git fetch --prune {0}".format(args.remote_name)) - if status != 0: - logging.critical("failed to fetch from '%s'",args.remote_name) - return None - - # get list of branches MRs should be forwarded to - target_branch_rules = get_sweep_target_branch_rules(args.branch) - if not target_branch_rules: - logging.info("no sweeping rules for branch '%s' found",args.branch) - sys.exit(0) - - # get list of MRs in relevant period - MR_list = get_list_of_merge_commits(args.branch,args.since) - if not MR_list: - logging.info("no MRs to '%s' found during last %s",args.branch,args.since) - sys.exit(0) - - # do the actual cherry-picking - for mr in MR_list: - cherry_pick_mr(mr,args.branch,target_branch_rules,project) - # change back to initial directory - os.chdir(current_dir) - -if __name__ == "__main__": - main() diff --git a/CI/test/__init__.py b/CI/test/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/CI/test/test_domain_map.py b/CI/test/test_domain_map.py deleted file mode 100644 index 3fcb2fac37a9c50a5f64951f66e527cb6fdb6540..0000000000000000000000000000000000000000 --- a/CI/test/test_domain_map.py +++ /dev/null @@ -1,25 +0,0 @@ -import re, unittest -from domain_map import DOMAIN_MAP - -class TestDomainMapDictionary(unittest.TestCase): - def test_DomainMapIsDict(self): - self.assertIsInstance(DOMAIN_MAP,dict) - - def test_KeysAreStrings(self): - for domain_name in DOMAIN_MAP.keys(): - self.assertIsInstance(domain_name,str) - - def test_ValuesAreSets(self): - for package_patterns in DOMAIN_MAP.values(): - self.assertIsInstance(package_patterns,set) - - def test_PatternsAreValidRegex(self): - for package_patterns in DOMAIN_MAP.values(): - for pattern in package_patterns: - try: - _ = re.compile(pattern) - except: - self.fail("compiling '%s' as regular expression failed" % pattern) - -if __name__ == "__main__": - unittest.main() diff --git a/CI/test/test_watch_list.py b/CI/test/test_watch_list.py deleted file mode 100644 index f8221f89dfac8ece2457ea6eea902d6144dca389..0000000000000000000000000000000000000000 --- a/CI/test/test_watch_list.py +++ /dev/null @@ -1,25 +0,0 @@ -import re, unittest -from watch_list import WATCH_LIST - -class TestWatchListDictionary(unittest.TestCase): - def test_WatchListIsDict(self): - self.assertIsInstance(WATCH_LIST,dict) - - def test_ValuesAreSets(self): - for watchers in WATCH_LIST.values(): - self.assertIsInstance(watchers,set) - - def test_UsernamesAreStrings(self): - for watchers in WATCH_LIST.values(): - for username in watchers: - self.assertIsInstance(username,str) - - def test_PatternsAreValidRegex(self): - for package_pattern in WATCH_LIST.keys(): - try: - _ = re.compile(package_pattern) - except: - self.fail("compiling '%s' as regular expression failed" % package_pattern) - -if __name__ == "__main__": - unittest.main() diff --git a/CI/watch_list.py b/CI/watch_list.py deleted file mode 100644 index 31e4593ae31065fa131d6734e960c993e6c40741..0000000000000000000000000000000000000000 --- a/CI/watch_list.py +++ /dev/null @@ -1,23 +0,0 @@ -# dictionary defining a watch list for MR notifications -# keys ... must be valid regular expression -# values ... must be sets containing Gitlab usernames as strings -WATCH_LIST = {} - -WATCH_LIST['^CI$'] = set(['cgumpert']) -WATCH_LIST['Simulation'] = set(['ritsch','jchapman','vpascuzz']) -WATCH_LIST['Digitization'] = set(['jchapman']) -WATCH_LIST['Overlay'] = set(['jchapman','ahaas','tkharlam','tadej']) -WATCH_LIST['TrkiPatFitter'] = set(['pop']) -WATCH_LIST['MooPerformance'] = set(['pop']) -WATCH_LIST['JetCalibTools'] = set(['jbossios']) -WATCH_LIST['AFP'] = set(['ggach']) -WATCH_LIST['MuonEfficiencyCorrections'] = set(['nkoehler','jojungge']) -WATCH_LIST['MuonTPTools'] = set(['nkoehler','jojungge']) -WATCH_LIST['MuonPerformanceAlgs'] = set(['nkoehler','jojungge']) -WATCH_LIST['MuonPerformanceHistUtils'] = set(['nkoehler','jojungge']) -WATCH_LIST['(PixelMonitoring)|(PixelConditionsServices)|(PixelRawData)'] = set(['kzoch','ibragimo']) -WATCH_LIST['LongLivedParticleDPDMaker'] = set(['cohm','leejr','kdipetri','ctreado','will']) -WATCH_LIST['TrkV0Fitter'] = set(['bouhova']) -WATCH_LIST['TrkVertexAnalysisUtils'] = set(['bouhova']) -WATCH_LIST['InDetV0Finder'] = set(['bouhova']) -WATCH_LIST['InDetBeamSpot'] = set(['csuster', 'amorley'])