commit ccde0a02cbc44dfeee23cc80e717eeb46b19d34c Author: AppFactory Bot Date: Wed May 27 12:39:08 2026 +0200 Initial AppFactory webhook service diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..041bb1b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends docker.io git curl \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app + +EXPOSE 9000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9000"] diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..69c4766 --- /dev/null +++ b/app/main.py @@ -0,0 +1,69 @@ +import hmac +import hashlib +import subprocess +import threading +from fastapi import FastAPI, Request, Header, HTTPException + +WEBHOOK_SECRET = "change-me-webhook-secret" +DEPLOY_SCRIPT = "/tools/deploy-app.sh" + +app = FastAPI(title="AppFactory Webhook") + +def verify_signature(body: bytes, signature: str | None): + if not signature: + raise HTTPException(status_code=401, detail="Missing signature") + + expected = hmac.new( + WEBHOOK_SECRET.encode("utf-8"), + body, + hashlib.sha256 + ).hexdigest() + + valid_signatures = [ + expected, + f"sha256={expected}", + ] + + if not any(hmac.compare_digest(s, signature) for s in valid_signatures): + raise HTTPException(status_code=401, detail="Invalid signature") + +def run_deploy(app_id: str): + subprocess.run( + [DEPLOY_SCRIPT, app_id], + capture_output=True, + text=True + ) + +@app.get("/health") +def health(): + return {"status": "ok"} + +@app.post("/gitea") +async def gitea_webhook( + request: Request, + x_gitea_signature: str | None = Header(default=None) +): + body = await request.body() + verify_signature(body, x_gitea_signature) + + payload = await request.json() + + repo_name = payload.get("repository", {}).get("name") + ref = payload.get("ref", "") + + if not repo_name: + raise HTTPException(status_code=400, detail="Missing repo name") + + if ref != "refs/heads/main": + return { + "status": "ignored", + "reason": "not main branch", + "ref": ref + } + + threading.Thread(target=run_deploy, args=(repo_name,), daemon=True).start() + + return { + "status": "accepted", + "repo": repo_name + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..364e2ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn[standard]