diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..e8c7552 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,30 @@ +name: deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + # Requires a Gitea act runner with network access to the LXC host. + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure SSH + run: | + install -m 600 /dev/null ~/.ssh/id_ed25519 + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 + echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + + - name: Deploy + env: + SSH_HOST: ${{ secrets.DEPLOY_HOST }} # e.g. deploy@10.0.0.42 + SSH_PORT: ${{ secrets.DEPLOY_PORT }} # e.g. 2222 + DEPLOY_DIR: ${{ secrets.DEPLOY_DIR }} # e.g. /opt/ai-rendezvous + DEPLOY_MODE: ${{ secrets.DEPLOY_MODE }} # docker | systemd + SERVICE_NAME: ${{ secrets.DEPLOY_SERVICE }} # systemd mode only + HEALTH_URL: ${{ secrets.DEPLOY_HEALTH_URL }} # e.g. http://127.0.0.1:3000/health + run: ./scripts/deploy.sh diff --git a/.gitignore b/.gitignore index 1427233..53e305a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ data/ *.db *.db-* .env +deploy.env diff --git a/README.md b/README.md index 641b5f4..ad6a06f 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,8 @@ curl -H 'authorization: Bearer ' http://localhost:3000/api/rooms/ ``` Full API reference: [docs/API.md](docs/API.md). MCP setup: -[docs/MCP.md](docs/MCP.md). +[docs/MCP.md](docs/MCP.md). Deploying to your own LXC via Gitea Actions or +webhooks: [docs/DEPLOY.md](docs/DEPLOY.md). ## Negotiation protocol in one paragraph diff --git a/contrib/ai-rendezvous.service b/contrib/ai-rendezvous.service new file mode 100644 index 0000000..3dc2520 --- /dev/null +++ b/contrib/ai-rendezvous.service @@ -0,0 +1,18 @@ +[Unit] +Description=AI Rendezvous server +After=network.target + +[Service] +Type=simple +User=deploy +WorkingDirectory=/opt/ai-rendezvous +ExecStart=/usr/bin/node packages/server/dist/src/index.js +Environment=PORT=3000 +Environment=DB_PATH=/opt/ai-rendezvous/data/rendezvous.db +# Set to the public URL agents will see in invite links: +Environment=BASE_URL=http://localhost:3000 +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..a6f2426 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,89 @@ +# Deploy to LXC + +One script deploys everywhere: [`scripts/deploy.sh`](../scripts/deploy.sh). +It SSHes to the LXC host, does `git fetch + reset --hard origin/main`, rebuilds +and restarts the service, then hits the health check. It never keeps secrets +in the repo — configuration comes from env vars, a gitignored `deploy.env`, or +Gitea secrets. + +## Target host (one-time setup on the LXC) + +```bash +# as root on the LXC container: +apt install -y git # plus docker+compose, or node 22 for systemd mode +useradd -m -s /bin/bash deploy || true + +# checkout (read-only deploy key tied to this repo, no push rights): +sudo -u deploy bash -c ' + git clone git@git.omniagency.ru:2222/a.andreev/AI-Rendezvous.git /opt/ai-rendezvous' +chown -R deploy:deploy /opt/ai-rendezvous +``` + +Generate a read-only deploy key on the LXC (`sudo -u deploy ssh-keygen -t +ed25519`) and add the public key as a **read-only** deploy key in Gitea +(repo Settings → Deploy keys, *without* write access). + +Choose how the service runs: + +- **docker mode (default):** `docker compose up -d` is used on every deploy. + Prepare `.env` with `BASE_URL=https://rendezvous.example` next to + `docker-compose.yml` on the LXC. +- **systemd mode:** install + [`ai-rendezvous.service`](../contrib/ai-rendezvous.service) and set + `DEPLOY_MODE=systemd`. + +## Option A — Gitea Actions (recommended) + +Add repository secrets (Settings → Actions → Secrets): + +| Secret | Example | Notes | +|---|---|---| +| `DEPLOY_HOST` | `deploy@10.0.0.42` | user@lxc-container | +| `DEPLOY_PORT` | `22` | | +| `DEPLOY_DIR` | `/opt/ai-rendezvous` | | +| `DEPLOY_MODE` | `docker` | or `systemd` | +| `DEPLOY_SERVICE` | `ai-rendezvous` | systemd mode only | +| `DEPLOY_HEALTH_URL` | `http://127.0.0.1:3000/health` | checked via SSH on the LXC | +| `DEPLOY_SSH_KEY` | *(private key)* | key of a user allowed to deploy (not the read-only one) | +| `DEPLOY_KNOWN_HOSTS` | output of `ssh-keyscan -p 22 10.0.0.42` | | + +`.gitea/workflows/deploy.yml` runs `scripts/deploy.sh` on every push to +`main`. Needs one act runner registered in Gitea (can run anywhere with SSH +access to the LXC — including the LXC itself). + +## Option B — Gitea webhook + +If you don't want an act runner: add a webhook (Settings → Webhooks) pointing +to a minimal receiver on the LXC (e.g. `webhook`/` webhookd`/a 20-line HTTP +server) that verifies the shared secret and executes: + +```bash +sudo -u deploy env SSH_HOST=deploy@localhost DEPLOY_MODE=docker \ + /opt/ai-rendezvous/scripts/deploy.sh +``` + +Gitea → webhook → receiver → `deploy.sh`. Same script, no CI needed. + +## Option C — manual / from a dev machine (or the agent's shell) + +```bash +./scripts/deploy.sh # reads deploy.env (gitignored) if present +``` + +`deploy.env` example: + +```bash +SSH_HOST=deploy@lxc42.internal +SSH_PORT=22 +DEPLOY_DIR=/opt/ai-rendezvous +DEPLOY_MODE=docker +HEALTH_URL=http://127.0.0.1:3000/health +``` + +## Deploy flow for the agent + +1. Make changes, `npm test` locally. +2. Commit and push to `main` on Gitea. +3. Gitea Actions (or webhook) runs `scripts/deploy.sh`. +4. The health check must pass; the action fails loudly otherwise. +5. Human verifies on the deployed instance. diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..b30a77c --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Deploy AI Rendezvous to the LXC host over SSH. +# +# Works in three ways: +# 1. locally: ./scripts/deploy.sh +# 2. from Gitea Actions: .gitea/workflows/deploy.yml calls this script +# 3. from a webhook: any tiny receiver on the LXC can exec this script +# +# Configuration via environment (or a gitignored deploy.env next to this file): +# SSH_HOST user@lxc-host (required) +# SSH_PORT 22 +# DEPLOY_DIR /opt/ai-rendezvous checkout on the LXC +# BRANCH main +# DEPLOY_MODE docker | systemd how to run the service +# SERVICE_NAME ai-rendezvous systemd unit name (systemd mode) +# HEALTH_URL e.g. http://127.0.0.1:3000/health checked after deploy +set -euo pipefail + +cd "$(dirname "$0")/.." + +# shellcheck disable=SC1091 +[ -f deploy.env ] && . ./deploy.env + +: "${SSH_HOST:?Set SSH_HOST (user@lxc-host) via env, deploy.env or CI secrets}" +SSH_PORT="${SSH_PORT:-22}" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/ai-rendezvous}" +BRANCH="${BRANCH:-main}" +DEPLOY_MODE="${DEPLOY_MODE:-docker}" +SERVICE_NAME="${SERVICE_NAME:-ai-rendezvous}" +HEALTH_URL="${HEALTH_URL:-}" + +SSH_CMD=(ssh -p "$SSH_PORT" -o StrictHostKeyChecking=accept-new "$SSH_HOST") + +echo "==> Deploying $BRANCH to $SSH_HOST:$DEPLOY_DIR (mode: $DEPLOY_MODE)" + +"${SSH_CMD[@]}" BRANCH="$BRANCH" DEPLOY_DIR="$DEPLOY_DIR" DEPLOY_MODE="$DEPLOY_MODE" \ + SERVICE_NAME="$SERVICE_NAME" bash -s <<'REMOTE' +set -euo pipefail +cd "$DEPLOY_DIR" + +echo "--> fetching $BRANCH" +umask 022 +git fetch --prune origin +git reset --hard "origin/$BRANCH" + +if [ "$DEPLOY_MODE" = "docker" ]; then + echo "--> rebuilding containers" + docker compose up -d --build --remove-orphans +elif [ "$DEPLOY_MODE" = "systemd" ]; then + echo "--> building" + npm ci --no-audit --no-fund + npm run build + echo "--> restarting $SERVICE_NAME" + systemctl restart "$SERVICE_NAME" +else + echo "unknown DEPLOY_MODE: $DEPLOY_MODE" >&2 + exit 1 +fi +REMOTE + +if [ -n "$HEALTH_URL" ]; then + echo "==> Health check $HEALTH_URL" + if "${SSH_CMD[@]}" curl -sf --max-time 5 --retry 10 --retry-delay 3 --retry-connrefused "$HEALTH_URL" >/dev/null; then + echo "==> healthy" + else + echo "!! health check FAILED after deploy" >&2 + exit 1 + fi +fi + +echo "==> deploy OK"