forked from sagnik/Project_Velocity
feat: Oracle Canvas Component Schema and Qwen 3.6 integration (#31)
Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: sagnik/Project_Velocity#31
This commit is contained in:
@@ -26,6 +26,10 @@ Key files:
|
||||
- `desineuron-ingress-home-ip-sync.service`: systemd oneshot service for the IP sync
|
||||
- `desineuron-ingress-home-ip-sync.timer`: persistent timer that reruns the sync every 5 minutes and on boot
|
||||
- `install_linux_ingress_ip_sync.sh`: Linux-side installer for the IP sync service
|
||||
- `deploy_velocity_site.sh`: canonical manual, timer, and webhook deploy entrypoint on the Linux origin
|
||||
- `gitea_velocity_webhook_receiver.py`: authenticated Gitea push-hook receiver on Linux origin
|
||||
- `desineuron-velocity-gitea-webhook.service`: systemd service for the webhook receiver
|
||||
- `install_linux_velocity_webhook.sh`: targeted installer for the webhook receiver
|
||||
|
||||
Manual Cloudflare work still required unless API credentials are provided:
|
||||
- set the six hostnames to DNS-only
|
||||
@@ -36,3 +40,20 @@ Dynamic home IP handling:
|
||||
- `rathole` control port `2333/tcp` is intentionally open on the ingress so public services do not break when the ISP IP changes
|
||||
- SSH fallback on the ingress remains restricted to the current home public IP on `22/tcp`
|
||||
- the Linux-side IP sync service keeps that SSH fallback rule current after ISP churn or reboot
|
||||
|
||||
Project Velocity deploy triggers:
|
||||
- Manual:
|
||||
- `sudo systemctl start desineuron-velocity-site-update.service`
|
||||
- or `sudo /usr/local/bin/deploy_velocity_site.sh`
|
||||
- Timer:
|
||||
- `desineuron-velocity-site-update.timer`
|
||||
- Webhook:
|
||||
- `https://velocity.desineuron.in/hooks/gitea/project-velocity`
|
||||
- secret is stored in `/etc/desineuron-velocity-webhook.env`
|
||||
- only `push` events for `refs/heads/main` on `sagnik/Project_Velocity` trigger deploy
|
||||
|
||||
Webhook design:
|
||||
- receiver binds `127.0.0.1:8788` only
|
||||
- nginx proxies the public HTTPS hook path to the local receiver
|
||||
- signature is verified with `X-Gitea-Signature`
|
||||
- deploy execution is serialized with `flock` so overlapping pushes cannot race each other
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name api.desineuron.in;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/desineuron-infra/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/desineuron-infra/privkey.pem;
|
||||
|
||||
access_log /var/log/nginx/api.desineuron.in.access.log;
|
||||
error_log /var/log/nginx/api.desineuron.in.error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,12 @@ CURRENT_DIR="${CURRENT_DIR:-$SERVE_ROOT/current}"
|
||||
STATE_DIR="${STATE_DIR:-$APP_ROOT/state}"
|
||||
REVISION_FILE="${REVISION_FILE:-$STATE_DIR/current_revision.txt}"
|
||||
PERSISTENT_VIDEO_DIR="${PERSISTENT_VIDEO_DIR:-$APP_ROOT/shared/videos}"
|
||||
BACKEND_SERVICE="${BACKEND_SERVICE:-desineuron-velocity-backend}"
|
||||
BACKEND_HEALTH_URL="${BACKEND_HEALTH_URL:-http://127.0.0.1:8001/health}"
|
||||
BACKEND_HEALTH_TIMEOUT_S="${BACKEND_HEALTH_TIMEOUT_S:-60}"
|
||||
RUN_BACKEND_RESTART="${RUN_BACKEND_RESTART:-1}"
|
||||
RUN_DB_MIGRATIONS="${RUN_DB_MIGRATIONS:-0}"
|
||||
DB_MIGRATION_CMD="${DB_MIGRATION_CMD:-}"
|
||||
|
||||
mkdir -p "$APP_ROOT" "$STATE_DIR" "$SERVE_ROOT" "$PERSISTENT_VIDEO_DIR"
|
||||
|
||||
@@ -25,11 +31,28 @@ if ! command -v npm >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$REPO_DIR/.git" ]; then
|
||||
git clone "$REPO_URL" "$REPO_DIR"
|
||||
fi
|
||||
|
||||
git -C "$REPO_DIR" fetch --all --prune
|
||||
REMOTE_REVISION="$(git -C "$REPO_DIR" rev-parse "origin/$BRANCH")"
|
||||
CURRENT_REVISION=""
|
||||
if [ -f "$REVISION_FILE" ]; then
|
||||
CURRENT_REVISION="$(tr -d '\r\n' < "$REVISION_FILE")"
|
||||
fi
|
||||
|
||||
if [ -n "$CURRENT_REVISION" ] && [ "$CURRENT_REVISION" = "$REMOTE_REVISION" ]; then
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STATE_DIR/last_check_utc.txt"
|
||||
echo "No new revision on origin/$BRANCH. Current revision: $CURRENT_REVISION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git -C "$REPO_DIR" checkout "$BRANCH"
|
||||
git -C "$REPO_DIR" reset --hard "origin/$BRANCH"
|
||||
|
||||
@@ -51,7 +74,30 @@ if [ -d "$PERSISTENT_VIDEO_DIR" ] && [ "$(find "$PERSISTENT_VIDEO_DIR" -maxdepth
|
||||
cp -a "$PERSISTENT_VIDEO_DIR"/. "$CURRENT_DIR/videos"/
|
||||
fi
|
||||
|
||||
git -C "$REPO_DIR" rev-parse HEAD > "$REVISION_FILE"
|
||||
if [ "$RUN_DB_MIGRATIONS" = "1" ] && [ -n "$DB_MIGRATION_CMD" ]; then
|
||||
echo "Running DB migration command..."
|
||||
bash -lc "$DB_MIGRATION_CMD"
|
||||
fi
|
||||
|
||||
if [ "$RUN_BACKEND_RESTART" = "1" ]; then
|
||||
echo "Restarting backend service: $BACKEND_SERVICE"
|
||||
systemctl restart "$BACKEND_SERVICE"
|
||||
fi
|
||||
|
||||
echo "Waiting for backend health: $BACKEND_HEALTH_URL"
|
||||
deadline=$(( $(date +%s) + BACKEND_HEALTH_TIMEOUT_S ))
|
||||
until curl -fsS "$BACKEND_HEALTH_URL" >/dev/null 2>&1; do
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
echo "Backend health check failed for $BACKEND_HEALTH_URL" >&2
|
||||
if command -v journalctl >/dev/null 2>&1; then
|
||||
journalctl -u "$BACKEND_SERVICE" -n 80 --no-pager || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
printf '%s\n' "$REMOTE_REVISION" > "$REVISION_FILE"
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ" > "$STATE_DIR/last_deploy_utc.txt"
|
||||
|
||||
echo "Deployed revision $(cat "$REVISION_FILE") to $CURRENT_DIR"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Project Velocity Gitea webhook receiver
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
EnvironmentFile=-/etc/desineuron-velocity-webhook.env
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/gitea_velocity_webhook_receiver.py
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -2,8 +2,8 @@
|
||||
Description=Periodically refresh Project Velocity site from Gitea
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=2min
|
||||
OnBootSec=60s
|
||||
OnUnitActiveSec=60s
|
||||
Unit=desineuron-velocity-site-update.service
|
||||
Persistent=true
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HOST = os.getenv("WEBHOOK_BIND_HOST", "127.0.0.1")
|
||||
PORT = int(os.getenv("WEBHOOK_BIND_PORT", "8788"))
|
||||
PATH = os.getenv("WEBHOOK_PATH", "/hooks/gitea/project-velocity")
|
||||
SECRET = os.getenv("GITEA_WEBHOOK_SECRET", "")
|
||||
EXPECTED_REF = os.getenv("GITEA_EXPECTED_REF", "refs/heads/main")
|
||||
EXPECTED_REPO = os.getenv("GITEA_REPO_FULL_NAME", "sagnik/Project_Velocity")
|
||||
LOCK_FILE = os.getenv("DEPLOY_LOCK_FILE", "/tmp/desineuron-velocity-deploy.lock")
|
||||
DEPLOY_CMD = os.getenv("DEPLOY_COMMAND", "/usr/local/bin/deploy_velocity_site.sh")
|
||||
|
||||
|
||||
def verify_signature(secret: str, body: bytes, signature: str) -> bool:
|
||||
if not secret:
|
||||
return False
|
||||
digest = hmac.new(secret.encode("utf-8"), body, hashlib.sha256).hexdigest()
|
||||
return hmac.compare_digest(digest, signature or "")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "VelocityWebhook/1.0"
|
||||
|
||||
def _respond(self, code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt: str, *args) -> None:
|
||||
print("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), fmt % args))
|
||||
|
||||
def do_GET(self) -> None:
|
||||
if self.path == "/health":
|
||||
self._respond(200, {"ok": True, "service": "desineuron-velocity-gitea-webhook"})
|
||||
return
|
||||
self._respond(404, {"error": "not_found"})
|
||||
|
||||
def do_POST(self) -> None:
|
||||
if self.path != PATH:
|
||||
self._respond(404, {"error": "not_found"})
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
body = self.rfile.read(length)
|
||||
signature = self.headers.get("X-Gitea-Signature", "")
|
||||
event = self.headers.get("X-Gitea-Event", "")
|
||||
|
||||
if not verify_signature(SECRET, body, signature):
|
||||
self._respond(401, {"error": "invalid_signature"})
|
||||
return
|
||||
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
self._respond(400, {"error": "invalid_json"})
|
||||
return
|
||||
|
||||
if event == "ping":
|
||||
self._respond(200, {"ok": True, "message": "ping_received"})
|
||||
return
|
||||
|
||||
if event != "push":
|
||||
self._respond(202, {"ok": True, "ignored": True, "reason": f"unsupported_event:{event}"})
|
||||
return
|
||||
|
||||
ref = payload.get("ref")
|
||||
repo_full_name = ((payload.get("repository") or {}).get("full_name")) or ""
|
||||
|
||||
if ref != EXPECTED_REF:
|
||||
self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_ref:{ref}"})
|
||||
return
|
||||
|
||||
if EXPECTED_REPO and repo_full_name != EXPECTED_REPO:
|
||||
self._respond(202, {"ok": True, "ignored": True, "reason": f"unexpected_repo:{repo_full_name}"})
|
||||
return
|
||||
|
||||
cmd = ["/usr/bin/flock", "-n", LOCK_FILE, "/bin/bash", "-lc", DEPLOY_CMD]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if proc.returncode == 0:
|
||||
self._respond(
|
||||
202,
|
||||
{
|
||||
"ok": True,
|
||||
"deployed": True,
|
||||
"ref": ref,
|
||||
"repository": repo_full_name,
|
||||
"stdout_tail": proc.stdout.strip().splitlines()[-10:],
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
self._respond(
|
||||
500,
|
||||
{
|
||||
"ok": False,
|
||||
"deployed": False,
|
||||
"ref": ref,
|
||||
"repository": repo_full_name,
|
||||
"returncode": proc.returncode,
|
||||
"stdout_tail": proc.stdout.strip().splitlines()[-10:],
|
||||
"stderr_tail": proc.stderr.strip().splitlines()[-10:],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||
print(f"Velocity Gitea webhook listening on http://{HOST}:{PORT}{PATH}")
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -6,6 +6,9 @@ TIMER_FILE=/etc/systemd/system/desineuron-velocity-site-update.timer
|
||||
ENV_FILE=/etc/desineuron-velocity-site.env
|
||||
SCRIPT_PATH=/usr/local/bin/deploy_velocity_site.sh
|
||||
NGINX_PATH=/etc/nginx/conf.d/velocity.desineuron.in.conf
|
||||
WEBHOOK_SERVICE_FILE=/etc/systemd/system/desineuron-velocity-gitea-webhook.service
|
||||
WEBHOOK_SCRIPT_PATH=/usr/local/bin/gitea_velocity_webhook_receiver.py
|
||||
WEBHOOK_ENV_FILE=/etc/desineuron-velocity-webhook.env
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git curl rsync nginx
|
||||
@@ -16,8 +19,10 @@ if ! command -v node >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
sudo install -m 0755 /tmp/desineuron_ingress/deploy_velocity_site.sh "$SCRIPT_PATH"
|
||||
sudo install -m 0755 /tmp/desineuron_ingress/gitea_velocity_webhook_receiver.py "$WEBHOOK_SCRIPT_PATH"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-site-update.service "$SERVICE_FILE"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-site-update.timer "$TIMER_FILE"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-gitea-webhook.service "$WEBHOOK_SERVICE_FILE"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/velocity.desineuron.in.nginx.conf "$NGINX_PATH"
|
||||
|
||||
sudo tee "$ENV_FILE" >/dev/null <<'EOF'
|
||||
@@ -31,13 +36,32 @@ SERVE_ROOT=/var/www/velocity.desineuron.in
|
||||
CURRENT_DIR=/var/www/velocity.desineuron.in/current
|
||||
STATE_DIR=/opt/desineuron-velocity-site/state
|
||||
REVISION_FILE=/opt/desineuron-velocity-site/state/current_revision.txt
|
||||
BACKEND_SERVICE=desineuron-velocity-backend
|
||||
BACKEND_HEALTH_URL=http://127.0.0.1:8001/health
|
||||
BACKEND_HEALTH_TIMEOUT_S=60
|
||||
RUN_BACKEND_RESTART=1
|
||||
RUN_DB_MIGRATIONS=0
|
||||
EOF
|
||||
|
||||
sudo chmod 0640 "$ENV_FILE"
|
||||
if [ ! -f "$WEBHOOK_ENV_FILE" ]; then
|
||||
sudo tee "$WEBHOOK_ENV_FILE" >/dev/null <<'EOF'
|
||||
WEBHOOK_BIND_HOST=127.0.0.1
|
||||
WEBHOOK_BIND_PORT=8788
|
||||
WEBHOOK_PATH=/hooks/gitea/project-velocity
|
||||
GITEA_EXPECTED_REF=refs/heads/main
|
||||
GITEA_REPO_FULL_NAME=sagnik/Project_Velocity
|
||||
DEPLOY_LOCK_FILE=/tmp/desineuron-velocity-deploy.lock
|
||||
DEPLOY_COMMAND=/usr/local/bin/deploy_velocity_site.sh
|
||||
GITEA_WEBHOOK_SECRET=replace-me
|
||||
EOF
|
||||
fi
|
||||
sudo chmod 0600 "$WEBHOOK_ENV_FILE"
|
||||
sudo mkdir -p /var/www/velocity.desineuron.in /opt/desineuron-velocity-site/state
|
||||
sudo nginx -t
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now desineuron-velocity-site-update.timer
|
||||
sudo systemctl enable --now desineuron-velocity-gitea-webhook.service
|
||||
sudo systemctl start desineuron-velocity-site-update.service
|
||||
sudo systemctl reload nginx
|
||||
sudo systemctl --no-pager --full status desineuron-velocity-site-update.service desineuron-velocity-site-update.timer
|
||||
sudo systemctl --no-pager --full status desineuron-velocity-site-update.service desineuron-velocity-site-update.timer desineuron-velocity-gitea-webhook.service
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_FILE=/etc/systemd/system/desineuron-velocity-gitea-webhook.service
|
||||
SCRIPT_PATH=/usr/local/bin/gitea_velocity_webhook_receiver.py
|
||||
ENV_FILE=/etc/desineuron-velocity-webhook.env
|
||||
NGINX_PATH=/etc/nginx/conf.d/velocity.desineuron.in.conf
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3 nginx
|
||||
|
||||
sudo install -m 0755 /tmp/desineuron_ingress/gitea_velocity_webhook_receiver.py "$SCRIPT_PATH"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/desineuron-velocity-gitea-webhook.service "$SERVICE_FILE"
|
||||
sudo install -m 0644 /tmp/desineuron_ingress/velocity.desineuron.in.nginx.conf "$NGINX_PATH"
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
sudo tee "$ENV_FILE" >/dev/null <<'EOF'
|
||||
WEBHOOK_BIND_HOST=127.0.0.1
|
||||
WEBHOOK_BIND_PORT=8788
|
||||
WEBHOOK_PATH=/hooks/gitea/project-velocity
|
||||
GITEA_EXPECTED_REF=refs/heads/main
|
||||
GITEA_REPO_FULL_NAME=sagnik/Project_Velocity
|
||||
DEPLOY_LOCK_FILE=/tmp/desineuron-velocity-deploy.lock
|
||||
DEPLOY_COMMAND=/usr/local/bin/deploy_velocity_site.sh
|
||||
GITEA_WEBHOOK_SECRET=replace-me
|
||||
EOF
|
||||
sudo chmod 0600 "$ENV_FILE"
|
||||
fi
|
||||
|
||||
sudo nginx -t
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now desineuron-velocity-gitea-webhook.service
|
||||
sudo systemctl reload nginx
|
||||
sudo systemctl --no-pager --full status desineuron-velocity-gitea-webhook.service
|
||||
@@ -14,4 +14,13 @@ server {
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = /hooks/gitea/project-velocity {
|
||||
proxy_pass http://127.0.0.1:8788/hooks/gitea/project-velocity;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user