blumeops/mise-tasks/pr-comments
Erich Blume 0d422f5234
All checks were successful
Deploy Fly.io Proxy / deploy (push) Successful in 2m51s
Update tooling dependencies (March 2026) (#307)
## Summary

Monthly tooling dependency update per [[update-tooling-dependencies]].

- **Prek hooks:** trufflehog v3.93.4→v3.94.0, ruff v0.15.2→v0.15.7, shfmt v3.12.0-2→v3.13.0-1, ansible-lint floor→26.3.0, ansible-core floor→2.18
- **Fly.io proxy:** nginx 1.28.2→1.29.6, Grafana Alloy v1.13.1→v1.14.1
- **Forgejo workflows:** actions/checkout v4.3.1→v6.0.2 (SHA-pinned across all 5 workflows)
- **Mise tasks:** tightened Python lower bounds — rich≥14.0.0, typer≥0.24.0, httpx≥0.28.1, pyyaml≥6.0.2

## Test plan

- [x] `prek run --all-files` passes
- [ ] Verify Fly.io deploy succeeds after merge (nginx minor bump + Alloy bump)
- [ ] Spot-check a workflow run with the new actions/checkout v6

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: #307
2026-03-24 08:11:46 -07:00

112 lines
3.5 KiB
Text
Executable file

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx>=0.28.1", "rich>=14.0.0", "typer>=0.24.0"]
# ///
#MISE description="List unresolved comments on a PR"
#USAGE arg "<pr_number>" help="Pull request number"
"""Fetch and display unresolved review comments on a pull request.
This script queries the Forge (Gitea) API to find all review comments that
have not been resolved on a given PR. A comment is considered unresolved
if its 'resolver' field is null.
Usage: mise run pr-comments <pr_number>
"""
from typing import Annotated
import httpx
import typer
from rich.console import Console
from rich.text import Text
FORGE_API_BASE = "https://forge.eblu.me/api/v1"
REPO_OWNER = "eblume"
REPO_NAME = "blumeops"
def get_reviews(client: httpx.Client, pr_number: int) -> list[dict]:
"""Get all reviews for a pull request."""
response = client.get(
f"{FORGE_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls/{pr_number}/reviews"
)
response.raise_for_status()
return response.json()
def get_review_comments(client: httpx.Client, pr_number: int, review_id: int) -> list[dict]:
"""Get all comments for a specific review."""
response = client.get(
f"{FORGE_API_BASE}/repos/{REPO_OWNER}/{REPO_NAME}/pulls/{pr_number}/reviews/{review_id}/comments"
)
response.raise_for_status()
return response.json()
app = typer.Typer()
@app.command()
def main(
pr_number: Annotated[int, typer.Argument(help="Pull request number")],
) -> None:
console = Console()
unresolved_comments: list[tuple[dict, dict]] = [] # (review, comment) pairs
with httpx.Client() as client:
# Get all reviews
try:
reviews = get_reviews(client, pr_number)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
console.print(f"[red]Error:[/red] PR #{pr_number} not found")
else:
console.print(f"[red]Error:[/red] API request failed: {e}")
raise SystemExit(1)
# For each review, get comments and filter to unresolved
for review in reviews:
try:
comments = get_review_comments(client, pr_number, review["id"])
except httpx.HTTPStatusError:
continue # Skip reviews we can't fetch comments for
for comment in comments:
if comment.get("resolver") is None:
unresolved_comments.append((review, comment))
if not unresolved_comments:
console.print(f"[green]No unresolved comments on PR #{pr_number}[/green]")
raise SystemExit(0)
# Display unresolved comments
console.print(f"[bold]Unresolved Comments on PR #{pr_number}[/bold] ({len(unresolved_comments)} comments)")
console.print("=" * 60)
console.print()
for review, comment in unresolved_comments:
# Header with file path and reviewer
header = Text()
path = comment.get("path", "unknown file")
reviewer = review.get("user", {}).get("login", "unknown")
header.append(f"{path}", style="bold cyan")
header.append(f" (by {reviewer})")
console.print(header)
# Comment body
body = comment.get("body", "").strip()
for line in body.split("\n"):
console.print(f" {line}")
# Link to comment
html_url = comment.get("html_url", "")
if html_url:
console.print(f" [dim]{html_url}[/dim]")
console.print()
if __name__ == "__main__":
app()