Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions machine/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,23 @@ def render(findings: list[Finding], config: dict[str, Any]) -> str:
return "\n".join(lines) + "\n"


def publish(body: str, token: str, repo: str) -> None:
def fingerprint(findings: list[Finding]) -> str:
"""Stable identity of a problem set: what is broken, not when it was checked."""
return ",".join(sorted(f"{f.where}={f.what}" for f in findings))


def alert_needed(previous_body: str, current: str) -> bool:
"""Alert only when the set of problems changed — and never for all clear.

A daily ping about the same known failure trains people to ignore the
ping; the point is to hear about the *new* one.
"""
if not current:
return False
return f"<!-- fingerprint:{current} -->" not in previous_body


def publish(body: str, token: str, repo: str, findings: list[Finding], mention: str) -> None:
"""Keep one open issue up to date instead of opening one per run."""
def call(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
data = json.dumps(payload).encode("utf-8") if payload is not None else None
Expand All @@ -138,12 +154,24 @@ def call(method: str, path: str, payload: dict[str, Any] | None = None) -> Any:
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8") or "null")

current = fingerprint(findings)
stamped = f"{body}\n<!-- fingerprint:{current} -->\n"

issues = call("GET", f"/repos/{repo}/issues?state=open&per_page=100")
existing = next((i for i in issues if i.get("title") == ISSUE_TITLE), None)
if existing:
call("PATCH", f"/repos/{repo}/issues/{existing['number']}", {"body": body})
number = existing["number"]
previous = existing.get("body") or ""
call("PATCH", f"/repos/{repo}/issues/{number}", {"body": stamped})
else:
call("POST", f"/repos/{repo}/issues", {"title": ISSUE_TITLE, "body": body})
number = call("POST", f"/repos/{repo}/issues", {"title": ISSUE_TITLE, "body": stamped})["number"]
previous = ""

# A comment, not an edit: editing an issue body notifies nobody.
if mention and alert_needed(previous, current):
lines = "\n".join(f"- {f.where}: **{f.what}**" for f in findings)
call("POST", f"/repos/{repo}/issues/{number}/comments",
{"body": f"@{mention} the org health report changed:\n\n{lines}"})


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

Expand Down
50 changes: 43 additions & 7 deletions machine/repos.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
{
"notify": "Seungpyo1007",
"repos": [
{"name": "GetTechAPI/TechAPI", "branches": ["develop", "main"]},
{"name": "GetTechAPI/TechEngine", "branches": ["main"]},
{"name": "GetTechAPI/game-catalog", "branches": ["develop", "main"]},
{"name": "GetTechAPI/cpu-engineering-samples", "branches": ["develop", "main"]}
{
"name": "GetTechAPI/TechAPI",
"branches": [
"develop",
"main"
]
},
{
"name": "GetTechAPI/TechEngine",
"branches": [
"main"
]
},
{
"name": "GetTechAPI/game-catalog",
"branches": [
"develop",
"main"
]
},
{
"name": "GetTechAPI/cpu-engineering-samples",
"branches": [
"develop",
"main"
]
}
],
"endpoints": [
{"name": "TechAPI manifest", "url": "https://gettechapi.github.io/TechAPI/v1/index.json", "expect": "collections"},
{"name": "game-catalog summary", "url": "https://gettechapi.github.io/game-catalog/summary.json", "expect": "count"},
{"name": "cpu-engineering-samples summary", "url": "https://gettechapi.github.io/cpu-engineering-samples/summary.json", "expect": "count"}
{
"name": "TechAPI manifest",
"url": "https://gettechapi.github.io/TechAPI/v1/index.json",
"expect": "collections"
},
{
"name": "game-catalog summary",
"url": "https://gettechapi.github.io/game-catalog/summary.json",
"expect": "count"
},
{
"name": "cpu-engineering-samples summary",
"url": "https://gettechapi.github.io/cpu-engineering-samples/summary.json",
"expect": "count"
}
]
}
33 changes: 33 additions & 0 deletions tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,36 @@ def test_render_all_clear_and_problem_table():
assert "all clear" in render([], config)
problems = render(run_findings("org/repo", "main", [_run("deploy", "cancelled")]), config)
assert "1 problem" in problems and "| cancelled |" in problems


# --- alerts -----------------------------------------------------------------

from machine.health import Finding, alert_needed, fingerprint # noqa: E402

BROKEN = [Finding("TechEngine@main · weekly-refresh", "cancelled")]


def test_new_problem_alerts():
assert alert_needed("", fingerprint(BROKEN))


def test_same_problem_twice_does_not_alert_again():
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
assert not alert_needed(body, fingerprint(BROKEN))


def test_a_different_problem_alerts():
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
worse = BROKEN + [Finding("TechAPI@main · deploy-pages", "failure")]
assert alert_needed(body, fingerprint(worse))


def test_all_clear_never_alerts():
body = f"report\n<!-- fingerprint:{fingerprint(BROKEN)} -->\n"
assert not alert_needed(body, fingerprint([]))


def test_fingerprint_ignores_order_and_links():
a = [Finding("x", "failure", "https://run/1"), Finding("y", "cancelled", "https://run/2")]
b = [Finding("y", "cancelled", "https://run/9"), Finding("x", "failure", "https://run/8")]
assert fingerprint(a) == fingerprint(b)
Loading