- Sanitize AI responses by replacing BLIGHT: with BLIGHT: to prevent the service's own commits from triggering another processing cycle - Pass branch (extracted from refs/heads/<branch>) through to Gitea get/update calls so pushes to non-default branches are read and written correctly - Commit message now includes the file path: "BLIGHT: process triggers in <path>" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
3.5 KiB
Python
113 lines
3.5 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import subprocess
|
|
import threading
|
|
from flask import Flask, request, abort
|
|
|
|
import config
|
|
import gitea_client
|
|
import processor
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
def _verify_signature(payload: bytes, signature_header: str | None) -> bool:
|
|
"""Validate the Gitea webhook HMAC-SHA256 signature.
|
|
|
|
Gitea sends X-Gitea-Signature as a raw hex digest (no scheme prefix).
|
|
"""
|
|
if not signature_header:
|
|
return False
|
|
expected = hmac.new(
|
|
config.WEBHOOK_SECRET.encode(), payload, hashlib.sha256
|
|
).hexdigest()
|
|
return hmac.compare_digest(expected, signature_header.strip())
|
|
|
|
|
|
def _handle_push(owner: str, repo: str, branch: str, changed_files: list[str]) -> None:
|
|
"""Process all changed markdown files in a push event."""
|
|
for file_path in changed_files:
|
|
if not file_path.endswith(".md"):
|
|
continue
|
|
logger.info("Checking %s/%s@%s: %s", owner, repo, branch, file_path)
|
|
try:
|
|
content, sha = gitea_client.get_file(owner, repo, file_path, branch)
|
|
updated, changed = processor.process_document(content)
|
|
if changed:
|
|
gitea_client.update_file(owner, repo, file_path, updated, sha, branch)
|
|
logger.info("Updated %s", file_path)
|
|
else:
|
|
logger.info("No BLIGHT triggers found in %s", file_path)
|
|
except Exception as exc:
|
|
logger.error("Failed processing %s: %s", file_path, exc)
|
|
|
|
|
|
@app.post("/webhook")
|
|
def webhook():
|
|
payload = request.get_data()
|
|
|
|
if not _verify_signature(payload, request.headers.get("X-Gitea-Signature")):
|
|
logger.warning("Rejected webhook: invalid signature")
|
|
abort(403)
|
|
|
|
event = request.headers.get("X-Gitea-Event")
|
|
if event != "push":
|
|
return {"status": "ignored", "event": event}, 200
|
|
|
|
data = json.loads(payload)
|
|
owner = data["repository"]["owner"]["login"]
|
|
repo = data["repository"]["name"]
|
|
branch = data.get("ref", "").removeprefix("refs/heads/")
|
|
|
|
# Collect unique file paths from all commits in the push
|
|
seen: set[str] = set()
|
|
changed_files: list[str] = []
|
|
for commit in data.get("commits", []):
|
|
for path in (commit.get("added") or []) + (commit.get("modified") or []):
|
|
if path not in seen:
|
|
seen.add(path)
|
|
changed_files.append(path)
|
|
|
|
if not changed_files:
|
|
return {"status": "no files"}, 200
|
|
|
|
# Process in background so we return 200 to Gitea immediately
|
|
thread = threading.Thread(
|
|
target=_handle_push,
|
|
args=(owner, repo, branch, changed_files),
|
|
daemon=True,
|
|
)
|
|
thread.start()
|
|
|
|
return {"status": "processing", "files": len(changed_files)}, 200
|
|
|
|
|
|
def _self_update() -> None:
|
|
"""Pull the latest code from origin before starting."""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "pull", "--ff-only"],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
logger.info("Self-update: %s", result.stdout.strip())
|
|
else:
|
|
logger.warning("Self-update failed: %s", result.stderr.strip())
|
|
except Exception as exc:
|
|
logger.warning("Self-update error: %s", exc)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_self_update()
|
|
logger.info("BLIGHT: CUE starting on port %d", config.WEBHOOK_PORT)
|
|
app.run(host="0.0.0.0", port=config.WEBHOOK_PORT)
|