1bf215546Sopenharmony_ci#!/usr/bin/env python3 2bf215546Sopenharmony_ci# Copyright © 2020 - 2022 Collabora Ltd. 3bf215546Sopenharmony_ci# Authors: 4bf215546Sopenharmony_ci# Tomeu Vizoso <tomeu.vizoso@collabora.com> 5bf215546Sopenharmony_ci# David Heidelberg <david.heidelberg@collabora.com> 6bf215546Sopenharmony_ci# 7bf215546Sopenharmony_ci# TODO GraphQL for dependencies 8bf215546Sopenharmony_ci# SPDX-License-Identifier: MIT 9bf215546Sopenharmony_ci 10bf215546Sopenharmony_ci""" 11bf215546Sopenharmony_ciHelper script to restrict running only required CI jobs 12bf215546Sopenharmony_ciand show the job(s) logs. 13bf215546Sopenharmony_ci""" 14bf215546Sopenharmony_ci 15bf215546Sopenharmony_cifrom typing import Optional 16bf215546Sopenharmony_cifrom functools import partial 17bf215546Sopenharmony_cifrom concurrent.futures import ThreadPoolExecutor 18bf215546Sopenharmony_ci 19bf215546Sopenharmony_ciimport os 20bf215546Sopenharmony_ciimport re 21bf215546Sopenharmony_ciimport time 22bf215546Sopenharmony_ciimport argparse 23bf215546Sopenharmony_ciimport sys 24bf215546Sopenharmony_ciimport gitlab 25bf215546Sopenharmony_ci 26bf215546Sopenharmony_cifrom colorama import Fore, Style 27bf215546Sopenharmony_ci 28bf215546Sopenharmony_ciREFRESH_WAIT_LOG = 10 29bf215546Sopenharmony_ciREFRESH_WAIT_JOBS = 6 30bf215546Sopenharmony_ci 31bf215546Sopenharmony_ciURL_START = "\033]8;;" 32bf215546Sopenharmony_ciURL_END = "\033]8;;\a" 33bf215546Sopenharmony_ci 34bf215546Sopenharmony_ciSTATUS_COLORS = { 35bf215546Sopenharmony_ci "created": "", 36bf215546Sopenharmony_ci "running": Fore.BLUE, 37bf215546Sopenharmony_ci "success": Fore.GREEN, 38bf215546Sopenharmony_ci "failed": Fore.RED, 39bf215546Sopenharmony_ci "canceled": Fore.MAGENTA, 40bf215546Sopenharmony_ci "manual": "", 41bf215546Sopenharmony_ci "pending": "", 42bf215546Sopenharmony_ci "skipped": "", 43bf215546Sopenharmony_ci} 44bf215546Sopenharmony_ci 45bf215546Sopenharmony_ci# TODO: This hardcoded list should be replaced by querying the pipeline's 46bf215546Sopenharmony_ci# dependency graph to see which jobs the target jobs need 47bf215546Sopenharmony_ciDEPENDENCIES = [ 48bf215546Sopenharmony_ci "debian/x86_build-base", 49bf215546Sopenharmony_ci "debian/x86_build", 50bf215546Sopenharmony_ci "debian/x86_test-base", 51bf215546Sopenharmony_ci "debian/x86_test-gl", 52bf215546Sopenharmony_ci "debian/arm_build", 53bf215546Sopenharmony_ci "debian/arm_test", 54bf215546Sopenharmony_ci "kernel+rootfs_amd64", 55bf215546Sopenharmony_ci "kernel+rootfs_arm64", 56bf215546Sopenharmony_ci "kernel+rootfs_armhf", 57bf215546Sopenharmony_ci "debian-testing", 58bf215546Sopenharmony_ci "debian-arm64", 59bf215546Sopenharmony_ci] 60bf215546Sopenharmony_ci 61bf215546Sopenharmony_ciCOMPLETED_STATUSES = ["success", "failed"] 62bf215546Sopenharmony_ci 63bf215546Sopenharmony_ci 64bf215546Sopenharmony_cidef get_gitlab_project(glab, name: str): 65bf215546Sopenharmony_ci """Finds a specified gitlab project for given user""" 66bf215546Sopenharmony_ci glab.auth() 67bf215546Sopenharmony_ci username = glab.user.username 68bf215546Sopenharmony_ci return glab.projects.get(f"{username}/mesa") 69bf215546Sopenharmony_ci 70bf215546Sopenharmony_ci 71bf215546Sopenharmony_cidef wait_for_pipeline(project, sha: str): 72bf215546Sopenharmony_ci """await until pipeline appears in Gitlab""" 73bf215546Sopenharmony_ci print("⏲ for the pipeline to appear..", end="") 74bf215546Sopenharmony_ci while True: 75bf215546Sopenharmony_ci pipelines = project.pipelines.list(sha=sha) 76bf215546Sopenharmony_ci if pipelines: 77bf215546Sopenharmony_ci print("", flush=True) 78bf215546Sopenharmony_ci return pipelines[0] 79bf215546Sopenharmony_ci print("", end=".", flush=True) 80bf215546Sopenharmony_ci time.sleep(1) 81bf215546Sopenharmony_ci 82bf215546Sopenharmony_ci 83bf215546Sopenharmony_cidef print_job_status(job) -> None: 84bf215546Sopenharmony_ci """It prints a nice, colored job status with a link to the job.""" 85bf215546Sopenharmony_ci if job.status == "canceled": 86bf215546Sopenharmony_ci return 87bf215546Sopenharmony_ci 88bf215546Sopenharmony_ci print( 89bf215546Sopenharmony_ci STATUS_COLORS[job.status] 90bf215546Sopenharmony_ci + " job " 91bf215546Sopenharmony_ci + URL_START 92bf215546Sopenharmony_ci + f"{job.web_url}\a{job.name}" 93bf215546Sopenharmony_ci + URL_END 94bf215546Sopenharmony_ci + f" :: {job.status}" 95bf215546Sopenharmony_ci + Style.RESET_ALL 96bf215546Sopenharmony_ci ) 97bf215546Sopenharmony_ci 98bf215546Sopenharmony_ci 99bf215546Sopenharmony_cidef print_job_status_change(job) -> None: 100bf215546Sopenharmony_ci """It reports job status changes.""" 101bf215546Sopenharmony_ci if job.status == "canceled": 102bf215546Sopenharmony_ci return 103bf215546Sopenharmony_ci 104bf215546Sopenharmony_ci print( 105bf215546Sopenharmony_ci STATUS_COLORS[job.status] 106bf215546Sopenharmony_ci + " job " 107bf215546Sopenharmony_ci + URL_START 108bf215546Sopenharmony_ci + f"{job.web_url}\a{job.name}" 109bf215546Sopenharmony_ci + URL_END 110bf215546Sopenharmony_ci + f" has new status: {job.status}" 111bf215546Sopenharmony_ci + Style.RESET_ALL 112bf215546Sopenharmony_ci ) 113bf215546Sopenharmony_ci 114bf215546Sopenharmony_ci 115bf215546Sopenharmony_cidef pretty_wait(sec: int) -> None: 116bf215546Sopenharmony_ci """shows progressbar in dots""" 117bf215546Sopenharmony_ci for val in range(sec, 0, -1): 118bf215546Sopenharmony_ci print(f"⏲ {val} seconds", end="\r") 119bf215546Sopenharmony_ci time.sleep(1) 120bf215546Sopenharmony_ci 121bf215546Sopenharmony_ci 122bf215546Sopenharmony_cidef monitor_pipeline( 123bf215546Sopenharmony_ci project, pipeline, target_job: Optional[str], dependencies, force_manual: bool 124bf215546Sopenharmony_ci) -> tuple[Optional[int], Optional[int]]: 125bf215546Sopenharmony_ci """Monitors pipeline and delegate canceling jobs""" 126bf215546Sopenharmony_ci statuses = {} 127bf215546Sopenharmony_ci target_statuses = {} 128bf215546Sopenharmony_ci 129bf215546Sopenharmony_ci if not dependencies: 130bf215546Sopenharmony_ci dependencies = [] 131bf215546Sopenharmony_ci dependencies.extend(DEPENDENCIES) 132bf215546Sopenharmony_ci 133bf215546Sopenharmony_ci if target_job: 134bf215546Sopenharmony_ci target_jobs_regex = re.compile(target_job.strip()) 135bf215546Sopenharmony_ci 136bf215546Sopenharmony_ci while True: 137bf215546Sopenharmony_ci to_cancel = [] 138bf215546Sopenharmony_ci for job in pipeline.jobs.list(all=True, sort="desc"): 139bf215546Sopenharmony_ci # target jobs 140bf215546Sopenharmony_ci if target_job and target_jobs_regex.match(job.name): 141bf215546Sopenharmony_ci if force_manual and job.status == "manual": 142bf215546Sopenharmony_ci enable_job(project, job, True) 143bf215546Sopenharmony_ci 144bf215546Sopenharmony_ci if (job.id not in target_statuses) or ( 145bf215546Sopenharmony_ci job.status not in target_statuses[job.id] 146bf215546Sopenharmony_ci ): 147bf215546Sopenharmony_ci print_job_status_change(job) 148bf215546Sopenharmony_ci target_statuses[job.id] = job.status 149bf215546Sopenharmony_ci else: 150bf215546Sopenharmony_ci print_job_status(job) 151bf215546Sopenharmony_ci 152bf215546Sopenharmony_ci continue 153bf215546Sopenharmony_ci 154bf215546Sopenharmony_ci # all jobs 155bf215546Sopenharmony_ci if (job.id not in statuses) or (job.status not in statuses[job.id]): 156bf215546Sopenharmony_ci print_job_status_change(job) 157bf215546Sopenharmony_ci statuses[job.id] = job.status 158bf215546Sopenharmony_ci 159bf215546Sopenharmony_ci # dependencies and cancelling the rest 160bf215546Sopenharmony_ci if job.name in dependencies: 161bf215546Sopenharmony_ci if job.status == "manual": 162bf215546Sopenharmony_ci enable_job(project, job, False) 163bf215546Sopenharmony_ci 164bf215546Sopenharmony_ci elif target_job and job.status not in [ 165bf215546Sopenharmony_ci "canceled", 166bf215546Sopenharmony_ci "success", 167bf215546Sopenharmony_ci "failed", 168bf215546Sopenharmony_ci "skipped", 169bf215546Sopenharmony_ci ]: 170bf215546Sopenharmony_ci to_cancel.append(job) 171bf215546Sopenharmony_ci 172bf215546Sopenharmony_ci if target_job: 173bf215546Sopenharmony_ci cancel_jobs(project, to_cancel) 174bf215546Sopenharmony_ci 175bf215546Sopenharmony_ci print("---------------------------------", flush=False) 176bf215546Sopenharmony_ci 177bf215546Sopenharmony_ci if len(target_statuses) == 1 and {"running"}.intersection( 178bf215546Sopenharmony_ci target_statuses.values() 179bf215546Sopenharmony_ci ): 180bf215546Sopenharmony_ci return next(iter(target_statuses)), None 181bf215546Sopenharmony_ci 182bf215546Sopenharmony_ci if {"failed", "canceled"}.intersection(target_statuses.values()): 183bf215546Sopenharmony_ci return None, 1 184bf215546Sopenharmony_ci 185bf215546Sopenharmony_ci if {"success", "manual"}.issuperset(target_statuses.values()): 186bf215546Sopenharmony_ci return None, 0 187bf215546Sopenharmony_ci 188bf215546Sopenharmony_ci pretty_wait(REFRESH_WAIT_JOBS) 189bf215546Sopenharmony_ci 190bf215546Sopenharmony_ci 191bf215546Sopenharmony_cidef enable_job(project, job, target: bool) -> None: 192bf215546Sopenharmony_ci """enable manual job""" 193bf215546Sopenharmony_ci pjob = project.jobs.get(job.id, lazy=True) 194bf215546Sopenharmony_ci pjob.play() 195bf215546Sopenharmony_ci if target: 196bf215546Sopenharmony_ci jtype = " " 197bf215546Sopenharmony_ci else: 198bf215546Sopenharmony_ci jtype = "(dependency)" 199bf215546Sopenharmony_ci print(Fore.MAGENTA + f"{jtype} job {job.name} manually enabled" + Style.RESET_ALL) 200bf215546Sopenharmony_ci 201bf215546Sopenharmony_ci 202bf215546Sopenharmony_cidef cancel_job(project, job) -> None: 203bf215546Sopenharmony_ci """Cancel GitLab job""" 204bf215546Sopenharmony_ci pjob = project.jobs.get(job.id, lazy=True) 205bf215546Sopenharmony_ci pjob.cancel() 206bf215546Sopenharmony_ci print(f"♲ {job.name}") 207bf215546Sopenharmony_ci 208bf215546Sopenharmony_ci 209bf215546Sopenharmony_cidef cancel_jobs(project, to_cancel) -> None: 210bf215546Sopenharmony_ci """Cancel unwanted GitLab jobs""" 211bf215546Sopenharmony_ci if not to_cancel: 212bf215546Sopenharmony_ci return 213bf215546Sopenharmony_ci 214bf215546Sopenharmony_ci with ThreadPoolExecutor(max_workers=6) as exe: 215bf215546Sopenharmony_ci part = partial(cancel_job, project) 216bf215546Sopenharmony_ci exe.map(part, to_cancel) 217bf215546Sopenharmony_ci 218bf215546Sopenharmony_ci 219bf215546Sopenharmony_cidef print_log(project, job_id) -> None: 220bf215546Sopenharmony_ci """Print job log into output""" 221bf215546Sopenharmony_ci printed_lines = 0 222bf215546Sopenharmony_ci while True: 223bf215546Sopenharmony_ci job = project.jobs.get(job_id) 224bf215546Sopenharmony_ci 225bf215546Sopenharmony_ci # GitLab's REST API doesn't offer pagination for logs, so we have to refetch it all 226bf215546Sopenharmony_ci lines = job.trace().decode("unicode_escape").splitlines() 227bf215546Sopenharmony_ci for line in lines[printed_lines:]: 228bf215546Sopenharmony_ci print(line) 229bf215546Sopenharmony_ci printed_lines = len(lines) 230bf215546Sopenharmony_ci 231bf215546Sopenharmony_ci if job.status in COMPLETED_STATUSES: 232bf215546Sopenharmony_ci print(Fore.GREEN + f"Job finished: {job.web_url}" + Style.RESET_ALL) 233bf215546Sopenharmony_ci return 234bf215546Sopenharmony_ci pretty_wait(REFRESH_WAIT_LOG) 235bf215546Sopenharmony_ci 236bf215546Sopenharmony_ci 237bf215546Sopenharmony_cidef parse_args() -> None: 238bf215546Sopenharmony_ci """Parse args""" 239bf215546Sopenharmony_ci parser = argparse.ArgumentParser( 240bf215546Sopenharmony_ci description="Tool to trigger a subset of container jobs " 241bf215546Sopenharmony_ci + "and monitor the progress of a test job", 242bf215546Sopenharmony_ci epilog="Example: mesa-monitor.py --rev $(git rev-parse HEAD) " 243bf215546Sopenharmony_ci + '--target ".*traces" ', 244bf215546Sopenharmony_ci ) 245bf215546Sopenharmony_ci parser.add_argument("--target", metavar="target-job", help="Target job") 246bf215546Sopenharmony_ci parser.add_argument("--deps", nargs="+", help="Job dependencies") 247bf215546Sopenharmony_ci parser.add_argument( 248bf215546Sopenharmony_ci "--rev", metavar="revision", help="repository git revision", required=True 249bf215546Sopenharmony_ci ) 250bf215546Sopenharmony_ci parser.add_argument( 251bf215546Sopenharmony_ci "--token", 252bf215546Sopenharmony_ci metavar="token", 253bf215546Sopenharmony_ci help="force GitLab token, otherwise it's read from ~/.config/gitlab-token", 254bf215546Sopenharmony_ci ) 255bf215546Sopenharmony_ci parser.add_argument( 256bf215546Sopenharmony_ci "--force-manual", action="store_true", help="Force jobs marked as manual" 257bf215546Sopenharmony_ci ) 258bf215546Sopenharmony_ci return parser.parse_args() 259bf215546Sopenharmony_ci 260bf215546Sopenharmony_ci 261bf215546Sopenharmony_cidef read_token(token_arg: Optional[str]) -> str: 262bf215546Sopenharmony_ci """pick token from args or file""" 263bf215546Sopenharmony_ci if token_arg: 264bf215546Sopenharmony_ci return token_arg 265bf215546Sopenharmony_ci return ( 266bf215546Sopenharmony_ci open(os.path.expanduser("~/.config/gitlab-token"), encoding="utf-8") 267bf215546Sopenharmony_ci .readline() 268bf215546Sopenharmony_ci .rstrip() 269bf215546Sopenharmony_ci ) 270bf215546Sopenharmony_ci 271bf215546Sopenharmony_ci 272bf215546Sopenharmony_ciif __name__ == "__main__": 273bf215546Sopenharmony_ci try: 274bf215546Sopenharmony_ci t_start = time.perf_counter() 275bf215546Sopenharmony_ci 276bf215546Sopenharmony_ci args = parse_args() 277bf215546Sopenharmony_ci 278bf215546Sopenharmony_ci token = read_token(args.token) 279bf215546Sopenharmony_ci 280bf215546Sopenharmony_ci gl = gitlab.Gitlab(url="https://gitlab.freedesktop.org", private_token=token) 281bf215546Sopenharmony_ci 282bf215546Sopenharmony_ci cur_project = get_gitlab_project(gl, "mesa") 283bf215546Sopenharmony_ci 284bf215546Sopenharmony_ci print(f"Revision: {args.rev}") 285bf215546Sopenharmony_ci pipe = wait_for_pipeline(cur_project, args.rev) 286bf215546Sopenharmony_ci print(f"Pipeline: {pipe.web_url}") 287bf215546Sopenharmony_ci if args.target: 288bf215546Sopenharmony_ci print(" job: " + Fore.BLUE + args.target + Style.RESET_ALL) 289bf215546Sopenharmony_ci print(f"Extra dependencies: {args.deps}") 290bf215546Sopenharmony_ci target_job_id, ret = monitor_pipeline( 291bf215546Sopenharmony_ci cur_project, pipe, args.target, args.deps, args.force_manual 292bf215546Sopenharmony_ci ) 293bf215546Sopenharmony_ci 294bf215546Sopenharmony_ci if target_job_id: 295bf215546Sopenharmony_ci print_log(cur_project, target_job_id) 296bf215546Sopenharmony_ci 297bf215546Sopenharmony_ci t_end = time.perf_counter() 298bf215546Sopenharmony_ci spend_minutes = (t_end - t_start) / 60 299bf215546Sopenharmony_ci print(f"⏲ Duration of script execution: {spend_minutes:0.1f} minutes") 300bf215546Sopenharmony_ci 301bf215546Sopenharmony_ci sys.exit(ret) 302bf215546Sopenharmony_ci except KeyboardInterrupt: 303bf215546Sopenharmony_ci sys.exit(1) 304