xref: /openbmc/qemu/scripts/ci/gitlab-failure-analysis (revision d08b8becc368cf8035eaecb49f96c135d2680615)
1*5a449678SAlex Bennée#!/usr/bin/env python3
2*5a449678SAlex Bennée#
3*5a449678SAlex Bennée# A script to analyse failures in the gitlab pipelines. It requires an
4*5a449678SAlex Bennée# API key from gitlab with the following permissions:
5*5a449678SAlex Bennée#  - api
6*5a449678SAlex Bennée#  - read_repository
7*5a449678SAlex Bennée#  - read_user
8*5a449678SAlex Bennée#
9*5a449678SAlex Bennée
10*5a449678SAlex Bennéeimport argparse
11*5a449678SAlex Bennéeimport gitlab
12*5a449678SAlex Bennéeimport os
13*5a449678SAlex Bennée
14*5a449678SAlex Bennée#
15*5a449678SAlex Bennée# Arguments
16*5a449678SAlex Bennée#
17*5a449678SAlex Bennéeclass NoneForEmptyStringAction(argparse.Action):
18*5a449678SAlex Bennée    def __call__(self, parser, namespace, value, option_string=None):
19*5a449678SAlex Bennée        if value == '':
20*5a449678SAlex Bennée            setattr(namespace, self.dest, None)
21*5a449678SAlex Bennée        else:
22*5a449678SAlex Bennée            setattr(namespace, self.dest, value)
23*5a449678SAlex Bennée
24*5a449678SAlex Bennée
25*5a449678SAlex Bennéeparser = argparse.ArgumentParser(description="Analyse failed GitLab CI runs.")
26*5a449678SAlex Bennée
27*5a449678SAlex Bennéeparser.add_argument("--gitlab",
28*5a449678SAlex Bennée                    default="https://gitlab.com",
29*5a449678SAlex Bennée                    help="GitLab instance URL (default: https://gitlab.com).")
30*5a449678SAlex Bennéeparser.add_argument("--id", default=11167699,
31*5a449678SAlex Bennée                    type=int,
32*5a449678SAlex Bennée                    help="GitLab project id (default: 11167699 for qemu-project/qemu)")
33*5a449678SAlex Bennéeparser.add_argument("--token",
34*5a449678SAlex Bennée                    default=os.getenv("GITLAB_TOKEN"),
35*5a449678SAlex Bennée                    help="Your personal access token with 'api' scope.")
36*5a449678SAlex Bennéeparser.add_argument("--branch",
37*5a449678SAlex Bennée                    type=str,
38*5a449678SAlex Bennée                    default="staging",
39*5a449678SAlex Bennée                    action=NoneForEmptyStringAction,
40*5a449678SAlex Bennée                    help="The name of the branch (default: 'staging')")
41*5a449678SAlex Bennéeparser.add_argument("--status",
42*5a449678SAlex Bennée                    type=str,
43*5a449678SAlex Bennée                    action=NoneForEmptyStringAction,
44*5a449678SAlex Bennée                    default="failed",
45*5a449678SAlex Bennée                    help="Filter by branch status (default: 'failed')")
46*5a449678SAlex Bennéeparser.add_argument("--count", type=int,
47*5a449678SAlex Bennée                    default=3,
48*5a449678SAlex Bennée                    help="The number of failed runs to fetch.")
49*5a449678SAlex Bennéeparser.add_argument("--skip-jobs",
50*5a449678SAlex Bennée                    default=False,
51*5a449678SAlex Bennée                    action='store_true',
52*5a449678SAlex Bennée                    help="Skip dumping the job info")
53*5a449678SAlex Bennéeparser.add_argument("--pipeline", type=int,
54*5a449678SAlex Bennée                    nargs="+",
55*5a449678SAlex Bennée                    default=None,
56*5a449678SAlex Bennée                    help="Explicit pipeline ID(s) to fetch.")
57*5a449678SAlex Bennée
58*5a449678SAlex Bennée
59*5a449678SAlex Bennéeif __name__ == "__main__":
60*5a449678SAlex Bennée    args = parser.parse_args()
61*5a449678SAlex Bennée
62*5a449678SAlex Bennée    gl = gitlab.Gitlab(url=args.gitlab, private_token=args.token)
63*5a449678SAlex Bennée    project = gl.projects.get(args.id)
64*5a449678SAlex Bennée
65*5a449678SAlex Bennée
66*5a449678SAlex Bennée    pipelines_to_process = []
67*5a449678SAlex Bennée
68*5a449678SAlex Bennée    # Use explicit pipeline IDs if provided, otherwise fetch a list
69*5a449678SAlex Bennée    if args.pipeline:
70*5a449678SAlex Bennée        args.count = len(args.pipeline)
71*5a449678SAlex Bennée        for p_id in args.pipeline:
72*5a449678SAlex Bennée            pipelines_to_process.append(project.pipelines.get(p_id))
73*5a449678SAlex Bennée    else:
74*5a449678SAlex Bennée        # Use an iterator to fetch the pipelines
75*5a449678SAlex Bennée        pipe_iter = project.pipelines.list(iterator=True,
76*5a449678SAlex Bennée                                           status=args.status,
77*5a449678SAlex Bennée                                           ref=args.branch)
78*5a449678SAlex Bennée        # Check each failed pipeline
79*5a449678SAlex Bennée        pipelines_to_process = [next(pipe_iter) for _ in range(args.count)]
80*5a449678SAlex Bennée
81*5a449678SAlex Bennée    # Check each pipeline
82*5a449678SAlex Bennée    for p in pipelines_to_process:
83*5a449678SAlex Bennée
84*5a449678SAlex Bennée        jobs = p.jobs.list(get_all=True)
85*5a449678SAlex Bennée        failed_jobs = [j for j in jobs if j.status == "failed"]
86*5a449678SAlex Bennée        skipped_jobs = [j for j in jobs if j.status == "skipped"]
87*5a449678SAlex Bennée        manual_jobs = [j for j in jobs if j.status == "manual"]
88*5a449678SAlex Bennée
89*5a449678SAlex Bennée        trs = p.test_report_summary.get()
90*5a449678SAlex Bennée        total = trs.total["count"]
91*5a449678SAlex Bennée        skipped = trs.total["skipped"]
92*5a449678SAlex Bennée        failed = trs.total["failed"]
93*5a449678SAlex Bennée
94*5a449678SAlex Bennée        print(f"{p.status} pipeline {p.id}, total jobs {len(jobs)}, "
95*5a449678SAlex Bennée              f"skipped {len(skipped_jobs)}, "
96*5a449678SAlex Bennée              f"failed {len(failed_jobs)}, ",
97*5a449678SAlex Bennée              f"{total} tests, "
98*5a449678SAlex Bennée              f"{skipped} skipped tests, "
99*5a449678SAlex Bennée              f"{failed} failed tests")
100*5a449678SAlex Bennée
101*5a449678SAlex Bennée        if not args.skip_jobs:
102*5a449678SAlex Bennée            for j in failed_jobs:
103*5a449678SAlex Bennée                print(f"  Failed job {j.id}, {j.name}, {j.web_url}")
104*5a449678SAlex Bennée
105*5a449678SAlex Bennée        # It seems we can only extract failing tests from the full
106*5a449678SAlex Bennée        # test report, maybe there is some way to filter it.
107*5a449678SAlex Bennée
108*5a449678SAlex Bennée        if failed > 0:
109*5a449678SAlex Bennée            ftr = p.test_report.get()
110*5a449678SAlex Bennée            failed_suites = [s for s in ftr.test_suites if
111*5a449678SAlex Bennée                             s["failed_count"] > 0]
112*5a449678SAlex Bennée            for fs in failed_suites:
113*5a449678SAlex Bennée                name = fs["name"]
114*5a449678SAlex Bennée                tests = fs["test_cases"]
115*5a449678SAlex Bennée                failed_tests = [t for t in tests if t["status"] == 'failed']
116*5a449678SAlex Bennée                for t in failed_tests:
117*5a449678SAlex Bennée                    print(f"  Failed test {t["classname"]}, {name}, {t["name"]}")
118