4"""The one reader of the ci-monitor status file.
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.
12The verdict rules encode two hard-won distinctions (see the matching notes in
15* SKIPPED IS NOT SUCCESS -- an all-skipped sha ran no gate, so it is UNKNOWN,
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).
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.
24from __future__
import annotations
28from pathlib
import Path
30Run = dict[str, object]
32FAIL_CONC = {
"failure",
"timed_out"}
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)]
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}"
48def _workflow_verdict(wf_runs: list[Run]) -> str:
49 """Verdict for one workflow's runs of a sha: PASS / FAIL / RUNNING / NORESULT.
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.
60 if r.get(
"conclusion") ==
"success" or r.get(
"conclusion")
in FAIL_CONC
62 key=
lambda r: str(r.get(
"created")
or ""),
65 return "PASS" if decisive[-1].get(
"conclusion") ==
"success" else "FAIL"
66 if any(r.get(
"status") !=
"completed" for r
in wf_runs):
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)
76 by_wf: dict[object, list[Run]] = {}
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:
82 if "RUNNING" in wf_verdicts:
84 if "PASS" in wf_verdicts:
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)
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:
101 runs = doc.get(
"runs")
or []
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):
116 elif mode ==
"lines-head":
117 for r
in runs[:HEAD_ROWS]:
118 print(render(r, with_sha=
True))
120 sys.exit(
"unknown mode: " + mode)
124if __name__ ==
"__main__":
125 sys.exit(
main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.