гих хук и сохранение изменений в контексте стори
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Webhook server: on push from remote repo, pull and run index --changed."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="RAG Agent Webhook", version="0.1.0")
|
||||
|
||||
|
||||
def _branch_from_ref(ref: str) -> str | None:
|
||||
"""refs/heads/main -> main."""
|
||||
if not ref or not ref.startswith("refs/heads/"):
|
||||
return None
|
||||
return ref.removeprefix("refs/heads/")
|
||||
|
||||
|
||||
def _verify_github_signature(body: bytes, secret: str, signature_header: str | None) -> bool:
|
||||
if not secret or not signature_header or not signature_header.startswith("sha256="):
|
||||
return not secret
|
||||
expected = hmac.new(
|
||||
secret.encode("utf-8"), body, digestmod=hashlib.sha256
|
||||
).hexdigest()
|
||||
received = signature_header.removeprefix("sha256=").strip()
|
||||
return hmac.compare_digest(received, expected)
|
||||
|
||||
|
||||
def _run_index(repo_path: str, story: str, base_ref: str, head_ref: str) -> bool:
|
||||
env = os.environ.copy()
|
||||
env["RAG_REPO_PATH"] = repo_path
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["rag-agent", "index", "--story", story, "--changed", "--base-ref", base_ref, "--head-ref", head_ref],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
logger.error("index failed: %s %s", proc.stdout, proc.stderr)
|
||||
return False
|
||||
logger.info("index completed for story=%s %s..%s", story, base_ref, head_ref)
|
||||
return True
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("index timeout for story=%s", story)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.exception("index error: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def _pull_and_index(repo_path: str, branch: str) -> None:
|
||||
repo = Path(repo_path)
|
||||
if not repo.is_dir() or not (repo / ".git").exists():
|
||||
logger.warning("not a git repo or missing: %s", repo_path)
|
||||
return
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "-C", repo_path, "fetch", "origin", branch],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=60,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning("git fetch failed: %s", e.stderr)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.exception("git fetch error: %s", e)
|
||||
return
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "-C", repo_path, "checkout", branch],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning("git checkout %s failed: %s", branch, e.stderr)
|
||||
return
|
||||
|
||||
try:
|
||||
old_head = subprocess.run(
|
||||
["git", "-C", repo_path, "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
old_head = (old_head.stdout or "").strip() if old_head.returncode == 0 else None
|
||||
except Exception as e:
|
||||
logger.exception("rev-parse HEAD: %s", e)
|
||||
return
|
||||
|
||||
try:
|
||||
merge_proc = subprocess.run(
|
||||
["git", "-C", repo_path, "merge", "--ff-only", f"origin/{branch}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("git merge timeout")
|
||||
return
|
||||
if merge_proc.returncode != 0:
|
||||
logger.warning("git merge --ff-only failed (non-fast-forward?). Skipping index.")
|
||||
return
|
||||
|
||||
new_head = subprocess.run(
|
||||
["git", "-C", repo_path, "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
new_head = (new_head.stdout or "").strip() if new_head.returncode == 0 else None
|
||||
if not old_head or not new_head or old_head == new_head:
|
||||
logger.info("no new commits for branch=%s", branch)
|
||||
return
|
||||
|
||||
_run_index(repo_path, story=branch, base_ref=old_head, head_ref=new_head)
|
||||
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook(request: Request) -> Response:
|
||||
"""Handle push webhook from GitHub/GitLab: pull repo and run index --changed."""
|
||||
body = await request.body()
|
||||
secret = os.getenv("WEBHOOK_SECRET", "").strip()
|
||||
sig = request.headers.get("X-Hub-Signature-256")
|
||||
|
||||
if secret and not _verify_github_signature(body, secret, sig):
|
||||
return PlainTextResponse("Invalid signature", status_code=401)
|
||||
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
payload = None
|
||||
if not payload or not isinstance(payload, dict):
|
||||
return PlainTextResponse("Invalid JSON", status_code=400)
|
||||
|
||||
ref = payload.get("ref")
|
||||
branch = _branch_from_ref(ref) if ref else None
|
||||
if not branch:
|
||||
return PlainTextResponse("Missing or unsupported ref", status_code=400)
|
||||
|
||||
repo_path = os.getenv("RAG_REPO_PATH", "").strip()
|
||||
if not repo_path:
|
||||
return PlainTextResponse("RAG_REPO_PATH not set", status_code=500)
|
||||
|
||||
threading.Thread(
|
||||
target=_pull_and_index,
|
||||
args=(repo_path, branch),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
return PlainTextResponse("Accepted", status_code=202)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> str:
|
||||
return "ok"
|
||||
Reference in New Issue
Block a user