Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

147 lines
5.1 KiB
Python
Raw Permalink Normal View History

"""Render the final PR comment from check results.
The comment is rendered fully up-front. Checks whose status is `NEEDS_AGENT`
get `{{CHECK_CELL:<package>:<kind>}}` and `{{CHECK_DETAIL:<package>:<kind>}}`
placeholders that the agent replaces before posting.
To add a new check kind: extend `CheckKind` and add an entry to `_CHECK_DISPLAY`
below. The agent prompt must also gain a matching instruction section, or the
agent will refuse to resolve the new kind.
"""
from .models import CheckKind, CheckRunResult, CheckStatus, PackageChange
MARKER = "<!-- requirements-check -->"
HEADER = "## Check requirements"
REPO_URL = "https://github.com/home-assistant/core"
COMMIT_PATH = "/commit/"
# Column / bullet labels per check kind, in display order.
_CHECK_DISPLAY: tuple[tuple[CheckKind, str], ...] = (
(CheckKind.VULNERABILITIES, "No Advisories"),
(CheckKind.YANKED, "Not Yanked"),
(CheckKind.REPO_PUBLIC, "Repo Public"),
(CheckKind.CI_UPLOAD, "CI Upload"),
(CheckKind.RELEASE_PIPELINE, "Release Pipeline"),
(CheckKind.SECURITY, "Security"),
(CheckKind.PR_LINK, "PR Link"),
(CheckKind.ASYNC_BLOCKING, "Async Safe"),
)
_ICONS: dict[CheckStatus, str] = {
CheckStatus.PASS: "✅",
CheckStatus.WARN: "⚠️",
CheckStatus.FAIL: "❌",
}
SKIPPED = "—"
SUMMARY_PASS = "All requirements checks passed. ✅"
SUMMARY_ATTENTION = "⚠️ Some checks require attention — see the details below."
# Emitted when at least one check still needs the agent. The agent resolves it
# to one of the two lines above once it has replaced the cell/detail
# placeholders, so the summary reflects the final verdicts rather than the
# deterministic-stage state.
SUMMARY_PLACEHOLDER = "{{SUMMARY}}"
def _placeholder(slot: str, pkg: PackageChange, kind: CheckKind) -> str:
"""Placeholder marker the agent replaces before posting."""
return f"{{{{{slot}:{pkg.name}:{kind.value}}}}}"
def _old_cell(pkg: PackageChange) -> str:
return pkg.old_version or SKIPPED
def _overall_status(pkg: PackageChange) -> CheckStatus | None:
"""Aggregate the per-package status across all checks."""
statuses = [c.status for c in pkg.checks.values()]
if CheckStatus.FAIL in statuses:
return CheckStatus.FAIL
if CheckStatus.WARN in statuses:
return CheckStatus.WARN
if CheckStatus.NEEDS_AGENT in statuses:
return None
return CheckStatus.PASS
def _summary_line(packages: list[PackageChange]) -> str:
statuses = [_overall_status(p) for p in packages]
if None in statuses:
return SUMMARY_PLACEHOLDER
if all(status == CheckStatus.PASS for status in statuses):
return SUMMARY_PASS
return SUMMARY_ATTENTION
def _cell(pkg: PackageChange, kind: CheckKind) -> str:
result = pkg.checks.get(kind)
if result is None:
return SKIPPED
if result.status == CheckStatus.NEEDS_AGENT:
return _placeholder("CHECK_CELL", pkg, kind)
return _ICONS.get(result.status, SKIPPED)
def _table(packages: list[PackageChange]) -> str:
labels = [label for _, label in _CHECK_DISPLAY]
rows = [
"| Package | Old | New | " + " | ".join(labels) + " |",
"|" + "|".join("---" for _ in range(3 + len(labels))) + "|",
]
for pkg in packages:
cells = [_cell(pkg, kind) for kind, _ in _CHECK_DISPLAY]
rows.append(
"| "
+ " | ".join([pkg.name, _old_cell(pkg), pkg.new_version, *cells])
+ " |"
)
return "\n".join(rows)
def _bullet(pkg: PackageChange, kind: CheckKind, label: str) -> str:
result = pkg.checks.get(kind)
if result is None:
return f"- **{label}**: {SKIPPED} skipped."
if result.status == CheckStatus.NEEDS_AGENT:
return f"- **{label}**: {_placeholder('CHECK_DETAIL', pkg, kind)}"
return f"- **{label}**: {_ICONS[result.status]} {result.details}"
def _details_block(pkg: PackageChange) -> str:
overall = _overall_status(pkg)
is_open = overall != CheckStatus.PASS
version = (
f"{pkg.old_version}{pkg.new_version}" if pkg.old_version else pkg.new_version
)
summary = f"<summary><strong>📦 {pkg.name}: {version}</strong></summary>"
open_attr = " open" if is_open else ""
body_lines = [_bullet(pkg, kind, label) for kind, label in _CHECK_DISPLAY]
return (
f"<details{open_attr}>\n{summary}\n\n"
+ "\n".join(body_lines)
+ "\n\n</details>"
)
def _intro(result: CheckRunResult) -> str:
"""Marker, header, and the optional commit line the gate reads back."""
parts: list[str] = []
if result.head_sha:
commit = f"[`{result.head_sha[:7]}`]({REPO_URL}{COMMIT_PATH}{result.head_sha})"
parts.append(f"Checked at commit {commit}.")
return "\n\n".join([f"{MARKER}\n{HEADER}", *parts])
def render_comment(result: CheckRunResult) -> str:
"""Build the full markdown comment, including placeholder markers."""
if not result.packages:
return f"{_intro(result)}\n\nNo tracked requirement changes detected. ✅"
return "\n\n".join(
[
f"{_intro(result)}\n\n{_summary_line(result.packages)}",
_table(result.packages),
*[_details_block(p) for p in result.packages],
]
)