blob: 746f8cec863a1ab1f80e737cffb582cd355b957b [file] [log] [blame]
#!/usr/bin/env python3
import argparse
import asyncio
import re
import sys
from dataclasses import dataclass
import aiohttp
# Constants to produce the report with
openci_url = "https://ci.trustedfirmware.org/"
# Jenkins API helpers
def get_job_url(job_name: str) -> str:
return openci_url + f"job/{job_name}/api/json"
def get_build_url(job_name: str, build_number: str) -> str:
return openci_url + f"job/{job_name}/{build_number}"
def get_build_api(build_url: str) -> str:
return build_url + "/api/json"
def get_build_console(build_url: str) -> str:
return build_url + "/consoleText"
async def get_json(session, url):
async with session.get(url) as response:
return await response.json()
async def get_text(session, url):
async with session.get(url) as response:
return await response.text()
"""Finds the latest run of a given job by name"""
async def process_job(session, job_name: str) -> str:
req = await get_json(session, get_job_url(job_name))
name = req["displayName"]
number = req["lastCompletedBuild"]["number"]
build = Build(session, name, name, number, level=0)
await build.process()
return (build.passed, build.print_build_status())
"""Represents an individual build. Will recursively fetch sub builds"""
class Build:
def __init__(self, session, job_name, pretty_job_name: str, build_number: str, level: int) -> None:
self.session = session
self.url = get_build_url(job_name, build_number)
self.pretty_job_name = pretty_job_name
self.name = None
self.build_number = build_number
self.level = level
async def process(self):
req = await get_json(self.session, get_build_api(self.url))
self.passed = req["result"].lower() == "success"
self.name = self.pretty_job_name
# The full display name is "{job_name} {build_number}"
if self.name == "":
self.name = req["fullDisplayName"].split(" ")[0]
# and builds should show up with their configuration name
elif self.name == "tf-a-builder":
self.name = req["actions"][0]["parameters"][1]["value"]
self.sub_builds = []
# parent job passed => children passed. Skip
if not self.passed:
# the main jobs list sub builds nicely
self.sub_builds = [
# the gateways get an alias to differentiate them
Build(self.session, build["jobName"], build["jobAlias"], build["buildNumber"], self.level + 1)
for build in req.get("subBuilds", [])
]
# gateways don't, since they determine them dynamically
if self.sub_builds == []:
self.sub_builds = [
Build(self.session, name, name, num, self.level + 1)
for name, num in await self.get_builds_from_console_log()
]
# process sub-jobs concurrently
await asyncio.gather(*[
build.process()
for build in self.sub_builds
])
# extracts (child_name, child_number) from the console output of a build
async def get_builds_from_console_log(self) -> str:
log = await get_text(self.session, get_build_console(self.url))
return re.findall(r"(tf-a[-\w+]+) #(\d+) started", log)
def print_build_status(self) -> str:
message = "" + str(self)
for build in self.sub_builds:
if not build.passed:
message += build.print_build_status()
return message
def __str__(self) -> str:
return (f"{' ' * self.level * 4}* {'✅' if self.passed else '❌'} "
f"**{self.name}** [#{self.build_number}]({self.url})\n"
)
async def main(session, job_names: list[str]) -> str:
# process jobs concurrently
results = await asyncio.gather(
*[process_job(session, name) for name in job_names]
)
final_msg = "🟢" if all(j[0] for j in results) else "🔴"
final_msg += " Daily Status\n"
for passed, message in results:
final_msg += message
return final_msg
async def run_local(jobs: list[str]) -> str:
async with aiohttp.ClientSession() as session:
msg = await main(session, jobs)
print(msg)
def add_jobs_arg(parser):
parser.add_argument(
"-j", "--jobs",
metavar="JOB_NAME", default=["tf-a-daily"], nargs="+",
help="CI jobs to monitor"
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Latest CI run status",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
add_jobs_arg(parser)
args = parser.parse_args(sys.argv[1:])
asyncio.run(run_local(args.jobs))