feat: publish weekly README audit report

This commit is contained in:
Wilson Freitas
2026-09-02 09:07:53 -03:00
parent 644832fd35
commit 7986ffb296
2 changed files with 432 additions and 1 deletions
+159 -1
View File
@@ -2,12 +2,14 @@
from __future__ import annotations
import re
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime, timezone
from enum import StrEnum
from itertools import islice
from pathlib import Path
from typing import Any, Callable
from typing import Any, Callable, Iterable
from urllib.parse import urlsplit
from github import GithubException
@@ -19,6 +21,9 @@ from scripts.readme_entries import (
)
from scripts.url_probe import Outcome, UrlObservation, probe_url
TRACKING_TITLE = "Weekly README audit report"
TRACKING_MARKER = "<!-- awesome-quant-readme-audit -->"
class FindingKind(StrEnum):
DEAD = "dead"
@@ -35,6 +40,31 @@ class FindingKind(StrEnum):
FINDING_ORDER = {kind: index for index, kind in enumerate(FindingKind)}
_REPORT_SECTIONS = (
("Confirmed dead links", (FindingKind.DEAD,)),
("Repeated transient failures", (FindingKind.TRANSIENT,)),
("Access-restricted links", (FindingKind.RESTRICTED,)),
("Other HTTP failures", (FindingKind.HTTP_ERROR,)),
("Permanent redirects", (FindingKind.PERMANENT_REDIRECT,)),
(
"GitHub repository findings",
(
FindingKind.GITHUB_MOVED,
FindingKind.GITHUB_ARCHIVED,
FindingKind.GITHUB_DISABLED,
FindingKind.GITHUB_PARENT,
),
),
("Candidate replacements", (FindingKind.CANDIDATES,)),
)
_REPORT_SECTION_ORDER = {
kind: index
for index, (_heading, kinds) in enumerate(_REPORT_SECTIONS)
for kind in kinds
}
_MARKDOWN_CONTROLS = frozenset(r"\`*_[\]{}()#+-.!|<>")
_URL_RE = re.compile(r"(?<!<)https?://[^\s<>]+")
@dataclass(frozen=True)
class AuditTarget:
@@ -62,6 +92,134 @@ class Finding:
candidates: tuple[Candidate, ...] = ()
def _candidate_sort_key(candidate: Candidate) -> tuple[Any, ...]:
return (
-candidate.stars,
candidate.full_name.casefold(),
candidate.full_name,
candidate.url,
)
def _sorted_candidates(finding: Finding) -> tuple[Candidate, ...]:
return tuple(sorted(finding.candidates, key=_candidate_sort_key)[:3])
def _report_sort_key(finding: Finding) -> tuple[Any, ...]:
return (
_REPORT_SECTION_ORDER[finding.kind],
finding.section,
finding.entry_name,
finding.line_number,
finding.url,
finding.evidence,
finding.suggestion,
tuple(_candidate_sort_key(candidate) for candidate in _sorted_candidates(finding)),
)
def _escape_markdown(text: str) -> str:
text = " ".join(text.splitlines())
return "".join(
f"\\{character}" if character in _MARKDOWN_CONTROLS else character
for character in text
)
def _render_text(text: str) -> str:
text = " ".join(text.split())
def angle_link(match: re.Match[str]) -> str:
value = match.group(0)
url = value.rstrip(".,;:!?")
return f"<{url}>{value[len(url):]}"
return _URL_RE.sub(angle_link, text)
def render_report(findings: Iterable[Finding], *, checked_at: datetime) -> str:
"""Render findings as stable Markdown suitable for the tracking issue body."""
ordered_findings = sorted(findings, key=_report_sort_key)
checked_at_utc = checked_at.astimezone(timezone.utc)
lines = [
TRACKING_MARKER,
f"# {TRACKING_TITLE}",
"",
f"Checked: {checked_at_utc:%Y-%m-%d %H:%M} UTC",
"Entries are reported for manual review; this automation did not modify README.md.",
"",
]
if not ordered_findings:
return "\n".join((*lines, "No findings.", ""))
findings_by_kind: dict[FindingKind, list[Finding]] = {
kind: [] for kind in FindingKind
}
for finding in ordered_findings:
findings_by_kind[finding.kind].append(finding)
for heading, kinds in _REPORT_SECTIONS:
section_findings = [
finding for kind in kinds for finding in findings_by_kind[kind]
]
if not section_findings:
continue
lines.extend((f"## {heading}", ""))
for finding in section_findings:
lines.append(
f"- **Entry:** {_escape_markdown(finding.entry_name)}; "
f"**README line:** {finding.line_number}; "
f"**Section:** {_escape_markdown(finding.section)}; "
f"**Checked URL:** <{finding.url}>; "
f"**Evidence:** {_render_text(finding.evidence)}; "
f"**Manual suggestion:** {_render_text(finding.suggestion)}"
)
for index, candidate in enumerate(_sorted_candidates(finding), start=1):
lines.append(
f" - **Candidate {index}:** {candidate.full_name} — "
f"<{candidate.url}> — {candidate.stars} stars"
)
lines.append("")
return "\n".join(lines)
def sync_tracking_issue(
repository: Any,
findings: Iterable[Finding],
body: str,
) -> str:
"""Create or transition the single marked README audit tracking issue."""
has_findings = bool(tuple(findings))
matches = [
issue
for issue in repository.get_issues(state="all")
if issue.title == TRACKING_TITLE and TRACKING_MARKER in (issue.body or "")
]
if len(matches) > 1:
raise RuntimeError("multiple tracking issues found")
if not matches:
if not has_findings:
return "clean"
repository.create_issue(title=TRACKING_TITLE, body=body)
return "created"
issue = matches[0]
desired_state = "open" if has_findings else "closed"
changes = {}
if issue.body != body:
changes["body"] = body
if issue.state != desired_state:
changes["state"] = desired_state
if not changes:
return "unchanged"
state_changed = "state" in changes
issue.edit(**changes)
if state_changed:
return "reopened" if has_findings else "closed"
return "updated"
def collect_targets(readme_path: str | Path) -> list[AuditTarget]:
"""Collect every entry URL in README source order with its entry context."""
return [
+273
View File
@@ -2,6 +2,7 @@ import tempfile
import threading
import time
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
@@ -11,9 +12,14 @@ from github import GithubException
from scripts.audit_readme import (
AuditTarget,
Candidate,
Finding,
FindingKind,
TRACKING_MARKER,
TRACKING_TITLE,
audit_readme,
collect_targets,
render_report,
sync_tracking_issue,
)
from scripts.readme_entries import ReadmeEntry
from scripts.url_probe import Outcome, UrlObservation
@@ -84,7 +90,274 @@ class FakeGithubClient:
return iter(self.searches.get(kwargs["query"], ()))
class FakeIssue:
def __init__(self, title, body, state):
self.title = title
self.body = body
self.state = state
self.edits = []
def edit(self, **kwargs):
self.edits.append(kwargs)
if "body" in kwargs:
self.body = kwargs["body"]
if "state" in kwargs:
self.state = kwargs["state"]
class FakeTrackingRepository:
def __init__(self, issues=()):
self.issues = list(issues)
self.get_issues_calls = []
self.create_issue_calls = []
def get_issues(self, **kwargs):
self.get_issues_calls.append(kwargs)
return iter(self.issues)
def create_issue(self, **kwargs):
self.create_issue_calls.append(kwargs)
issue = FakeIssue(kwargs["title"], kwargs["body"], "open")
self.issues.append(issue)
return issue
class AuditReadmeTests(unittest.TestCase):
def test_render_report_is_a_complete_deterministic_markdown_document(self):
findings = [
Finding(
FindingKind.CANDIDATES,
"Zeta [Section]",
"Replacement *Tool*",
80,
"https://dead.example/tool",
"The original primary URL returned confirmed HTTP 404.",
"Manual verification of these candidate repositories is required.",
(
Candidate("zeta/tool", "https://github.com/zeta/tool", 2),
Candidate("beta/tool", "https://github.com/beta/tool", 9),
Candidate("alpha/tool", "https://github.com/alpha/tool", 9),
),
),
Finding(
FindingKind.PERMANENT_REDIRECT,
"Redirects",
"Moved",
50,
"https://old.example/path",
"The link permanently redirects to https://new.example/path.",
"Update the link to https://new.example/path.",
),
Finding(
FindingKind.HTTP_ERROR,
"HTTP",
"Broken",
40,
"https://error.example",
"HTTP probe failed; last error unsafe URL.",
"Perform manual diagnosis before changing the link.",
),
Finding(
FindingKind.DEAD,
"Alpha & Beta",
"A_B [tool]",
10,
"https://dead.example/a",
"Confirmed HTTP 404 response.",
"Manually verify, then remove or replace this link.",
),
Finding(
FindingKind.GITHUB_ARCHIVED,
"Repositories",
"Archive",
60,
"https://github.com/owner/archive",
"GitHub marks the repository as archived.",
"Manually verify whether to replace it or move it to the historical section.",
),
Finding(
FindingKind.RESTRICTED,
"Access",
"Restricted",
30,
"https://restricted.example",
"Automated access was restricted by HTTP 403.",
"Perform manual browser verification before changing the link.",
),
Finding(
FindingKind.TRANSIENT,
"Transient",
"Retry",
20,
"https://retry.example",
"Transient failure after 3 attempts; last status 503.",
"Retry this link on the next audit run.",
),
Finding(
FindingKind.DEAD,
"Alpha & Beta",
"Zed",
11,
"https://dead.example/z",
"Confirmed HTTP 410 response.",
"Manually verify, then remove or replace this link.",
),
]
checked_at = datetime(
2026, 9, 2, 9, 45, tzinfo=timezone(timedelta(hours=-3))
)
expected = r"""<!-- awesome-quant-readme-audit -->
# Weekly README audit report
Checked: 2026-09-02 12:45 UTC
Entries are reported for manual review; this automation did not modify README.md.
## Confirmed dead links
- **Entry:** A\_B \[tool\]; **README line:** 10; **Section:** Alpha & Beta; **Checked URL:** <https://dead.example/a>; **Evidence:** Confirmed HTTP 404 response.; **Manual suggestion:** Manually verify, then remove or replace this link.
- **Entry:** Zed; **README line:** 11; **Section:** Alpha & Beta; **Checked URL:** <https://dead.example/z>; **Evidence:** Confirmed HTTP 410 response.; **Manual suggestion:** Manually verify, then remove or replace this link.
## Repeated transient failures
- **Entry:** Retry; **README line:** 20; **Section:** Transient; **Checked URL:** <https://retry.example>; **Evidence:** Transient failure after 3 attempts; last status 503.; **Manual suggestion:** Retry this link on the next audit run.
## Access-restricted links
- **Entry:** Restricted; **README line:** 30; **Section:** Access; **Checked URL:** <https://restricted.example>; **Evidence:** Automated access was restricted by HTTP 403.; **Manual suggestion:** Perform manual browser verification before changing the link.
## Other HTTP failures
- **Entry:** Broken; **README line:** 40; **Section:** HTTP; **Checked URL:** <https://error.example>; **Evidence:** HTTP probe failed; last error unsafe URL.; **Manual suggestion:** Perform manual diagnosis before changing the link.
## Permanent redirects
- **Entry:** Moved; **README line:** 50; **Section:** Redirects; **Checked URL:** <https://old.example/path>; **Evidence:** The link permanently redirects to <https://new.example/path>.; **Manual suggestion:** Update the link to <https://new.example/path>.
## GitHub repository findings
- **Entry:** Archive; **README line:** 60; **Section:** Repositories; **Checked URL:** <https://github.com/owner/archive>; **Evidence:** GitHub marks the repository as archived.; **Manual suggestion:** Manually verify whether to replace it or move it to the historical section.
## Candidate replacements
- **Entry:** Replacement \*Tool\*; **README line:** 80; **Section:** Zeta \[Section\]; **Checked URL:** <https://dead.example/tool>; **Evidence:** The original primary URL returned confirmed HTTP 404.; **Manual suggestion:** Manual verification of these candidate repositories is required.
- **Candidate 1:** alpha/tool — <https://github.com/alpha/tool> — 9 stars
- **Candidate 2:** beta/tool — <https://github.com/beta/tool> — 9 stars
- **Candidate 3:** zeta/tool — <https://github.com/zeta/tool> — 2 stars
"""
self.assertEqual(render_report(findings, checked_at=checked_at), expected)
self.assertEqual(
render_report(reversed(findings), checked_at=checked_at),
expected,
)
def test_render_report_clean_body_has_no_finding_headings(self):
report = render_report(
[],
checked_at=datetime(2026, 9, 2, 12, 45, tzinfo=timezone.utc),
)
self.assertEqual(
report,
"""<!-- awesome-quant-readme-audit -->
# Weekly README audit report
Checked: 2026-09-02 12:45 UTC
Entries are reported for manual review; this automation did not modify README.md.
No findings.
""",
)
def test_sync_tracking_issue_follows_the_full_lifecycle_matrix(self):
findings = [SimpleNamespace()]
new_body = f"{TRACKING_MARKER}\nnew report"
old_body = f"{TRACKING_MARKER}\nold report"
cases = (
("none with findings", (), findings, "created", None),
(
"open with findings",
(FakeIssue(TRACKING_TITLE, old_body, "open"),),
findings,
"updated",
{"body": new_body},
),
(
"closed with findings",
(FakeIssue(TRACKING_TITLE, old_body, "closed"),),
findings,
"reopened",
{"body": new_body, "state": "open"},
),
(
"open clean",
(FakeIssue(TRACKING_TITLE, old_body, "open"),),
[],
"closed",
{"body": new_body, "state": "closed"},
),
(
"closed clean",
(FakeIssue(TRACKING_TITLE, old_body, "closed"),),
[],
"updated",
{"body": new_body},
),
("none clean", (), [], "clean", None),
)
for name, issues, case_findings, expected, edit in cases:
with self.subTest(name=name):
repository = FakeTrackingRepository(issues)
result = sync_tracking_issue(repository, case_findings, new_body)
self.assertEqual(result, expected)
self.assertEqual(repository.get_issues_calls, [{"state": "all"}])
if issues:
self.assertEqual(issues[0].edits, [edit] if edit else [])
self.assertEqual(repository.create_issue_calls, [])
elif case_findings:
self.assertEqual(
repository.create_issue_calls,
[{"title": TRACKING_TITLE, "body": new_body}],
)
else:
self.assertEqual(repository.create_issue_calls, [])
def test_sync_tracking_issue_does_nothing_when_body_and_state_are_current(self):
body = f"{TRACKING_MARKER}\ncurrent report"
for findings, state in (([SimpleNamespace()], "open"), ([], "closed")):
with self.subTest(state=state):
issue = FakeIssue(TRACKING_TITLE, body, state)
repository = FakeTrackingRepository((issue,))
result = sync_tracking_issue(repository, findings, body)
self.assertEqual(result, "unchanged")
self.assertEqual(issue.edits, [])
self.assertEqual(repository.create_issue_calls, [])
def test_sync_tracking_issue_ignores_unmarked_issues_and_rejects_duplicates(self):
body = f"{TRACKING_MARKER}\nreport"
unmarked = FakeIssue(TRACKING_TITLE, "human issue", "open")
wrong_title = FakeIssue("Another report", body, "open")
repository = FakeTrackingRepository((unmarked, wrong_title))
self.assertEqual(
sync_tracking_issue(repository, [SimpleNamespace()], body),
"created",
)
duplicate_repository = FakeTrackingRepository(
(
FakeIssue(TRACKING_TITLE, body, "open"),
FakeIssue(TRACKING_TITLE, f"prefix\n{body}", "closed"),
)
)
with self.assertRaisesRegex(RuntimeError, "multiple tracking issues"):
sync_tracking_issue(duplicate_repository, [], body)
def test_collect_targets_uses_one_parser_pass_and_preserves_entry_and_url_order(self):
first = entry(
3,