Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | import re |
| 4 | from dataclasses import dataclass |
| 5 | |
| 6 | import requests |
| 7 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 8 | # Constants to produce the report with |
Boyan Karatotev | 12cd590 | 2025-06-25 09:22:46 +0100 | [diff] [blame] | 9 | openci_url = "https://ci.trustedfirmware.org/" |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 10 | job_names = ["tf-a-daily", "tf-a-tftf-main"] |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 11 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 12 | # Jenkins API helpers |
| 13 | def get_job_url(job_name: str) -> str: |
| 14 | return openci_url + f"job/{job_name}/api/json" |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 15 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 16 | def get_build_url(job_name: str, build_number: str) -> str: |
| 17 | return openci_url + f"job/{job_name}/{build_number}" |
| 18 | |
| 19 | def get_build_api(build_url: str) -> str: |
| 20 | return build_url + "/api/json" |
| 21 | |
| 22 | def get_build_console(build_url: str) -> str: |
| 23 | return build_url + "/consoleText" |
| 24 | |
| 25 | """Finds the latest run of a given job by name""" |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 26 | class Job: |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 27 | def __init__(self, job_name: str) -> None: |
| 28 | req = requests.get(get_job_url(job_name)).json() |
| 29 | name = req["displayName"] |
| 30 | number = req["lastCompletedBuild"]["number"] |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 31 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 32 | self.build = Build(name, name, number, level=0) |
| 33 | self.passed = self.build.passed |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 34 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 35 | def print_build_status(self): |
| 36 | self.build.print_build_status() |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 37 | |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 38 | """Represents an individual build. Will recursively fetch sub builds""" |
| 39 | class Build: |
| 40 | def __init__(self, job_name: str, pretty_job_name: str, build_number: str, level: int) -> None: |
| 41 | self.url = get_build_url(job_name, build_number) |
| 42 | req = requests.get(get_build_api(self.url)).json() |
| 43 | self.passed = req["result"].lower() == "success" |
| 44 | |
| 45 | self.name = pretty_job_name |
| 46 | # The full display name is "{job_name} {build_number}" |
| 47 | if self.name == "": |
| 48 | self.name = req["fullDisplayName"].split(" ")[0] |
| 49 | # and builds should show up with their configuration name |
| 50 | elif self.name == "tf-a-builder": |
| 51 | self.name = req["actions"][0]["parameters"][1]["value"] |
| 52 | |
| 53 | self.level = level |
| 54 | self.number = build_number |
| 55 | self.sub_builds = [] |
| 56 | |
| 57 | # parent job passed => children passed. Skip |
| 58 | if not self.passed: |
| 59 | # the main jobs list sub builds nicely |
| 60 | self.sub_builds = [ |
| 61 | # the gateways get an alias to differentiate them |
| 62 | Build(build["jobName"], build["jobAlias"], build["buildNumber"], level + 1) |
| 63 | for build in req.get("subBuilds", []) |
| 64 | ] |
| 65 | # gateways don't, since they determine them dynamically |
| 66 | if self.sub_builds == []: |
| 67 | self.sub_builds = [ |
| 68 | Build(name, name, num, level + 1) |
| 69 | for name, num in self.get_builds_from_console_log() |
| 70 | ] |
| 71 | |
| 72 | # extracts (child_name, child_number) from the console output of a build |
| 73 | def get_builds_from_console_log(self) -> str: |
| 74 | log = requests.get(get_build_console(self.url)).text |
| 75 | |
| 76 | return re.findall(r"(tf-a[-\w+]+) #(\d+) started", log) |
| 77 | |
| 78 | def print_build_status(self): |
| 79 | print(self) |
| 80 | |
| 81 | for build in self.sub_builds: |
| 82 | if not build.passed: |
| 83 | build.print_build_status() |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 84 | |
| 85 | def __str__(self) -> str: |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 86 | return (f"{' ' * self.level * 4}* {'✅' if self.passed else '❌'} " |
| 87 | f"*{self.name}* [#{self.number}]({self.url})" |
| 88 | ) |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 89 | |
| 90 | def main(): |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 91 | jobs = [Job(name) for name in job_names] |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 92 | |
| 93 | print("🟢" if all(j.passed for j in jobs) else "🔴", "Daily Status") |
| 94 | |
| 95 | for j in jobs: |
Boyan Karatotev | 1c5e5c9 | 2025-06-27 11:46:41 +0100 | [diff] [blame^] | 96 | j.print_build_status() |
Harrison Mutai | 0e9964e | 2024-06-14 09:51:42 +0100 | [diff] [blame] | 97 | |
| 98 | if __name__ == "__main__": |
| 99 | main() |