chore: added a Gitea pipeline that deploys to the VPS
This commit is contained in:
parent
3640b2d785
commit
5063791b62
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
47
.gitea/workflows/deploy.yml
Normal file
47
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore:
|
||||||
|
- '**/*.md'
|
||||||
|
- 'docs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm test
|
||||||
|
- run: npm run build
|
||||||
|
- run: python3 -m unittest discover -s hermes-plugin/srs-gate -v
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Deploy to the VPS
|
||||||
|
env:
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key && chmod 600 ~/.ssh/deploy_key
|
||||||
|
ssh-keygen -y -f ~/.ssh/deploy_key > /dev/null \
|
||||||
|
|| { echo "ERROR: DEPLOY_SSH_KEY is not a valid private key"; exit 1; }
|
||||||
|
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
|
||||||
|
SSH="ssh -i $HOME/.ssh/deploy_key -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=15 -o ServerAliveInterval=30 -o ServerAliveCountMax=6 srs@$DEPLOY_HOST"
|
||||||
|
|
||||||
|
# send exactly the commit that was tested, then run its deploy script, all under one lock
|
||||||
|
git archive --format=tar.gz "$GITHUB_SHA" \
|
||||||
|
| $SSH 'flock -w 900 ~/.srs-deploy.lock bash -c "rm -rf ~/srs-api.new && mkdir ~/srs-api.new && tar -xz -C ~/srs-api.new && bash ~/srs-api.new/deploy/deploy.sh"'
|
||||||
89
deploy/deploy.sh
Normal file
89
deploy/deploy.sh
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NEW="$HOME/srs-api.new"
|
||||||
|
LIVE="$HOME/srs-api"
|
||||||
|
DATA="$HOME/srs-data"
|
||||||
|
ENV_FILE="$DATA/.env"
|
||||||
|
BACKUPS="$DATA/backups"
|
||||||
|
|
||||||
|
fail() { echo "ERROR: $*" >&2; exit 1; }
|
||||||
|
env_value() { grep -E "^$1=" "$ENV_FILE" | tail -n1 | cut -d= -f2- | tr -d '"' || true; }
|
||||||
|
|
||||||
|
echo "== checks"
|
||||||
|
[ -d "$NEW" ] || fail "missing $NEW"
|
||||||
|
[ -f "$ENV_FILE" ] || fail "missing $ENV_FILE: create it once by hand (first deploy)"
|
||||||
|
chmod 600 "$ENV_FILE"
|
||||||
|
|
||||||
|
DB_PATH="$(env_value DB_PATH)"
|
||||||
|
case "$DB_PATH" in
|
||||||
|
"$LIVE"|"$LIVE"/*|"$NEW"|"$NEW"/*) fail "DB_PATH must be outside $LIVE" ;;
|
||||||
|
/*) ;;
|
||||||
|
*) fail "DB_PATH must be an absolute path under $DATA (got '${DB_PATH:-unset}')" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
PORT="$(env_value PORT)"; PORT="${PORT:-8080}"
|
||||||
|
|
||||||
|
if ! pm2 describe srs-api >/dev/null 2>&1 && ss -tln | grep -q "127.0.0.1:$PORT "; then
|
||||||
|
fail "port $PORT is already in use and srs-api is not running: close the SSH tunnel"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo -n -H -u hermes true || fail "srs cannot act as hermes: add the sudoers line (first deploy)"
|
||||||
|
|
||||||
|
echo "== build"
|
||||||
|
cd "$NEW"
|
||||||
|
ln -sfn "$ENV_FILE" .env
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "== database backup"
|
||||||
|
if [ -f "$DB_PATH" ]; then
|
||||||
|
mkdir -p "$BACKUPS"
|
||||||
|
sqlite3 "$DB_PATH" ".backup '$BACKUPS/srs-$(date +%Y%m%d-%H%M%S).db'"
|
||||||
|
ls -1t "$BACKUPS"/srs-*.db | tail -n +15 | xargs -r rm -f
|
||||||
|
else
|
||||||
|
echo "no database yet: skipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== migrate"
|
||||||
|
node dist/db/migrate.js
|
||||||
|
|
||||||
|
echo "== swap and restart"
|
||||||
|
cd "$HOME"
|
||||||
|
rm -rf "$LIVE"
|
||||||
|
mv "$NEW" "$LIVE"
|
||||||
|
cd "$LIVE"
|
||||||
|
pm2 startOrRestart ecosystem.config.cjs --update-env
|
||||||
|
pm2 save
|
||||||
|
|
||||||
|
echo "== health"
|
||||||
|
ok=0
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if curl -fsS "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then ok=1; break; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
[ "$ok" = 1 ] || fail "srs-api did not answer /api/health: check 'pm2 logs srs-api'"
|
||||||
|
echo "srs-api is healthy"
|
||||||
|
|
||||||
|
echo "== plugin"
|
||||||
|
SRC="$LIVE/hermes-plugin/srs-gate"
|
||||||
|
version_of() { sed -n 's/^version:[[:space:]]*"\{0,1\}\([^" ]*\)"\{0,1\}.*/\1/p' | head -n1; }
|
||||||
|
NEW_VER="$(version_of < "$SRC/plugin.yaml")"
|
||||||
|
# Runs from "/" because hermes cannot enter srs's folders.
|
||||||
|
OLD_VER="$(cd / && sudo -n -H -u hermes bash -c 'cat "$HOME/.hermes/plugins/srs-gate/plugin.yaml" 2>/dev/null' | version_of || true)"
|
||||||
|
|
||||||
|
[ -n "$NEW_VER" ] || fail "no version in the repo's plugin.yaml"
|
||||||
|
if [ -z "$OLD_VER" ]; then
|
||||||
|
fail "srs-gate is not installed on this server: install and enable it by hand first (HERMES-PLUGIN-PLAN section 6)"
|
||||||
|
fi
|
||||||
|
if [ "$NEW_VER" = "$OLD_VER" ]; then echo "plugin $OLD_VER is current: nothing to do"; exit 0; fi
|
||||||
|
HIGHEST="$(printf '%s\n%s\n' "$OLD_VER" "$NEW_VER" | sort -V | tail -n1)"
|
||||||
|
if [ "$HIGHEST" != "$NEW_VER" ]; then
|
||||||
|
echo "WARNING: repo plugin $NEW_VER is older than installed $OLD_VER: plugin skipped (rollback is manual)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "updating plugin $OLD_VER -> $NEW_VER"
|
||||||
|
INSTALLER="$(cat "$LIVE/deploy/install-plugin.sh")"
|
||||||
|
tar -c -C "$SRC" __init__.py plugin.yaml \
|
||||||
|
| (cd / && sudo -n -H -u hermes bash -c "$INSTALLER" install-plugin "$OLD_VER" "$NEW_VER")
|
||||||
|
echo "plugin updated: send a test WhatsApp message and look for 'srs-gate: id=... status=200' in agent.log"
|
||||||
77
deploy/install-plugin.sh
Normal file
77
deploy/install-plugin.sh
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OLD_VER="${1:?old version}"
|
||||||
|
NEW_VER="${2:?new version}"
|
||||||
|
|
||||||
|
H="$HOME/.hermes"
|
||||||
|
PLUGIN="$H/plugins/srs-gate"
|
||||||
|
BACKUPS="$H/plugin-backups"
|
||||||
|
KEEP=5
|
||||||
|
|
||||||
|
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
|
||||||
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus"
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
|
||||||
|
command -v hermes >/dev/null || { echo "ERROR: hermes command not found in PATH" >&2; exit 1; }
|
||||||
|
[ -d "$PLUGIN" ] || { echo "ERROR: $PLUGIN does not exist" >&2; exit 1; }
|
||||||
|
|
||||||
|
STAGE="$(mktemp -d "$H/.srs-gate-stage.XXXXXX")"
|
||||||
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
|
|
||||||
|
echo "-- unpack"
|
||||||
|
tar -x -C "$STAGE" -f -
|
||||||
|
for f in __init__.py plugin.yaml; do
|
||||||
|
[ -f "$STAGE/$f" ] || { echo "ERROR: missing $f in upload" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
[ "$(ls -A "$STAGE" | wc -l)" = 2 ] || { echo "ERROR: unexpected files in upload" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "-- load check"
|
||||||
|
PY="$H/hermes-agent/venv/bin/python"
|
||||||
|
[ -x "$PY" ] || PY=python3
|
||||||
|
"$PY" -B - "$STAGE/__init__.py" <<'CHECK'
|
||||||
|
import asyncio, importlib.util, sys
|
||||||
|
spec = importlib.util.spec_from_file_location("srs_gate_load_check", sys.argv[1])
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
hooks = []
|
||||||
|
class Ctx:
|
||||||
|
def register_hook(self, name, callback):
|
||||||
|
hooks.append((name, callback))
|
||||||
|
module.register(Ctx())
|
||||||
|
assert [name for name, _ in hooks] == ["pre_gateway_dispatch"], "hook not registered"
|
||||||
|
result = asyncio.run(hooks[0][1](event=None))
|
||||||
|
assert result.get("action") == "skip", "hook did not answer skip"
|
||||||
|
print("plugin loads, registers and skips")
|
||||||
|
CHECK
|
||||||
|
|
||||||
|
echo "-- backup"
|
||||||
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
BACKUP="$BACKUPS/srs-gate-$OLD_VER-$TS"
|
||||||
|
mkdir -p "$BACKUPS"
|
||||||
|
cp -a "$PLUGIN" "$BACKUP"
|
||||||
|
ls -1dt "$BACKUPS"/srs-gate-* | tail -n +$((KEEP + 1)) | xargs -r rm -rf
|
||||||
|
|
||||||
|
echo "-- swap"
|
||||||
|
chmod 755 "$STAGE"; chmod 644 "$STAGE"/*
|
||||||
|
mv "$PLUGIN" "$H/.srs-gate-replaced"
|
||||||
|
mv "$STAGE" "$PLUGIN"
|
||||||
|
rm -rf "$H/.srs-gate-replaced"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
echo "-- restart gateway"
|
||||||
|
hermes -p default gateway restart
|
||||||
|
|
||||||
|
echo "-- check"
|
||||||
|
ok=0
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
sleep 2
|
||||||
|
if systemctl --user is-active --quiet hermes-gateway \
|
||||||
|
&& curl -fsS http://127.0.0.1:3000/health 2>/dev/null | grep -q '"connected"'; then ok=1; break; fi
|
||||||
|
done
|
||||||
|
if [ "$ok" != 1 ]; then
|
||||||
|
echo "ERROR: gateway or bridge not healthy after restart." >&2
|
||||||
|
echo "Previous plugin is in: $BACKUP" >&2
|
||||||
|
echo "Roll back: mv $PLUGIN $BACKUPS/srs-gate-bad && cp -a $BACKUP $PLUGIN && hermes -p default gateway restart" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "plugin $OLD_VER -> $NEW_VER installed; backup: $BACKUP"
|
||||||
@ -7,6 +7,7 @@ module.exports = {
|
|||||||
exec_mode: 'fork',
|
exec_mode: 'fork',
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
max_memory_restart: '300M',
|
max_memory_restart: '300M',
|
||||||
|
kill_timeout: 12000,
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user