Imports 67 files from VPS (/opt/teleo-eval/) into repo as the single source of truth. Previously only 8 of 67 files existed in repo — the rest were deployed directly to VPS via SCP, causing massive drift. Includes: - pipeline/lib/: 33 Python modules (daemon core, extraction, evaluation, merge, cascade, cross-domain, costs, attribution, etc.) - pipeline/: main daemon (teleo-pipeline.py), reweave.py, batch-extract-50.sh - diagnostics/: 19 files (4-page dashboard, alerting, daily digest, review queue, tier1 metrics) - agent-state/: bootstrap, lib-state, cascade inbox processor, schema - systemd/: service unit files for reference - deploy.sh: rsync-based deploy with --dry-run, syntax checks, dirty-tree gate - research-session.sh: updated with Step 8.5 digest + cascade inbox processing No new code written — all files are exact copies from VPS as of 2026-04-06. From this point forward: edit in repo, commit, then deploy.sh. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
"""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 {}
|
|
# Forgejo sometimes returns 200 with HTML (not JSON) on merge success.
|
|
# Treat 200 with non-JSON content-type as success rather than error.
|
|
content_type = resp.content_type or ""
|
|
if "json" not in content_type:
|
|
logger.debug("Forgejo API %s %s → %d (non-JSON: %s), treating as success", method, path, resp.status, content_type)
|
|
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
|