ganymede: extract lib/forgejo.py — single Forgejo API client
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
Some checks failed
CI / lint-and-test (pull_request) Has been cancelled
- What: Unified forgejo_api(), get_pr_diff(), get_agent_token(), repo_path() into lib/forgejo.py. Removed 3 duplicate _forgejo_api functions (evaluate.py, merge.py, validate.py), 2 duplicate _get_pr_diff functions (evaluate.py, validate.py), and 1 _agent_token function (evaluate.py). - Why: Phase 3 structural refactor. Single source of truth for all Forgejo HTTP calls. Eliminates ~90 lines of duplicated code across 3 modules. - Connections: All hardcoded repo paths now use repo_path() helper. Consumer modules no longer reference config.FORGEJO_URL/OWNER/REPO/TOKEN_FILE directly. Pentagon-Agent: Ganymede <F99EBFA6-547B-4096-BEEA-1D59C3E4028A>
This commit is contained in:
parent
927b5011b4
commit
9d69629893
4 changed files with 119 additions and 166 deletions
|
|
@ -22,6 +22,8 @@ import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from . import config, db
|
from . import config, db
|
||||||
|
from .forgejo import api as forgejo_api
|
||||||
|
from .forgejo import get_agent_token, get_pr_diff, repo_path
|
||||||
|
|
||||||
logger = logging.getLogger("pipeline.evaluate")
|
logger = logging.getLogger("pipeline.evaluate")
|
||||||
|
|
||||||
|
|
@ -61,14 +63,6 @@ async def kill_active_subprocesses():
|
||||||
_active_subprocesses.clear()
|
_active_subprocesses.clear()
|
||||||
|
|
||||||
|
|
||||||
def _agent_token(agent_name: str) -> str | None:
|
|
||||||
"""Read Forgejo token for a named agent. Returns token string or None."""
|
|
||||||
token_file = config.SECRETS_DIR / f"forgejo-{agent_name.lower()}-token"
|
|
||||||
if token_file.exists():
|
|
||||||
return token_file.read_text().strip()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
REVIEW_STYLE_GUIDE = (
|
REVIEW_STYLE_GUIDE = (
|
||||||
"Be concise. Only mention what fails or is interesting. "
|
"Be concise. Only mention what fails or is interesting. "
|
||||||
"Do not summarize what the PR does — the diff speaks for itself. "
|
"Do not summarize what the PR does — the diff speaks for itself. "
|
||||||
|
|
@ -192,32 +186,6 @@ End your review with exactly one of:
|
||||||
# ─── API helpers ───────────────────────────────────────────────────────────
|
# ─── API helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def _forgejo_api(method: str, path: str, body: dict = None, token: str = None):
|
|
||||||
"""Call Forgejo API."""
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
url = f"{config.FORGEJO_URL}/api/v1{path}"
|
|
||||||
if token is None:
|
|
||||||
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
|
||||||
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.request(
|
|
||||||
method, url, headers=headers, json=body, timeout=aiohttp.ClientTimeout(total=60)
|
|
||||||
) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
text = await resp.text()
|
|
||||||
logger.error("Forgejo API %s %s → %d: %s", method, path, resp.status, text[:200])
|
|
||||||
return None
|
|
||||||
if resp.status == 204:
|
|
||||||
return {}
|
|
||||||
return await resp.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Forgejo API error: %s %s → %s", method, path, e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _openrouter_call(model: str, prompt: str, timeout_sec: int = 120) -> str | None:
|
async def _openrouter_call(model: str, prompt: str, timeout_sec: int = 120) -> str | None:
|
||||||
"""Call OpenRouter API. Returns response text or None on failure."""
|
"""Call OpenRouter API. Returns response text or None on failure."""
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
@ -301,31 +269,6 @@ async def _claude_cli_call(model: str, prompt: str, timeout_sec: int = 600, cwd:
|
||||||
# ─── Diff helpers ──────────────────────────────────────────────────────────
|
# ─── Diff helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
async def _get_pr_diff(pr_number: int) -> str:
|
|
||||||
"""Fetch PR diff via Forgejo API."""
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
url = f"{config.FORGEJO_URL}/api/v1/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}.diff"
|
|
||||||
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(
|
|
||||||
url,
|
|
||||||
headers={"Authorization": f"token {token}", "Accept": "text/plain"},
|
|
||||||
timeout=aiohttp.ClientTimeout(total=60),
|
|
||||||
) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
return ""
|
|
||||||
diff = await resp.text()
|
|
||||||
if len(diff) > 2_000_000:
|
|
||||||
return ""
|
|
||||||
return diff
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to fetch diff for PR #%d: %s", pr_number, e)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_diff(diff: str) -> tuple[str, str]:
|
def _filter_diff(diff: str) -> tuple[str, str]:
|
||||||
"""Filter diff to only review-relevant files.
|
"""Filter diff to only review-relevant files.
|
||||||
|
|
||||||
|
|
@ -499,12 +442,11 @@ async def _post_formal_approvals(pr_number: int, pr_author: str):
|
||||||
continue
|
continue
|
||||||
if approvals >= 2:
|
if approvals >= 2:
|
||||||
break
|
break
|
||||||
token_file = config.SECRETS_DIR / f"forgejo-{agent_name}-token"
|
token = get_agent_token(agent_name)
|
||||||
if token_file.exists():
|
if token:
|
||||||
token = token_file.read_text().strip()
|
result = await forgejo_api(
|
||||||
result = await _forgejo_api(
|
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}/reviews",
|
repo_path(f"pulls/{pr_number}/reviews"),
|
||||||
{"body": "Approved.", "event": "APPROVED"},
|
{"body": "Approved.", "event": "APPROVED"},
|
||||||
token=token,
|
token=token,
|
||||||
)
|
)
|
||||||
|
|
@ -528,16 +470,16 @@ async def evaluate_pr(conn, pr_number: int, tier: str = None) -> dict:
|
||||||
return {"pr": pr_number, "skipped": True, "reason": "already_claimed"}
|
return {"pr": pr_number, "skipped": True, "reason": "already_claimed"}
|
||||||
|
|
||||||
# Fetch diff
|
# Fetch diff
|
||||||
diff = await _get_pr_diff(pr_number)
|
diff = await get_pr_diff(pr_number)
|
||||||
if not diff:
|
if not diff:
|
||||||
return {"pr": pr_number, "skipped": True, "reason": "no_diff"}
|
return {"pr": pr_number, "skipped": True, "reason": "no_diff"}
|
||||||
|
|
||||||
# Musings bypass
|
# Musings bypass
|
||||||
if _is_musings_only(diff):
|
if _is_musings_only(diff):
|
||||||
logger.info("PR #%d is musings-only — auto-approving", pr_number)
|
logger.info("PR #%d is musings-only — auto-approving", pr_number)
|
||||||
await _forgejo_api(
|
await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments",
|
repo_path(f"issues/{pr_number}/comments"),
|
||||||
{"body": "Auto-approved: musings bypass eval per collective policy."},
|
{"body": "Auto-approved: musings bypass eval per collective policy."},
|
||||||
)
|
)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -605,10 +547,10 @@ async def evaluate_pr(conn, pr_number: int, tier: str = None) -> dict:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Post domain review as comment (from agent's Forgejo account)
|
# Post domain review as comment (from agent's Forgejo account)
|
||||||
agent_tok = _agent_token(agent)
|
agent_tok = get_agent_token(agent)
|
||||||
await _forgejo_api(
|
await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments",
|
repo_path(f"issues/{pr_number}/comments"),
|
||||||
{"body": domain_review},
|
{"body": domain_review},
|
||||||
token=agent_tok,
|
token=agent_tok,
|
||||||
)
|
)
|
||||||
|
|
@ -640,10 +582,10 @@ async def evaluate_pr(conn, pr_number: int, tier: str = None) -> dict:
|
||||||
conn.execute("UPDATE prs SET leo_verdict = ? WHERE number = ?", (leo_verdict, pr_number))
|
conn.execute("UPDATE prs SET leo_verdict = ? WHERE number = ?", (leo_verdict, pr_number))
|
||||||
|
|
||||||
# Post Leo review as comment (from Leo's Forgejo account)
|
# Post Leo review as comment (from Leo's Forgejo account)
|
||||||
leo_tok = _agent_token("Leo")
|
leo_tok = get_agent_token("Leo")
|
||||||
await _forgejo_api(
|
await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments",
|
repo_path(f"issues/{pr_number}/comments"),
|
||||||
{"body": leo_review},
|
{"body": leo_review},
|
||||||
token=leo_tok,
|
token=leo_tok,
|
||||||
)
|
)
|
||||||
|
|
@ -656,9 +598,9 @@ async def evaluate_pr(conn, pr_number: int, tier: str = None) -> dict:
|
||||||
|
|
||||||
if both_approve:
|
if both_approve:
|
||||||
# Get PR author for formal approvals
|
# Get PR author for formal approvals
|
||||||
pr_info = await _forgejo_api(
|
pr_info = await forgejo_api(
|
||||||
"GET",
|
"GET",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}",
|
repo_path(f"pulls/{pr_number}"),
|
||||||
)
|
)
|
||||||
pr_author = pr_info.get("user", {}).get("login", "") if pr_info else ""
|
pr_author = pr_info.get("user", {}).get("login", "") if pr_info else ""
|
||||||
|
|
||||||
|
|
|
||||||
83
lib/forgejo.py
Normal file
83
lib/forgejo.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Forgejo API client — single shared module for all pipeline stages.
|
||||||
|
|
||||||
|
Extracted from evaluate.py, merge.py, validate.py (Phase 3 refactor).
|
||||||
|
All Forgejo HTTP calls go through this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
logger = logging.getLogger("pipeline.forgejo")
|
||||||
|
|
||||||
|
|
||||||
|
async def api(method: str, path: str, body: dict = None, token: str = None):
|
||||||
|
"""Call Forgejo API. Returns parsed JSON, {} for 204, or None on error.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: HTTP method (GET, POST, DELETE, etc.)
|
||||||
|
path: API path after /api/v1 (e.g. "/repos/teleo/teleo-codex/pulls")
|
||||||
|
body: JSON body for POST/PUT/PATCH
|
||||||
|
token: Override token. If None, reads from FORGEJO_TOKEN_FILE (admin token).
|
||||||
|
"""
|
||||||
|
url = f"{config.FORGEJO_URL}/api/v1{path}"
|
||||||
|
if token is None:
|
||||||
|
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
||||||
|
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.request(
|
||||||
|
method, url, headers=headers, json=body, timeout=aiohttp.ClientTimeout(total=60)
|
||||||
|
) as resp:
|
||||||
|
if resp.status >= 400:
|
||||||
|
text = await resp.text()
|
||||||
|
logger.error("Forgejo API %s %s → %d: %s", method, path, resp.status, text[:200])
|
||||||
|
return None
|
||||||
|
if resp.status == 204:
|
||||||
|
return {}
|
||||||
|
return await resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Forgejo API error: %s %s → %s", method, path, e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pr_diff(pr_number: int) -> str:
|
||||||
|
"""Fetch PR diff via Forgejo API. Returns diff text or empty string."""
|
||||||
|
url = f"{config.FORGEJO_URL}/api/v1/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}.diff"
|
||||||
|
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(
|
||||||
|
url,
|
||||||
|
headers={"Authorization": f"token {token}", "Accept": "text/plain"},
|
||||||
|
timeout=aiohttp.ClientTimeout(total=60),
|
||||||
|
) as resp:
|
||||||
|
if resp.status >= 400:
|
||||||
|
return ""
|
||||||
|
diff = await resp.text()
|
||||||
|
if len(diff) > 2_000_000:
|
||||||
|
return ""
|
||||||
|
return diff
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to fetch diff for PR #%d: %s", pr_number, e)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_token(agent_name: str) -> str | None:
|
||||||
|
"""Read Forgejo token for a named agent. Returns token string or None."""
|
||||||
|
token_file = config.SECRETS_DIR / f"forgejo-{agent_name.lower()}-token"
|
||||||
|
if token_file.exists():
|
||||||
|
return token_file.read_text().strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def repo_path(subpath: str = "") -> str:
|
||||||
|
"""Build standard repo API path: /repos/{owner}/{repo}/{subpath}."""
|
||||||
|
base = f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}"
|
||||||
|
if subpath:
|
||||||
|
return f"{base}/{subpath}"
|
||||||
|
return base
|
||||||
44
lib/merge.py
44
lib/merge.py
|
|
@ -16,6 +16,8 @@ import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from . import config, db
|
from . import config, db
|
||||||
|
from .forgejo import api as forgejo_api
|
||||||
|
from .forgejo import repo_path
|
||||||
|
|
||||||
logger = logging.getLogger("pipeline.merge")
|
logger = logging.getLogger("pipeline.merge")
|
||||||
|
|
||||||
|
|
@ -50,32 +52,6 @@ async def _git(*args, cwd: str = None, timeout: int = 60) -> tuple[int, str]:
|
||||||
return proc.returncode, output
|
return proc.returncode, output
|
||||||
|
|
||||||
|
|
||||||
async def _forgejo_api(method: str, path: str, body: dict = None) -> dict | list | None:
|
|
||||||
"""Call Forgejo API. Returns parsed JSON or None on error."""
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
url = f"{config.FORGEJO_URL}/api/v1{path}"
|
|
||||||
token_file = config.FORGEJO_TOKEN_FILE
|
|
||||||
token = token_file.read_text().strip() if token_file.exists() else ""
|
|
||||||
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.request(
|
|
||||||
method, url, headers=headers, json=body, timeout=aiohttp.ClientTimeout(total=30)
|
|
||||||
) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
text = await resp.text()
|
|
||||||
logger.error("Forgejo API %s %s → %d: %s", method, path, resp.status, text[:200])
|
|
||||||
return None
|
|
||||||
if resp.status == 204: # No content (DELETE)
|
|
||||||
return {}
|
|
||||||
return await resp.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Forgejo API error: %s %s → %s", method, path, e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# --- PR Discovery (Multiplayer v1) ---
|
# --- PR Discovery (Multiplayer v1) ---
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,9 +68,9 @@ async def discover_external_prs(conn) -> int:
|
||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
prs = await _forgejo_api(
|
prs = await forgejo_api(
|
||||||
"GET",
|
"GET",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls?state=open&limit=50&page={page}",
|
repo_path(f"pulls?state=open&limit=50&page={page}"),
|
||||||
)
|
)
|
||||||
if not prs:
|
if not prs:
|
||||||
break
|
break
|
||||||
|
|
@ -185,9 +161,9 @@ async def _post_ack_comment(pr_number: int):
|
||||||
"(priority: high). Expected review time: ~5 minutes.\n\n"
|
"(priority: high). Expected review time: ~5 minutes.\n\n"
|
||||||
"_This is an automated message from the Teleo pipeline._"
|
"_This is an automated message from the Teleo pipeline._"
|
||||||
)
|
)
|
||||||
await _forgejo_api(
|
await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments",
|
repo_path(f"issues/{pr_number}/comments"),
|
||||||
{"body": body},
|
{"body": body},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -296,9 +272,9 @@ async def _rebase_and_push(branch: str) -> tuple[bool, str]:
|
||||||
|
|
||||||
async def _merge_pr(pr_number: int) -> tuple[bool, str]:
|
async def _merge_pr(pr_number: int) -> tuple[bool, str]:
|
||||||
"""Merge PR via Forgejo API. Preserves PR metadata and reviewer attribution."""
|
"""Merge PR via Forgejo API. Preserves PR metadata and reviewer attribution."""
|
||||||
result = await _forgejo_api(
|
result = await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}/merge",
|
repo_path(f"pulls/{pr_number}/merge"),
|
||||||
{"Do": "merge", "merge_message_field": ""},
|
{"Do": "merge", "merge_message_field": ""},
|
||||||
)
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|
@ -312,9 +288,9 @@ async def _delete_remote_branch(branch: str):
|
||||||
If DELETE fails, log and move on — stale branch is cosmetic,
|
If DELETE fails, log and move on — stale branch is cosmetic,
|
||||||
stale merge is operational.
|
stale merge is operational.
|
||||||
"""
|
"""
|
||||||
result = await _forgejo_api(
|
result = await forgejo_api(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/branches/{branch}",
|
repo_path(f"branches/{branch}"),
|
||||||
)
|
)
|
||||||
if result is None:
|
if result is None:
|
||||||
logger.warning("Failed to delete remote branch %s — cosmetic, continuing", branch)
|
logger.warning("Failed to delete remote branch %s — cosmetic, continuing", branch)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ from difflib import SequenceMatcher
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from . import config, db
|
from . import config, db
|
||||||
|
from .forgejo import api as forgejo_api
|
||||||
|
from .forgejo import get_pr_diff, repo_path
|
||||||
|
|
||||||
logger = logging.getLogger("pipeline.validate")
|
logger = logging.getLogger("pipeline.validate")
|
||||||
|
|
||||||
|
|
@ -356,61 +358,11 @@ def extract_claim_files_from_diff(diff: str) -> dict[str, str]:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
# ─── Forgejo API (using merge module's helper) ─────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
async def _forgejo_api(method: str, path: str, body: dict = None):
|
|
||||||
"""Call Forgejo API. Reuses merge module pattern."""
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
url = f"{config.FORGEJO_URL}/api/v1{path}"
|
|
||||||
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
|
||||||
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.request(
|
|
||||||
method, url, headers=headers, json=body, timeout=aiohttp.ClientTimeout(total=30)
|
|
||||||
) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
text = await resp.text()
|
|
||||||
logger.error("Forgejo API %s %s → %d: %s", method, path, resp.status, text[:200])
|
|
||||||
return None
|
|
||||||
if resp.status == 204:
|
|
||||||
return {}
|
|
||||||
return await resp.json()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Forgejo API error: %s %s → %s", method, path, e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_pr_diff(pr_number: int) -> str:
|
|
||||||
"""Fetch PR diff via Forgejo API."""
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
url = f"{config.FORGEJO_URL}/api/v1/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}.diff"
|
|
||||||
token = config.FORGEJO_TOKEN_FILE.read_text().strip() if config.FORGEJO_TOKEN_FILE.exists() else ""
|
|
||||||
headers = {"Authorization": f"token {token}", "Accept": "text/plain"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=60)) as resp:
|
|
||||||
if resp.status >= 400:
|
|
||||||
return ""
|
|
||||||
diff = await resp.text()
|
|
||||||
if len(diff) > 2_000_000:
|
|
||||||
return "" # Too large
|
|
||||||
return diff
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Failed to fetch diff for PR #%d: %s", pr_number, e)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_pr_head_sha(pr_number: int) -> str:
|
async def _get_pr_head_sha(pr_number: int) -> str:
|
||||||
"""Get HEAD SHA of PR's branch."""
|
"""Get HEAD SHA of PR's branch."""
|
||||||
pr_info = await _forgejo_api(
|
pr_info = await forgejo_api(
|
||||||
"GET",
|
"GET",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/pulls/{pr_number}",
|
repo_path(f"pulls/{pr_number}"),
|
||||||
)
|
)
|
||||||
if pr_info:
|
if pr_info:
|
||||||
return pr_info.get("head", {}).get("sha", "")
|
return pr_info.get("head", {}).get("sha", "")
|
||||||
|
|
@ -424,9 +376,9 @@ async def _has_tier0_comment(pr_number: int, head_sha: str) -> bool:
|
||||||
# Paginate comments (Ganymede standing rule)
|
# Paginate comments (Ganymede standing rule)
|
||||||
page = 1
|
page = 1
|
||||||
while True:
|
while True:
|
||||||
comments = await _forgejo_api(
|
comments = await forgejo_api(
|
||||||
"GET",
|
"GET",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments?limit=50&page={page}",
|
repo_path(f"issues/{pr_number}/comments?limit=50&page={page}"),
|
||||||
)
|
)
|
||||||
if not comments:
|
if not comments:
|
||||||
break
|
break
|
||||||
|
|
@ -469,9 +421,9 @@ async def _post_validation_comment(pr_number: int, results: list[dict], head_sha
|
||||||
|
|
||||||
lines.append(f"\n*tier0-gate v2 | {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*")
|
lines.append(f"\n*tier0-gate v2 | {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*")
|
||||||
|
|
||||||
await _forgejo_api(
|
await forgejo_api(
|
||||||
"POST",
|
"POST",
|
||||||
f"/repos/{config.FORGEJO_OWNER}/{config.FORGEJO_REPO}/issues/{pr_number}/comments",
|
repo_path(f"issues/{pr_number}/comments"),
|
||||||
{"body": "\n".join(lines)},
|
{"body": "\n".join(lines)},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -509,7 +461,7 @@ async def validate_pr(conn, pr_number: int) -> dict:
|
||||||
return {"pr": pr_number, "skipped": True, "reason": "already_validated"}
|
return {"pr": pr_number, "skipped": True, "reason": "already_validated"}
|
||||||
|
|
||||||
# Fetch diff
|
# Fetch diff
|
||||||
diff = await _get_pr_diff(pr_number)
|
diff = await get_pr_diff(pr_number)
|
||||||
if not diff:
|
if not diff:
|
||||||
logger.debug("PR #%d: empty or oversized diff", pr_number)
|
logger.debug("PR #%d: empty or oversized diff", pr_number)
|
||||||
return {"pr": pr_number, "skipped": True, "reason": "no_diff"}
|
return {"pr": pr_number, "skipped": True, "reason": "no_diff"}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue