diff --git a/CI/domain_map.py b/CI/domain_map.py index 338fcccd85c54d4cd935d526a50b93993d5d7244..af1b2fb58876db4fe08b660a8c7402e12e6d6ea4 100644 --- a/CI/domain_map.py +++ b/CI/domain_map.py @@ -4,11 +4,12 @@ DOMAIN_MAP = {} DOMAIN_MAP['Analysis'] = set(['^PhysicsAnalysis/']) -DOMAIN_MAP['Build'] = set(['^Build/','^Projects/']) +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']) @@ -18,13 +19,14 @@ 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','MET','MissingEt','EventShape']) +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/']) +DOMAIN_MAP['Simulation'] = set(['^Simulation/','G4']) DOMAIN_MAP['Tau'] = set(['Tau']) DOMAIN_MAP['Test'] = set(['^AtlasTest/','^TestPolicy/']) DOMAIN_MAP['TestBeam'] = set(['^TestBeam/']) @@ -32,4 +34,5 @@ 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/handle_new_mr.py b/CI/handle_new_mr.py index 22d299919ccc42a2da0fefd443c248d62a10848c..4e1d032065c47b346e1f1e0c8c7dfb1afe48fb04 100644 --- a/CI/handle_new_mr.py +++ b/CI/handle_new_mr.py @@ -92,7 +92,7 @@ def add_labels(mr,domains): # add label for target branch labels.add(mr.target_branch) logging.debug("updated labels:\n" + print_collection(labels)) - mr.labels = ",".join(labels) + mr.labels = list(labels) mr.save() return labels diff --git a/CI/post_ci_result.py b/CI/post_ci_result.py index dcec9fc4e391c78922367158d6e5e5e22fdb95c5..910fa9710a24624544c387edb450bedc84df97d5 100644 --- a/CI/post_ci_result.py +++ b/CI/post_ci_result.py @@ -7,19 +7,19 @@ def compose_result_text(status): status ... Jenkins exit code as string """ - text = ":question: **CI Build UNSET**" + text = ":question: **CI Result UNSET**" if status == "SUCCESS": - text = ":white_check_mark: **CI Build SUCCESS**" + text = ":white_check_mark: **CI Result SUCCESS**" elif status == "FAILURE": - text = ":negative_squared_cross_mark: **CI Build FAILURE**" + text = ":negative_squared_cross_mark: **CI Result FAILURE**" elif status == "ABORT": - text = ":point_up: **CI Build ABORTED**" + text = ":point_up: **CI Result ABORTED**" return text def compose_stage_text(stage,result): """ - generate comment line describing result of build stage + generate comment line describing result of CI job stage result ... Jenkin exit code as string """ diff --git a/CI/run_unit_tests_for_mr.py b/CI/run_unit_tests_for_mr.py index 030c6d584d588654a9bb4f918dfbabb3dbf30f0e..551e1ef318422bc2c0f9322ee5b28436e666d933 100644 --- a/CI/run_unit_tests_for_mr.py +++ b/CI/run_unit_tests_for_mr.py @@ -35,7 +35,7 @@ def run_unit_tests(args): # only run tests if we found some patterns for test labels if pattern_list: # assemble ctest command - ctest_cmd = "ctest --output-on-failure " + ctest_cmd = "ctest --output-on-failure -O ctest.log " ctest_cmd += "-L \"" + "|".join(pattern_list) + "\"" # execute diff --git a/CI/sweep_MR.py b/CI/sweep_MR.py new file mode 100644 index 0000000000000000000000000000000000000000..63aeefb4f039ddcf8b6ff1621b16916c1988fc94 --- /dev/null +++ b/CI/sweep_MR.py @@ -0,0 +1,287 @@ +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/watch_list.py b/CI/watch_list.py index fc9e08d3245d4c08568c27dfb6ae42c2b70ebcf8..8fd99e5d9f1ef17fbb2c572694d29cdb6b6722f0 100644 --- a/CI/watch_list.py +++ b/CI/watch_list.py @@ -4,6 +4,10 @@ WATCH_LIST = {} WATCH_LIST['^CI$'] = set(['cgumpert']) -WATCH_LIST['Simulation'] = set(['ritsch']) +WATCH_LIST['Simulation'] = set(['ritsch','jchapman']) +WATCH_LIST['Digitization'] = set(['jchapman']) +WATCH_LIST['Overlay'] = set(['jchapman','ahaas','tkharlam']) WATCH_LIST['TrkiPatFitter'] = set(['pop']) -WATCH_LIST['MooPerformance'] = set(['pop']) \ No newline at end of file +WATCH_LIST['MooPerformance'] = set(['pop']) +WATCH_LIST['JetCalibTools'] = set(['jbossios']) +WATCH_LIST['AFP'] = set(['ggach'])