Switch nginx config to directory mount and add webhook listener
- Move deploy/nginx.conf -> deploy/conf.d/default.conf and mount the directory so future config changes can be hot-reloaded with `nginx -s reload` instead of a full container restart. - Add deploy/hook.py: a tiny stdlib HMAC-validated webhook listener that runs pull.sh on Gitea push events. Bound to 127.0.0.1:9528 and fronted by openresty at /_hook/deploy. - Add the matching systemd unit at deploy/facere-deploy-hook.service. - Teach pull.sh the new layout (reload vs. restart vs. compose up -d) and self-restart the hook listener if hook.py changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
104
deploy/hook.py
Normal file
104
deploy/hook.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tiny webhook listener that runs deploy/pull.sh when Gitea pushes main.
|
||||
|
||||
Listens on 127.0.0.1:9528 — exposed publicly only via openresty at
|
||||
https://web.facere.cc/_hook/deploy. Validates the Gitea HMAC-SHA256
|
||||
signature against FACERE_DEPLOY_SECRET before running anything.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
SECRET = os.environ.get("FACERE_DEPLOY_SECRET", "").encode()
|
||||
PULL = os.environ.get(
|
||||
"FACERE_DEPLOY_SCRIPT",
|
||||
"/home/ubuntu/repo/facere-website/deploy/pull.sh",
|
||||
)
|
||||
LOG = os.environ.get("FACERE_DEPLOY_LOG", "/tmp/facere-deploy.log")
|
||||
BIND_HOST = os.environ.get("FACERE_DEPLOY_BIND", "127.0.0.1")
|
||||
BIND_PORT = int(os.environ.get("FACERE_DEPLOY_PORT", "9528"))
|
||||
|
||||
_run_lock = threading.Lock()
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def _reply(self, status: int, msg: str) -> None:
|
||||
body = (msg + "\n").encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self._reply(200, "facere-deploy-hook ready")
|
||||
|
||||
def do_POST(self) -> None:
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
body = self.rfile.read(length) if length > 0 else b""
|
||||
|
||||
sig = self.headers.get("X-Gitea-Signature", "") or self.headers.get(
|
||||
"X-Hub-Signature-256", ""
|
||||
).removeprefix("sha256=")
|
||||
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
|
||||
if not sig or not hmac.compare_digest(sig, expected):
|
||||
self._reply(401, "bad signature")
|
||||
return
|
||||
|
||||
ref = ""
|
||||
try:
|
||||
ref = json.loads(body).get("ref", "")
|
||||
except Exception:
|
||||
pass
|
||||
if ref and ref != "refs/heads/main":
|
||||
self._reply(202, f"ignored ref {ref}")
|
||||
return
|
||||
|
||||
if not _run_lock.acquire(blocking=False):
|
||||
self._reply(202, "deploy already running")
|
||||
return
|
||||
try:
|
||||
with open(LOG, "a") as f:
|
||||
f.write(f"[{self.log_date_time_string()}] webhook trigger\n")
|
||||
f.flush()
|
||||
proc = subprocess.run(
|
||||
[PULL], stdout=f, stderr=subprocess.STDOUT, timeout=180
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
self._reply(200, "ok")
|
||||
else:
|
||||
self._reply(500, f"pull.sh exit {proc.returncode}")
|
||||
except subprocess.TimeoutExpired:
|
||||
self._reply(504, "pull.sh timeout")
|
||||
finally:
|
||||
_run_lock.release()
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None:
|
||||
sys.stderr.write(
|
||||
"[%s] %s - %s\n"
|
||||
% (self.log_date_time_string(), self.address_string(), fmt % args)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SECRET:
|
||||
print("FACERE_DEPLOY_SECRET is not set", file=sys.stderr)
|
||||
return 1
|
||||
srv = ThreadingHTTPServer((BIND_HOST, BIND_PORT), Handler)
|
||||
print(f"facere-deploy-hook listening on {BIND_HOST}:{BIND_PORT}", flush=True)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
srv.shutdown()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user