from dataclasses import dataclass, field
[docs]
@dataclass
class GithubIssue:
number: int # primary key; pass back as `after` for pagination
title: str
state: str # "open" | "closed"
author: str # opener login
created_at: str # strings verbatim — no datetime parsing
updated_at: str
closed_at: str | None
body: str # issue markdown body (may be empty)
labels: list[str] # label names only
url: str # html_url
is_pull_request: bool # True if GitHub returned a non-null pull_request field
comments: list[GithubComment] = field(default_factory=list)
[docs]
def to_markdown(self) -> str:
"""Render the issue + comments as one LLM-ready text blob."""
kind = "PR" if self.is_pull_request else "Issue"
lines: list[str] = [
f"# {kind} #{self.number}: {self.title}",
"",
f"- State: {self.state}",
f"- Author: {self.author}",
f"- Created: {self.created_at}",
f"- Updated: {self.updated_at}",
]
if self.closed_at is not None:
lines.append(f"- Closed: {self.closed_at}")
if self.labels:
lines.append(f"- Labels: {', '.join(self.labels)}")
lines.append(f"- URL: {self.url}")
lines += ["", "## Body", "", self.body if self.body else "_(no body)_"]
if self.comments:
lines += ["", f"## Comments ({len(self.comments)})"]
for c in self.comments:
lines += [
"",
f"### Comment by {c.author} — {c.created_at}",
"",
c.body if c.body else "_(empty comment)_",
]
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# Pull-request review state (see chia.github.github_pulls_node)
# ---------------------------------------------------------------------------
[docs]
@dataclass
class GithubReview:
"""A submitted review on a PR (the summary, not the inline comments)."""
author: str
state: str # APPROVED | CHANGES_REQUESTED | COMMENTED | DISMISSED | PENDING
body: str # the review's top-level markdown (may be empty)
submitted_at: str # verbatim
[docs]
@dataclass
class GithubPull:
"""A pull request's metadata (separate from issue/PR-as-issue)."""
number: int
title: str
state: str # "open" | "closed"
author: str
body: str
base_ref: str # branch the PR merges INTO
head_ref: str # the PR's source branch
head_sha: str
draft: bool
merged: bool
url: str # html_url
[docs]
@dataclass
class GithubCheckAnnotation:
"""A file/line-anchored message a CI check attached to its run."""
path: str
start_line: int
end_line: int
level: str # notice | warning | failure
message: str
title: str = ""
[docs]
@dataclass
class GithubCheckRun:
"""One CI check's latest result on a PR's head commit."""
name: str
status: str # queued | in_progress | completed
conclusion: str # success | failure | neutral | cancelled | skipped | timed_out | action_required ("" while running)
summary: str # output title + summary excerpt (may be empty)
url: str # html_url of the run
annotations: list[GithubCheckAnnotation] = field(default_factory=list)
_FAILED = ("failure", "timed_out")
@property
def failed(self) -> bool:
return self.conclusion in self._FAILED
[docs]
@dataclass
class GithubPullFeedback:
"""Everything a PR author needs to act on a review round: the PR plus its
reviews, inline review comments, general conversation comments, and the CI
check results on its head commit."""
pull: GithubPull
reviews: list[GithubReview] = field(default_factory=list)
review_comments: list[GithubReviewComment] = field(default_factory=list)
conversation_comments: list[GithubComment] = field(default_factory=list)
check_runs: list[GithubCheckRun] = field(default_factory=list)
def failed_checks(self) -> list[GithubCheckRun]:
return [c for c in self.check_runs if c.failed]
[docs]
def is_empty(self) -> bool:
"""True if there is nothing to act on: no reviews/comments AND no
failing CI checks (green/neutral CI alone is not feedback)."""
return not (self.reviews or self.review_comments
or self.conversation_comments or self.failed_checks())
[docs]
def to_markdown(self) -> str:
"""Render the review feedback as one LLM-ready, reviewer-style blob.
Inline comments are numbered [C1], [C2], ... so an author can reference
them when replying. Each shows its file, line, the diff hunk it anchors
to, and the reviewer's text.
"""
p = self.pull
lines: list[str] = [
f"# Review feedback on PR #{p.number}: {p.title}",
"",
f"- Base: {p.base_ref} <- Head: {p.head_ref} ({p.head_sha[:10]})",
f"- URL: {p.url}",
]
if self.reviews:
lines += ["", "## Review summaries"]
for r in self.reviews:
lines += [
"",
f"### {r.author} — {r.state}" + (f" ({r.submitted_at})" if r.submitted_at else ""),
"",
r.body if r.body else "_(no summary text)_",
]
if self.review_comments:
lines += ["", f"## Inline comments ({len(self.review_comments)})"]
for i, c in enumerate(self.review_comments, 1):
tag = f"[C{i}] {c.path}" + (f":{c.line}" if c.line else "")
if c.in_reply_to:
tag += " (reply in thread)"
lines += ["", f"### {tag} — @{c.author}"]
if c.diff_hunk:
lines += ["", "```diff", c.diff_hunk.strip("\n"), "```"]
lines += ["", c.body if c.body else "_(empty comment)_"]
if self.conversation_comments:
lines += ["", f"## Conversation comments ({len(self.conversation_comments)})"]
for c in self.conversation_comments:
lines += ["", f"### @{c.author} — {c.created_at}", "",
c.body if c.body else "_(empty comment)_"]
if self.check_runs:
failed = self.failed_checks()
ok = sum(1 for c in self.check_runs if c.conclusion == "success")
other = len(self.check_runs) - ok - len(failed)
lines += ["", f"## CI status ({len(self.check_runs)} checks: "
f"{ok} success, {len(failed)} FAILED, {other} other)"]
for c in self.check_runs:
if not c.failed:
lines += [f"- [{c.conclusion or c.status}] {c.name}"]
for c in failed:
lines += ["", f"### FAILED: {c.name} ({c.conclusion})", f"- run: {c.url}"]
if c.summary:
lines += ["", c.summary[:1500]]
if c.annotations:
lines += ["", f"Annotations ({len(c.annotations)}):"]
for a in c.annotations[:20]:
loc = f"{a.path}:{a.start_line}" + (
f"-{a.end_line}" if a.end_line != a.start_line else "")
head = f" {a.title}:" if a.title else ""
lines += [f"- {loc} [{a.level}]{head} {a.message[:500]}"]
if len(c.annotations) > 20:
lines += [f"- (+{len(c.annotations) - 20} more annotations)"]
return "\n".join(lines) + "\n"