blob: 746f8cec863a1ab1f80e737cffb582cd355b957b [file] [log] [blame]
Harrison Mutai0e9964e2024-06-14 09:51:42 +01001#!/usr/bin/env python3
2
Boyan Karatotev97db34a2025-07-02 10:25:46 +01003import argparse
4import asyncio
Harrison Mutai0e9964e2024-06-14 09:51:42 +01005import re
Boyan Karatotev97db34a2025-07-02 10:25:46 +01006import sys
Harrison Mutai0e9964e2024-06-14 09:51:42 +01007from dataclasses import dataclass
8
Boyan Karatotev97db34a2025-07-02 10:25:46 +01009import aiohttp
Harrison Mutai0e9964e2024-06-14 09:51:42 +010010
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010011# Constants to produce the report with
Boyan Karatotev12cd5902025-06-25 09:22:46 +010012openci_url = "https://ci.trustedfirmware.org/"
Harrison Mutai0e9964e2024-06-14 09:51:42 +010013
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010014# Jenkins API helpers
15def get_job_url(job_name: str) -> str:
16 return openci_url + f"job/{job_name}/api/json"
Harrison Mutai0e9964e2024-06-14 09:51:42 +010017
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010018def get_build_url(job_name: str, build_number: str) -> str:
19 return openci_url + f"job/{job_name}/{build_number}"
20
21def get_build_api(build_url: str) -> str:
22 return build_url + "/api/json"
23
24def get_build_console(build_url: str) -> str:
25 return build_url + "/consoleText"
26
Boyan Karatotev97db34a2025-07-02 10:25:46 +010027async def get_json(session, url):
28 async with session.get(url) as response:
29 return await response.json()
30
31async def get_text(session, url):
32 async with session.get(url) as response:
33 return await response.text()
34
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010035"""Finds the latest run of a given job by name"""
Boyan Karatotev97db34a2025-07-02 10:25:46 +010036async def process_job(session, job_name: str) -> str:
37 req = await get_json(session, get_job_url(job_name))
Harrison Mutai0e9964e2024-06-14 09:51:42 +010038
Boyan Karatotev97db34a2025-07-02 10:25:46 +010039 name = req["displayName"]
40 number = req["lastCompletedBuild"]["number"]
Harrison Mutai0e9964e2024-06-14 09:51:42 +010041
Boyan Karatotev97db34a2025-07-02 10:25:46 +010042 build = Build(session, name, name, number, level=0)
43 await build.process()
44
45 return (build.passed, build.print_build_status())
Harrison Mutai0e9964e2024-06-14 09:51:42 +010046
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010047"""Represents an individual build. Will recursively fetch sub builds"""
48class Build:
Boyan Karatotev97db34a2025-07-02 10:25:46 +010049 def __init__(self, session, job_name, pretty_job_name: str, build_number: str, level: int) -> None:
50 self.session = session
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010051 self.url = get_build_url(job_name, build_number)
Boyan Karatotev97db34a2025-07-02 10:25:46 +010052 self.pretty_job_name = pretty_job_name
53 self.name = None
54 self.build_number = build_number
55 self.level = level
56
57 async def process(self):
58 req = await get_json(self.session, get_build_api(self.url))
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010059 self.passed = req["result"].lower() == "success"
60
Boyan Karatotev97db34a2025-07-02 10:25:46 +010061 self.name = self.pretty_job_name
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010062 # The full display name is "{job_name} {build_number}"
63 if self.name == "":
64 self.name = req["fullDisplayName"].split(" ")[0]
65 # and builds should show up with their configuration name
66 elif self.name == "tf-a-builder":
67 self.name = req["actions"][0]["parameters"][1]["value"]
68
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010069 self.sub_builds = []
70
71 # parent job passed => children passed. Skip
72 if not self.passed:
73 # the main jobs list sub builds nicely
74 self.sub_builds = [
75 # the gateways get an alias to differentiate them
Boyan Karatotev97db34a2025-07-02 10:25:46 +010076 Build(self.session, build["jobName"], build["jobAlias"], build["buildNumber"], self.level + 1)
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010077 for build in req.get("subBuilds", [])
78 ]
79 # gateways don't, since they determine them dynamically
80 if self.sub_builds == []:
81 self.sub_builds = [
Boyan Karatotev97db34a2025-07-02 10:25:46 +010082 Build(self.session, name, name, num, self.level + 1)
83 for name, num in await self.get_builds_from_console_log()
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010084 ]
85
Boyan Karatotev97db34a2025-07-02 10:25:46 +010086 # process sub-jobs concurrently
87 await asyncio.gather(*[
88 build.process()
89 for build in self.sub_builds
90 ])
91
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010092 # extracts (child_name, child_number) from the console output of a build
Boyan Karatotev97db34a2025-07-02 10:25:46 +010093 async def get_builds_from_console_log(self) -> str:
94 log = await get_text(self.session, get_build_console(self.url))
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +010095
96 return re.findall(r"(tf-a[-\w+]+) #(\d+) started", log)
97
Boyan Karatotev97db34a2025-07-02 10:25:46 +010098 def print_build_status(self) -> str:
99 message = "" + str(self)
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +0100100
101 for build in self.sub_builds:
102 if not build.passed:
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100103 message += build.print_build_status()
104 return message
Harrison Mutai0e9964e2024-06-14 09:51:42 +0100105
106 def __str__(self) -> str:
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +0100107 return (f"{' ' * self.level * 4}* {'✅' if self.passed else '❌'} "
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100108 f"**{self.name}** [#{self.build_number}]({self.url})\n"
Boyan Karatotev1c5e5c92025-06-27 11:46:41 +0100109 )
Harrison Mutai0e9964e2024-06-14 09:51:42 +0100110
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100111async def main(session, job_names: list[str]) -> str:
112 # process jobs concurrently
113 results = await asyncio.gather(
114 *[process_job(session, name) for name in job_names]
115 )
Harrison Mutai0e9964e2024-06-14 09:51:42 +0100116
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100117 final_msg = "🟢" if all(j[0] for j in results) else "🔴"
118 final_msg += " Daily Status\n"
119 for passed, message in results:
120 final_msg += message
Harrison Mutai0e9964e2024-06-14 09:51:42 +0100121
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100122 return final_msg
123
124async def run_local(jobs: list[str]) -> str:
125 async with aiohttp.ClientSession() as session:
126 msg = await main(session, jobs)
127 print(msg)
128
129def add_jobs_arg(parser):
130 parser.add_argument(
131 "-j", "--jobs",
132 metavar="JOB_NAME", default=["tf-a-daily"], nargs="+",
133 help="CI jobs to monitor"
134 )
Harrison Mutai0e9964e2024-06-14 09:51:42 +0100135
136if __name__ == "__main__":
Boyan Karatotev97db34a2025-07-02 10:25:46 +0100137 parser = argparse.ArgumentParser(
138 description="Latest CI run status",
139 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
140 )
141 add_jobs_arg(parser)
142
143 args = parser.parse_args(sys.argv[1:])
144
145 asyncio.run(run_local(args.jobs))