2026-01-21 19:14:27 -08:00
|
|
|
#!/usr/bin/env -S uv run --script
|
|
|
|
|
# /// script
|
|
|
|
|
# requires-python = ">=3.12"
|
Update tooling dependencies (Feb 2026 cycle) (#254)
## Summary
Monthly tooling dependency update cycle:
- **Pre-commit hooks**: trufflehog v3.92.5→v3.93.4, ruff v0.14.13→v0.15.2, shellcheck v0.10.0.1→v0.11.0.1, prettier v3.8.0→v3.8.1, actionlint v1.7.10→v1.7.11
- **Fly.io Dockerfile**: pin nginx to 1.28.2-alpine (was unpinned), bump alloy v1.5.1→v1.13.1
- **Mise tasks**: normalize httpx lower bound to >=0.28.0 and typer to >=0.15.0 across all scripts
- **Forgejo workflows**: actions/checkout@v4 is current, no changes needed
- **New how-to doc**: [[update-tooling-dependencies]] documenting this monthly cycle
## No changes needed
- pre-commit-hooks v6.0.0, yamllint v1.38.0, shfmt v3.12.0-2, taplo v0.9.3, ansible-lint 26.1.1 — all already at latest
## Test plan
- [x] `uvx pre-commit run --all-files` — all 24 hooks pass
- [ ] Fly.io deploy (triggered automatically on merge to main via deploy-fly workflow)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/254
2026-02-23 13:08:41 -08:00
|
|
|
# dependencies = ["httpx>=0.28.0", "rich>=13.0.0"]
|
2026-01-21 19:14:27 -08:00
|
|
|
# ///
|
|
|
|
|
#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>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
from rich.console import Console
|
|
|
|
|
from rich.text import Text
|
|
|
|
|
|
Expose Forgejo publicly at forge.eblu.me (#278)
## Summary
Expose Forgejo publicly at `forge.eblu.me` via the Fly.io reverse proxy — the first dynamic, authenticated public-facing service.
- **Forgejo hardening:** Domain changed to forge.eblu.me, SSH stays on forge.ops.eblu.me, reverse proxy trust headers configured, local registration locked to external-only (Authentik SSO)
- **Tailscale Ingress:** ExternalName Service + Ingress in tailscale-operator creates forge.tail8d86e.ts.net endpoint
- **Fly.io proxy:** nginx server block with rate-limited auth endpoints (3r/s), fail2ban with custom nginx-deny action, security headers, /swagger blocked, WebSocket support, 512m body limit
- **Authentik:** OAuth callback updated to forge.eblu.me
- **DNS/TLS:** CNAME record in Pulumi, cert in fly-setup
- **Rename:** ~29 files updated from forge.ops.eblu.me to forge.eblu.me (HTTPS refs only; SSH, container builds, and Caddy table kept as-is)
## Deployment Order
1. `mise run provision-indri -- --tags forgejo` (config changes)
2. Verify forge.ops.eblu.me still works
3. `argocd app set tailscale-operator --revision feature/forge-public && argocd app sync tailscale-operator`
4. Verify `curl https://forge.tail8d86e.ts.net`
5. `cd fly && fly deploy`
6. Verify pre-DNS: `curl -H "Host: forge.eblu.me" https://blumeops-proxy.fly.dev/`
7. `fly certs add forge.eblu.me -a blumeops-proxy`
8. `argocd app set authentik --revision feature/forge-public && argocd app sync authentik`
9. `mise run dns-preview && mise run dns-up`
10. Full verification (see below)
11. Rehearse `mise run fly-shutoff`
12. After merge: reset ArgoCD revisions to main, re-sync
## Verification Checklist
- [ ] forge.eblu.me loads, shows public repos
- [ ] forge.ops.eblu.me still works from tailnet
- [ ] SSH clone via forge.ops.eblu.me:2222 works
- [ ] HTTPS clone via forge.eblu.me works
- [ ] UI shows forge.eblu.me for HTTPS clone, forge.ops.eblu.me for SSH
- [ ] /swagger returns 403
- [ ] Rapid login attempts trigger 429 rate limit
- [ ] fail2ban bans after 5 failed logins in 10 minutes
- [ ] ArgoCD can still sync (SSH unaffected)
- [ ] `mise run fly-shutoff` stops all public traffic
- [ ] `mise run services-check` passes
Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/278
2026-03-03 08:40:41 -08:00
|
|
|
FORGE_API_BASE = "https://forge.eblu.me/api/v1"
|
2026-01-21 19:14:27 -08:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
console = Console()
|
|
|
|
|
|
|
|
|
|
if len(sys.argv) < 2:
|
|
|
|
|
console.print("[red]Error:[/red] Please provide a PR number")
|
|
|
|
|
console.print("Usage: mise run pr-comments <pr_number>")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
pr_number = int(sys.argv[1])
|
|
|
|
|
except ValueError:
|
|
|
|
|
console.print(f"[red]Error:[/red] '{sys.argv[1]}' is not a valid PR number")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
return 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]")
|
|
|
|
|
return 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()
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|