Initial commit: BLIGHT: CUE

Webhook listener that monitors Gitea repos for BLIGHT: triggers in
markdown files, processes them via Gemini 2.5 Flash-Lite, and writes
results back in-place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:17:31 -05:00
commit 4c9fecda16
12 changed files with 523 additions and 0 deletions

96
app.py Normal file
View File

@@ -0,0 +1,96 @@
import hashlib
import hmac
import json
import logging
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."""
if not signature_header:
return False
try:
scheme, provided_digest = signature_header.split("=", 1)
except ValueError:
return False
if scheme != "sha256":
return False
expected = hmac.new(
config.WEBHOOK_SECRET.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, provided_digest)
def _handle_push(owner: str, repo: 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", owner, repo, file_path)
try:
content, sha = gitea_client.get_file(owner, repo, file_path)
updated, changed = processor.process_document(content)
if changed:
gitea_client.update_file(owner, repo, file_path, updated, sha)
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"]
# 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", []) + commit.get("modified", []):
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, changed_files),
daemon=True,
)
thread.start()
return {"status": "processing", "files": len(changed_files)}, 200
if __name__ == "__main__":
logger.info("BLIGHT: CUE starting on port %d", config.WEBHOOK_PORT)
app.run(host="0.0.0.0", port=config.WEBHOOK_PORT)