mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
Adds six more rule families to the strict ruleset: - ``T10`` — committed ``breakpoint()`` / ``pdb`` calls - ``PGH`` — pygrep-hooks (blanket ``# type: ignore`` without code) - ``EXE`` — shebang/executable-bit consistency - ``RSE`` — ``raise X()`` style cleanups - ``ICN`` — standard import aliases - ``DTZ`` — tz-naive ``datetime`` bugs Fixes (99 violations cleared) ============================= EXE001 (68): every file with a Python shebang now has the executable bit set. Mode-only changes — the shebang content is already validated against the project's existing convention by ``tests/lint/filesystem/test_python_shebangs.py`` (``#\!/usr/bin/env python3`` for portable scripts, ``#\!/usr/bin/python`` for Ansible custom modules in ``library/``). PGH003 (17): removed blanket ``# type: ignore`` markers. The project doesn't run mypy/pyright in CI, so these were decoration only. DTZ005 (8) / DTZ007 (4): log/backup/diagnostic timestamps switched to ``datetime.now(tz=timezone.utc)`` so cross-host log correlation doesn't depend on each host's local TZ. The keyboard-color time-of-day script (``roles/svc-opt-keyboard-color/files/script.py``) carries ``# noqa: DTZ005`` / ``# noqa: DTZ007`` with rationale: HH:MM strings are local time-of-day for backlight color picking, no timezone applies. The OpenSSL date parser in ``utils/cert_utils.py`` anchors the parsed naive datetime to UTC explicitly via ``.replace(tzinfo=UTC)`` (openssl always emits the literal "GMT"). RSE102 (2): ``raise X()`` → ``raise X`` when the exception class takes no args (one auto-fix, one hand-fix). Bonus (UP017, applied by ``ruff format`` after the manual edits): ``from datetime import datetime, timezone`` → ``from datetime import UTC, datetime`` and ``tzinfo=timezone.utc`` → ``tzinfo=UTC`` (Python 3.11+ shorthand). Verification ============ - ``ruff check .`` → All checks passed - ``ruff format --check .`` → 894 files formatted - 1621 unit tests → all pass - Integration suite: 44 baseline failures, 1 skipped (unchanged) 86 files changed, +55 / −37 content lines, plus 68 mode-only changes (``chmod +x``). Combined with the prior strict-rule commits the branch satisfies 17 ruff rule families end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
119 lines
3.5 KiB
Python
Executable File
119 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Parse GHA annotation lines from a log file and write a Markdown summary
|
|
to $GITHUB_STEP_SUMMARY.
|
|
|
|
Usage:
|
|
python3 -m utils.annotations.summarize <logfile> [<title>]
|
|
|
|
Annotation format emitted by utils.annotations.message:
|
|
::warning title=<t>,file=<f>,line=<l>,col=<c>::<message>
|
|
::error title=<t>,...::<message>
|
|
::notice title=<t>,...::<message>
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
ANNOTATION_RE = re.compile(r"^::(warning|error|notice)(?:\s([^:]*))?::(.*)$")
|
|
PROP_RE = re.compile(r"(\w+)=([^,]*)")
|
|
|
|
ICONS = {"error": "🔴", "warning": "🟡", "notice": "🔵"}
|
|
HEADINGS = {"error": "Errors", "warning": "Warnings", "notice": "Notices"}
|
|
|
|
|
|
@dataclass
|
|
class Annotation:
|
|
level: str
|
|
message: str
|
|
title: str | None = None
|
|
file: str | None = None
|
|
line: int | None = None
|
|
col: int | None = None
|
|
|
|
|
|
def parse_props(props_str: str) -> dict[str, str]:
|
|
return {m.group(1): m.group(2) for m in PROP_RE.finditer(props_str or "")}
|
|
|
|
|
|
def parse_log(path: Path) -> list[Annotation]:
|
|
annotations: list[Annotation] = []
|
|
for raw in path.read_text(errors="replace").splitlines():
|
|
m = ANNOTATION_RE.match(raw.strip())
|
|
if not m:
|
|
continue
|
|
level, props_str, message = m.group(1), m.group(2) or "", m.group(3)
|
|
props = parse_props(props_str)
|
|
annotations.append(
|
|
Annotation(
|
|
level=level,
|
|
message=message,
|
|
title=props.get("title"),
|
|
file=props.get("file"),
|
|
line=int(props["line"]) if "line" in props else None,
|
|
col=int(props["col"]) if "col" in props else None,
|
|
)
|
|
)
|
|
return annotations
|
|
|
|
|
|
def render_markdown(annotations: list[Annotation], title: str) -> str:
|
|
if not annotations:
|
|
return f"## {title}\n\n✅ No annotations found.\n"
|
|
|
|
by_level: dict[str, list[Annotation]] = defaultdict(list)
|
|
for a in annotations:
|
|
by_level[a.level].append(a)
|
|
|
|
lines = [f"## {title}\n"]
|
|
for level in ("error", "warning", "notice"):
|
|
items = by_level.get(level, [])
|
|
if not items:
|
|
continue
|
|
icon = ICONS[level]
|
|
heading = HEADINGS[level]
|
|
lines.append(f"### {icon} {heading} ({len(items)})\n")
|
|
lines.append("| Title | File | Message |")
|
|
lines.append("|-------|------|---------|")
|
|
for a in items:
|
|
t = a.title or ""
|
|
loc = a.file or ""
|
|
if loc and a.line:
|
|
loc = f"{loc}:{a.line}"
|
|
msg = a.message.replace("|", "\\|")
|
|
lines.append(f"| {t} | {loc} | {msg} |")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
print("Usage: summarize <logfile> [<title>]", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
log_path = Path(sys.argv[1])
|
|
title = sys.argv[2] if len(sys.argv) > 2 else "Annotation Summary"
|
|
|
|
if not log_path.exists():
|
|
print(f"Log file not found, skipping summary: {log_path}", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
annotations = parse_log(log_path)
|
|
markdown = render_markdown(annotations, title)
|
|
|
|
summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if summary_file:
|
|
with open(summary_file, "a") as f:
|
|
f.write(markdown + "\n")
|
|
else:
|
|
print(markdown)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|