ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
ci_status.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""The one reader of the ci-monitor status file.
5
6Split out of ``scripts/ci/monitor.sh`` (its ``_status_read`` shell function): the
7reader had outgrown what a shell heredoc should carry, and holding the verdict
8logic in a real module makes it lintable and directly testable. ``monitor.sh``
9invokes it as ``python3 ci_status.py <state-file> <mode> [arg]`` and every field
10and rendered view the tool prints comes from here -- one reader, one place.
11
12The verdict rules encode two hard-won distinctions (see the matching notes in
13monitor.sh):
14
15* SKIPPED IS NOT SUCCESS -- an all-skipped sha ran no gate, so it is UNKNOWN,
16 never PASS (#530).
17* CANCELLED IS NOT FAILURE -- a superseded run is a non-result, not a red; a
18 workflow is judged by its latest run that actually concluded (#561).
19
20The daemon never calls this module (it computes its head verdict inline while
21polling), so the stand-alone copy install-service deploys does not need it.
22"""
23
24from __future__ import annotations
25
26import json
27import sys
28from pathlib import Path
29
30Run = dict[str, object]
31
32FAIL_CONC = {"failure", "timed_out"}
33HEAD_ROWS = 6
34SHA_ABBREV = 9
35
36
37def matching(runs: list[Run], sha: str) -> list[Run]:
38 """Return the runs whose sha starts with `sha`."""
39 return [r for r in runs if str(r.get("sha") or "").startswith(sha)]
40
41
42def render(run: Run, with_sha: bool = False) -> str:
43 """Render one run as an indented ``name: status/conclusion`` line."""
44 tail = " " + str(run.get("sha") or "")[:SHA_ABBREV] if with_sha else ""
45 return f" {run.get('name')}: {run.get('status')}/{run.get('conclusion') or '-'}{tail}"
46
47
48def _workflow_verdict(wf_runs: list[Run]) -> str:
49 """Verdict for one workflow's runs of a sha: PASS / FAIL / RUNNING / NORESULT.
50
51 The latest run that actually concluded success or failure decides it, so a
52 re-run that succeeded clears an earlier failure and a superseded ``cancelled``
53 (or a ``skipped``) run never overrides a real conclusion. Only cancelled /
54 skipped runs are a NORESULT; a run still in flight keeps it RUNNING.
55 """
56 decisive = sorted(
57 (
58 r
59 for r in wf_runs
60 if r.get("conclusion") == "success" or r.get("conclusion") in FAIL_CONC
61 ),
62 key=lambda r: str(r.get("created") or ""),
63 )
64 if decisive:
65 return "PASS" if decisive[-1].get("conclusion") == "success" else "FAIL"
66 if any(r.get("status") != "completed" for r in wf_runs):
67 return "RUNNING"
68 return "NORESULT"
69
70
71def verdict(runs: list[Run], sha: str) -> str:
72 """Aggregate a sha's per-workflow verdicts into PASS / FAIL / UNKNOWN."""
73 got = matching(runs, sha)
74 if not got:
75 return "UNKNOWN"
76 by_wf: dict[object, list[Run]] = {}
77 for r in got:
78 by_wf.setdefault(r.get("name"), []).append(r)
79 wf_verdicts = [_workflow_verdict(wf_runs) for wf_runs in by_wf.values()]
80 if "FAIL" in wf_verdicts:
81 return "FAIL"
82 if "RUNNING" in wf_verdicts:
83 return "UNKNOWN"
84 if "PASS" in wf_verdicts:
85 return "PASS"
86 # every workflow was cancelled/skipped -- nothing actually ran
87 return "UNKNOWN"
88
89
90def _conclusion_count(runs: list[Run], sha: str, conclusion: str) -> int:
91 """Count the sha's runs whose conclusion equals `conclusion`."""
92 return sum(1 for r in matching(runs, sha) if r.get("conclusion") == conclusion)
93
94
95def main(argv: list[str]) -> int:
96 """Dispatch one read mode against the status file named in argv[1]."""
97 path, mode, *rest = argv[1:]
98 arg = rest[0] if rest else ""
99 with Path(path).open(encoding="utf-8") as fh:
100 doc = json.load(fh)
101 runs = doc.get("runs") or []
102
103 if mode == "field":
104 print(doc.get(arg) or "")
105 elif mode == "count":
106 print(len(matching(runs, arg)))
107 elif mode == "verdict":
108 print(verdict(runs, arg))
109 elif mode == "skipped-count":
110 print(_conclusion_count(runs, arg, "skipped"))
111 elif mode == "cancelled-count":
112 print(_conclusion_count(runs, arg, "cancelled"))
113 elif mode == "lines-sha":
114 for r in matching(runs, arg):
115 print(render(r))
116 elif mode == "lines-head":
117 for r in runs[:HEAD_ROWS]:
118 print(render(r, with_sha=True))
119 else:
120 sys.exit("unknown mode: " + mode)
121 return 0
122
123
124if __name__ == "__main__":
125 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298