From 5e936b2ca14094d4ed49a045fc0db70afdde2026 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Wed, 18 Mar 2026 11:41:32 -0400 Subject: [PATCH] Add auto-deployment infrastructure for all 4 LXCs - Per-LXC deploy scripts (lxc151-hookshot, lxc106-cinny, lxc139-landing, lxc110-draupnir) - Per-LXC webhook hook configs with unique HMAC-SHA256 secrets - Livekit graceful restart script + systemd timer (waits for zero active calls) - Fix hookshot/deploy.sh capitalization bug (Uptime-Kuma, Tinker-Tickets, etc.) Each LXC independently clones repo and runs its own deploy.sh via adnanh/webhook on port 9000. Co-Authored-By: Claude Sonnet 4.6 --- deploy/hooks-lxc106.json | 18 ++++++++ deploy/hooks-lxc110.json | 18 ++++++++ deploy/hooks-lxc139.json | 18 ++++++++ deploy/hooks-lxc151.json | 18 ++++++++ deploy/livekit-graceful-restart.sh | 22 ++++++++++ deploy/lxc106-cinny.sh | 41 +++++++++++++++++++ deploy/lxc110-draupnir.sh | 52 ++++++++++++++++++++++++ deploy/lxc139-landing.sh | 36 ++++++++++++++++ deploy/lxc151-hookshot.sh | 49 ++++++++++++++++++++++ hookshot/deploy.sh | 5 ++- systemd/livekit-graceful-restart.service | 9 ++++ systemd/livekit-graceful-restart.timer | 9 ++++ 12 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 deploy/hooks-lxc106.json create mode 100644 deploy/hooks-lxc110.json create mode 100644 deploy/hooks-lxc139.json create mode 100644 deploy/hooks-lxc151.json create mode 100644 deploy/livekit-graceful-restart.sh create mode 100644 deploy/lxc106-cinny.sh create mode 100644 deploy/lxc110-draupnir.sh create mode 100644 deploy/lxc139-landing.sh create mode 100644 deploy/lxc151-hookshot.sh create mode 100644 systemd/livekit-graceful-restart.service create mode 100644 systemd/livekit-graceful-restart.timer diff --git a/deploy/hooks-lxc106.json b/deploy/hooks-lxc106.json new file mode 100644 index 0000000..f0839c6 --- /dev/null +++ b/deploy/hooks-lxc106.json @@ -0,0 +1,18 @@ +[ + { + "id": "matrix-deploy", + "execute-command": "/usr/local/bin/matrix-deploy.sh", + "command-working-directory": "/opt/matrix-config", + "response-message": "Deploying cinny config...", + "trigger-rule": { + "match": { + "type": "payload-hash-sha256", + "secret": "76dd5febd1cc3458545ce37537f4bfe26f241a9635b57a2cba183ebc9221230b", + "parameter": { + "source": "header", + "name": "X-Gitea-Signature" + } + } + } + } +] diff --git a/deploy/hooks-lxc110.json b/deploy/hooks-lxc110.json new file mode 100644 index 0000000..72b0d6c --- /dev/null +++ b/deploy/hooks-lxc110.json @@ -0,0 +1,18 @@ +[ + { + "id": "matrix-deploy", + "execute-command": "/usr/local/bin/matrix-deploy.sh", + "command-working-directory": "/opt/matrix-config", + "response-message": "Deploying draupnir config...", + "trigger-rule": { + "match": { + "type": "payload-hash-sha256", + "secret": "0d23fab8743e9ee6b52cbd05a889b04c927ffa2b2b21fe50244f1a534d1a22d0", + "parameter": { + "source": "header", + "name": "X-Gitea-Signature" + } + } + } + } +] diff --git a/deploy/hooks-lxc139.json b/deploy/hooks-lxc139.json new file mode 100644 index 0000000..b8d7846 --- /dev/null +++ b/deploy/hooks-lxc139.json @@ -0,0 +1,18 @@ +[ + { + "id": "matrix-deploy", + "execute-command": "/usr/local/bin/matrix-deploy.sh", + "command-working-directory": "/opt/matrix-config", + "response-message": "Deploying matrix landing page...", + "trigger-rule": { + "match": { + "type": "payload-hash-sha256", + "secret": "ddea576ef03bff35f0c9d138b626b273d9e9502434e0717899a87677cd5ac267", + "parameter": { + "source": "header", + "name": "X-Gitea-Signature" + } + } + } + } +] diff --git a/deploy/hooks-lxc151.json b/deploy/hooks-lxc151.json new file mode 100644 index 0000000..645c1a1 --- /dev/null +++ b/deploy/hooks-lxc151.json @@ -0,0 +1,18 @@ +[ + { + "id": "matrix-deploy", + "execute-command": "/usr/local/bin/matrix-deploy.sh", + "command-working-directory": "/opt/matrix-config", + "response-message": "Deploying matrix hookshot transforms...", + "trigger-rule": { + "match": { + "type": "payload-hash-sha256", + "secret": "38ba0e66763da2096c47645cbf636ce3c2c51232e006b964e57d6bb94a32dcaa", + "parameter": { + "source": "header", + "name": "X-Gitea-Signature" + } + } + } + } +] diff --git a/deploy/livekit-graceful-restart.sh b/deploy/livekit-graceful-restart.sh new file mode 100644 index 0000000..8e6a6d0 --- /dev/null +++ b/deploy/livekit-graceful-restart.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Checks every 5 minutes if a livekit restart is pending. +# Only restarts when there are no active WebSocket connections on port 7881 +# (established connections = active call participants). +# Run by: livekit-graceful-restart.timer (systemd) + +if [ ! -f /run/livekit-restart-pending ]; then + exit 0 +fi + +# Count established WebSocket signaling connections on livekit port 7881 +ACTIVE=$(ss -tn state established '( dport = :7881 or sport = :7881 )' | grep -c ESTAB || true) + +if [ "$ACTIVE" -gt 0 ]; then + echo "$(date): Livekit restart pending but $ACTIVE active connection(s) — waiting." + exit 0 +fi + +echo "$(date): No active calls — applying livekit-server restart." +systemctl restart livekit-server +rm -f /run/livekit-restart-pending +echo "$(date): livekit-server restarted successfully." diff --git a/deploy/lxc106-cinny.sh b/deploy/lxc106-cinny.sh new file mode 100644 index 0000000..8323dc6 --- /dev/null +++ b/deploy/lxc106-cinny.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Auto-deploy script for LXC 106 (cinny) +# Handles: cinny/config.json, cinny/dev-update.sh +# Triggered by: Gitea webhook on push to main +set -euo pipefail + +REPO_DIR="/opt/matrix-config" +LOG="/var/log/matrix-deploy.log" +CLONE_URL="https://code.lotusguild.org/LotusGuild/matrix.git" + +exec >> "$LOG" 2>&1 +echo "=== $(date) === LXC106 deploy triggered ===" + +# Clone or pull +if [ ! -d "$REPO_DIR/.git" ]; then + git clone "$CLONE_URL" "$REPO_DIR" + CHANGED="cinny/config.json cinny/dev-update.sh" +else + cd "$REPO_DIR" + git fetch --all + PREV=$(git rev-parse HEAD) + git reset --hard origin/main + NEW=$(git rev-parse HEAD) + CHANGED=$(git diff --name-only "$PREV" "$NEW") + echo "Changed files: $CHANGED" +fi + +if echo "$CHANGED" | grep -q '^cinny/config.json'; then + echo "Deploying cinny config.json..." + cp "$REPO_DIR/cinny/config.json" /var/www/html/config.json + echo "✓ config.json deployed" +fi + +if echo "$CHANGED" | grep -q '^cinny/dev-update.sh'; then + echo "Deploying cinny dev-update.sh..." + cp "$REPO_DIR/cinny/dev-update.sh" /usr/local/bin/cinny-dev-update.sh + chmod +x /usr/local/bin/cinny-dev-update.sh + echo "✓ dev-update.sh deployed" +fi + +echo "=== $(date) === LXC106 deploy complete ===" diff --git a/deploy/lxc110-draupnir.sh b/deploy/lxc110-draupnir.sh new file mode 100644 index 0000000..0fa3c25 --- /dev/null +++ b/deploy/lxc110-draupnir.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Auto-deploy script for LXC 110 (draupnir) +# Handles: draupnir/production.yaml — restarts draupnir after deploy +# NOTE: access token in production.yaml is redacted in git. +# The real token lives only at /opt/draupnir/config/production.yaml on this LXC. +# This script merges the git version (structure/settings) with the live token. +# Triggered by: Gitea webhook on push to main +set -euo pipefail + +REPO_DIR="/opt/matrix-config" +LIVE_CONFIG="/opt/draupnir/config/production.yaml" +LOG="/var/log/matrix-deploy.log" +CLONE_URL="https://code.lotusguild.org/LotusGuild/matrix.git" + +exec >> "$LOG" 2>&1 +echo "=== $(date) === LXC110 deploy triggered ===" + +# Clone or pull +if [ ! -d "$REPO_DIR/.git" ]; then + git clone "$CLONE_URL" "$REPO_DIR" + CHANGED="draupnir/production.yaml" +else + cd "$REPO_DIR" + git fetch --all + PREV=$(git rev-parse HEAD) + git reset --hard origin/main + NEW=$(git rev-parse HEAD) + CHANGED=$(git diff --name-only "$PREV" "$NEW") + echo "Changed files: $CHANGED" +fi + +if echo "$CHANGED" | grep -q '^draupnir/production.yaml'; then + echo "Deploying draupnir config..." + + # Extract live access token (never stored in git) + LIVE_TOKEN=$(grep '^accessToken:' "$LIVE_CONFIG" | awk '{print $2}' | tr -d '"') + if [ -z "$LIVE_TOKEN" ]; then + echo "ERROR: Could not extract live accessToken from $LIVE_CONFIG — aborting." >&2 + exit 1 + fi + + # Copy repo version and restore the live token + cp "$REPO_DIR/draupnir/production.yaml" "$LIVE_CONFIG" + sed -i "s|accessToken: \"REDACTED\"|accessToken: \"$LIVE_TOKEN\"|" "$LIVE_CONFIG" + + echo "Restarting draupnir..." + systemctl restart draupnir + sleep 3 + systemctl is-active draupnir && echo "✓ draupnir restarted successfully" || echo "✗ draupnir failed to start" +fi + +echo "=== $(date) === LXC110 deploy complete ===" diff --git a/deploy/lxc139-landing.sh b/deploy/lxc139-landing.sh new file mode 100644 index 0000000..962f76c --- /dev/null +++ b/deploy/lxc139-landing.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Auto-deploy script for LXC 139 (nginx proxy manager) +# Handles: landing/index.html +# Triggered by: Gitea webhook on push to main +set -euo pipefail + +REPO_DIR="/opt/matrix-config" +LOG="/var/log/matrix-deploy.log" +CLONE_URL="https://code.lotusguild.org/LotusGuild/matrix.git" + +exec >> "$LOG" 2>&1 +echo "=== $(date) === LXC139 deploy triggered ===" + +# Clone or pull +if [ ! -d "$REPO_DIR/.git" ]; then + git clone "$CLONE_URL" "$REPO_DIR" + CHANGED="landing/index.html" +else + cd "$REPO_DIR" + git fetch --all + PREV=$(git rev-parse HEAD) + git reset --hard origin/main + NEW=$(git rev-parse HEAD) + CHANGED=$(git diff --name-only "$PREV" "$NEW") + echo "Changed files: $CHANGED" +fi + +if echo "$CHANGED" | grep -q '^landing/index.html'; then + echo "Deploying landing page..." + mkdir -p /var/www/matrix-landing + cp "$REPO_DIR/landing/index.html" /var/www/matrix-landing/index.html + nginx -s reload + echo "✓ landing page deployed and nginx reloaded" +fi + +echo "=== $(date) === LXC139 deploy complete ===" diff --git a/deploy/lxc151-hookshot.sh b/deploy/lxc151-hookshot.sh new file mode 100644 index 0000000..b236667 --- /dev/null +++ b/deploy/lxc151-hookshot.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Auto-deploy script for LXC 151 (matrix homeserver) +# Handles: hookshot transformation functions, livekit service file (graceful) +# Triggered by: Gitea webhook on push to main +set -euo pipefail + +REPO_DIR="/opt/matrix-config" +LOG="/var/log/matrix-deploy.log" +CLONE_URL="https://code.lotusguild.org/LotusGuild/matrix.git" +ENV_FILE="/etc/matrix-deploy.env" + +exec >> "$LOG" 2>&1 +echo "=== $(date) === LXC151 deploy triggered ===" + +# Load env (MATRIX_TOKEN, MATRIX_SERVER, MATRIX_ROOM) +if [ -f "$ENV_FILE" ]; then + source "$ENV_FILE" +fi + +# Clone or pull +if [ ! -d "$REPO_DIR/.git" ]; then + git clone "$CLONE_URL" "$REPO_DIR" +else + cd "$REPO_DIR" + git fetch --all + PREV=$(git rev-parse HEAD) + git reset --hard origin/main + NEW=$(git rev-parse HEAD) + CHANGED=$(git diff --name-only "$PREV" "$NEW") + echo "Changed files: $CHANGED" + + # Hookshot transforms + if echo "$CHANGED" | grep -q '^hookshot/'; then + echo "Deploying hookshot transforms..." + export MATRIX_TOKEN MATRIX_SERVER MATRIX_ROOM + bash "$REPO_DIR/hookshot/deploy.sh" + fi + + # Livekit service file — graceful restart (never kill active calls) + if echo "$CHANGED" | grep -q '^systemd/livekit-server.service'; then + echo "livekit-server.service changed — staging graceful restart..." + cp "$REPO_DIR/systemd/livekit-server.service" /etc/systemd/system/livekit-server.service + systemctl daemon-reload + touch /run/livekit-restart-pending + echo "Restart pending — will apply when no active calls." + fi +fi + +echo "=== $(date) === LXC151 deploy complete ===" diff --git a/hookshot/deploy.sh b/hookshot/deploy.sh index 0102b9b..c3f7a0a 100644 --- a/hookshot/deploy.sh +++ b/hookshot/deploy.sh @@ -24,8 +24,9 @@ DIR="$(cd "$(dirname "$0")" && pwd)" deploy_hook() { local file="$1" local filename="$(basename "$file" .js)" - # Capitalize first letter to match state_key (e.g. proxmox -> Proxmox) - local state_key="$(echo "$filename" | sed 's/\b\(.\)/\u\1/g' | sed 's/-\(.\)/\u\1/g')" + # Capitalize first letter of each hyphen-separated word, preserving hyphens + # e.g. proxmox -> Proxmox, uptime-kuma -> Uptime-Kuma, tinker-tickets -> Tinker-Tickets + local state_key="$(python3 -c "import sys; s='$filename'; print('-'.join(w.capitalize() for w in s.split('-')))")" local fn="$(cat "$file")" local encoded_room="$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MATRIX_ROOM'))")" local encoded_key="$(python3 -c "import urllib.parse; print(urllib.parse.quote('$state_key'))")" diff --git a/systemd/livekit-graceful-restart.service b/systemd/livekit-graceful-restart.service new file mode 100644 index 0000000..0639213 --- /dev/null +++ b/systemd/livekit-graceful-restart.service @@ -0,0 +1,9 @@ +[Unit] +Description=LiveKit graceful restart when no active calls +After=livekit-server.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/livekit-graceful-restart.sh +StandardOutput=journal +StandardError=journal diff --git a/systemd/livekit-graceful-restart.timer b/systemd/livekit-graceful-restart.timer new file mode 100644 index 0000000..022de97 --- /dev/null +++ b/systemd/livekit-graceful-restart.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Check every 5 minutes if a pending livekit restart can be applied + +[Timer] +OnBootSec=5min +OnUnitActiveSec=5min + +[Install] +WantedBy=timers.target