Skip to content

Commit aac7de4

Browse files
committed
feat(health): mention the maintainer when the problem set changes
Editing an issue body notifies nobody, so a report nobody opens is a report nobody reads. When the set of problems changes, post a comment that @-mentions the configured user. Only on change: the same known failure every morning trains people to ignore the ping. The fingerprint is where + result, not the run link, so a workflow failing again tomorrow does not count as new. All clear never pings.
1 parent fcc1db1 commit aac7de4

3 files changed

Lines changed: 109 additions & 11 deletions

File tree

‎machine/health.py‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,23 @@ def render(findings: list[Finding], config: dict[str, Any]) -> str:
126126
return "\n".join(lines) + "\n"
127127

128128

129-
def publish(body: str, token: str, repo: str) -> None:
129+
def fingerprint(findings: list[Finding]) -> str:
130+
"""Stable identity of a problem set: what is broken, not when it was checked."""
131+
return ",".join(sorted(f"{f.where}={f.what}" for f in findings))
132+
133+
134+
def alert_needed(previous_body: str, current: str) -> bool:
135+
"""Alert only when the set of problems changed — and never for all clear.
136+
137+
A daily ping about the same known failure trains people to ignore the
138+
ping; the point is to hear about the *new* one.
139+
"""
140+
if not current:
141+
return False
142+
return f"<!-- fingerprint:{current} -->" not in previous_body
143+
144+
145+
def publish(body: str, token: str, repo: str, findings: list[Finding], mention: str) -> None:
130146
"""Keep one open issue up to date instead of opening one per run."""
131147
def call(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
132148
data = json.dumps(payload).encode("utf-8") if payload is not None else None
@@ -138,12 +154,24 @@ def call(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
138154
with urllib.request.urlopen(request, timeout=30) as response:
139155
return json.loads(response.read().decode("utf-8") or "null")
140156

157+
current = fingerprint(findings)
158+
stamped = f"{body}\n<!-- fingerprint:{current} -->\n"
159+
141160
issues = call("GET", f"/repos/{repo}/issues?state=open&per_page=100")
142161
existing = next((i for i in issues if i.get("title") == ISSUE_TITLE), None)
143162
if existing:
144-
call("PATCH", f"/repos/{repo}/issues/{existing['number']}", {"body": body})
163+
number = existing["number"]
164+
previous = existing.get("body") or ""
165+
call("PATCH", f"/repos/{repo}/issues/{number}", {"body": stamped})
145166
else:
146-
call("POST", f"/repos/{repo}/issues", {"title": ISSUE_TITLE, "body": body})
167+
number = call("POST", f"/repos/{repo}/issues", {"title": ISSUE_TITLE, "body": stamped})["number"]
168+
previous = ""
169+
170+
# A comment, not an edit: editing an issue body notifies nobody.
171+
if mention and alert_needed(previous, current):
172+
lines = "\n".join(f"- {f.where}: **{f.what}**" for f in findings)
173+
call("POST", f"/repos/{repo}/issues/{number}/comments",
174+
{"body": f"@{mention} the org health report changed:\n\n{lines}"})
147175

148176

149177
def main() -> int:
@@ -161,7 +189,8 @@ def main() -> int:
161189
if summary:
162190
Path(summary).write_text(report, encoding="utf-8")
163191
if args.issue and token:
164-
publish(report, token, os.environ.get("GITHUB_REPOSITORY", "GetTechAPI/TechMachine"))
192+
publish(report, token, os.environ.get("GITHUB_REPOSITORY", "GetTechAPI/TechMachine"),
193+
findings, config.get("notify", ""))
165194
# The report is the output; a red run would only add a second alert.
166195
return 0
167196

‎machine/repos.json‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,49 @@
11
{
2+
"notify": "Seungpyo1007",
23
"repos": [
3-
{"name": "GetTechAPI/TechAPI", "branches": ["develop", "main"]},
4-
{"name": "GetTechAPI/TechEngine", "branches": ["main"]},
5-
{"name": "GetTechAPI/game-catalog", "branches": ["develop", "main"]},
6-
{"name": "GetTechAPI/cpu-engineering-samples", "branches": ["develop", "main"]}
4+
{
5+
"name": "GetTechAPI/TechAPI",
6+
"branches": [
7+
"develop",
8+
"main"
9+
]
10+
},
11+
{
12+
"name": "GetTechAPI/TechEngine",
13+
"branches": [
14+
"main"
15+
]
16+
},
17+
{
18+
"name": "GetTechAPI/game-catalog",
19+
"branches": [
20+
"develop",
21+
"main"
22+
]
23+
},
24+
{
25+
"name": "GetTechAPI/cpu-engineering-samples",
26+
"branches": [
27+
"develop",
28+
"main"
29+
]
30+
}
731
],
832
"endpoints": [
9-
{"name": "TechAPI manifest", "url": "https://gettechapi.github.io/TechAPI/v1/index.json", "expect": "collections"},
10-
{"name": "game-catalog summary", "url": "https://gettechapi.github.io/game-catalog/summary.json", "expect": "count"},
11-
{"name": "cpu-engineering-samples summary", "url": "https://gettechapi.github.io/cpu-engineering-samples/summary.json", "expect": "count"}
33+
{
34+
"name": "TechAPI manifest",
35+
"url": "https://gettechapi.github.io/TechAPI/v1/index.json",
36+
"expect": "collections"
37+
},
38+
{
39+
"name": "game-catalog summary",
40+
"url": "https://gettechapi.github.io/game-catalog/summary.json",
41+
"expect": "count"
42+
},
43+
{
44+
"name": "cpu-engineering-samples summary",
45+
"url": "https://gettechapi.github.io/cpu-engineering-samples/summary.json",
46+
"expect": "count"
47+
}
1248
]
1349
}

‎tests/test_health.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,36 @@ def test_render_all_clear_and_problem_table():
5151
assert "all clear" in render([], config)
5252
problems = render(run_findings("org/repo", "main", [_run("deploy", "cancelled")]), config)
5353
assert "1 problem" in problems and "| cancelled |" in problems
54+
55+
56+
# --- alerts -----------------------------------------------------------------
57+
58+
from machine.health import Finding, alert_needed, fingerprint # noqa: E402
59+
60+
BROKEN = [Finding("TechEngine@main · weekly-refresh", "cancelled")]
61+
62+
63+
def test_new_problem_alerts():
64+
assert alert_needed("", fingerprint(BROKEN))
65+
66+
67+
def test_same_problem_twice_does_not_alert_again():
68+
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
69+
assert not alert_needed(body, fingerprint(BROKEN))
70+
71+
72+
def test_a_different_problem_alerts():
73+
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
74+
worse = BROKEN + [Finding("TechAPI@main · deploy-pages", "failure")]
75+
assert alert_needed(body, fingerprint(worse))
76+
77+
78+
def test_all_clear_never_alerts():
79+
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
80+
assert not alert_needed(body, fingerprint([]))
81+
82+
83+
def test_fingerprint_ignores_order_and_links():
84+
a = [Finding("x", "failure", "https://run/1"), Finding("y", "cancelled", "https://run/2")]
85+
b = [Finding("y", "cancelled", "https://run/9"), Finding("x", "failure", "https://run/8")]
86+
assert fingerprint(a) == fingerprint(b)

0 commit comments

Comments
 (0)