install.sh (4360B)
1 #!/usr/bin/env bash 2 set -euo pipefail 3 4 # Installs or updates the Anton bot as a systemd service. Safe to re-run: 5 # each run stages a full copy of this repo, installs dependencies and runs 6 # the test suite against that staged copy, and only touches the live 7 # deployment (and the running service) if those tests pass. An update 8 # whose tests fail aborts before anything live is touched, so the 9 # previous working install is never overwritten by a broken one. 10 # 11 # Run as root on the target LXC/VM, from within a checkout of this repo: 12 # sudo ./install.sh 13 # 14 # Override defaults via env vars if needed: 15 # ANTON_DEPLOY_USER=anton ANTON_DEPLOY_DIR=/opt/anton sudo -E ./install.sh 16 17 DEPLOY_USER="${ANTON_DEPLOY_USER:-anton}" 18 DEPLOY_DIR="${ANTON_DEPLOY_DIR:-/opt/anton}" 19 SERVICE_NAME="anton-bot" 20 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 21 22 if [[ $EUID -ne 0 ]]; then 23 echo "Run this as root: sudo $0" >&2 24 exit 1 25 fi 26 27 # Applied to both the repo->staging copy and the staging->deploy copy. 28 # .env and *.db are only relevant for the second copy (they don't exist 29 # in the repo checkout), but excluding them everywhere is harmless and 30 # keeps this one list authoritative. venv is excluded both times too -- 31 # virtualenvs embed absolute paths in their shebangs/activate scripts, so 32 # a venv is never safe to relocate between directories; the deploy venv 33 # is always built fresh in place instead (see below). 34 RSYNC_EXCLUDES=( 35 --exclude='.env' 36 --exclude='*.db' 37 --exclude='.git' 38 --exclude='.venv' 39 --exclude='venv' 40 --exclude='__pycache__' 41 --exclude='.pytest_cache' 42 --exclude='.claude' 43 --exclude='bot.log' 44 ) 45 46 echo "==> Staging a copy to test before touching the live install" 47 STAGE_DIR="$(mktemp -d)" 48 trap 'rm -rf "$STAGE_DIR"' EXIT 49 50 rsync -a "${RSYNC_EXCLUDES[@]}" "$SCRIPT_DIR"/ "$STAGE_DIR"/ 51 52 echo "==> Installing dependencies into a throwaway staging virtualenv" 53 python3 -m venv "$STAGE_DIR/venv" 54 "$STAGE_DIR/venv/bin/pip" install --upgrade -q -r "$STAGE_DIR/requirements.txt" 55 56 echo "==> Running the test suite against the staged code" 57 if ! (cd "$STAGE_DIR" && venv/bin/pytest -q); then 58 echo "==> Tests failed -- aborting. The live install (if any) is untouched." >&2 59 exit 1 60 fi 61 62 echo "==> Tests passed. Deploying to ${DEPLOY_DIR}" 63 64 if ! id "$DEPLOY_USER" &>/dev/null; then 65 echo "==> Creating system user '${DEPLOY_USER}'" 66 useradd --system --shell /usr/sbin/nologin --home-dir "$DEPLOY_DIR" --create-home "$DEPLOY_USER" 67 else 68 echo "==> System user '${DEPLOY_USER}' already exists" 69 fi 70 71 if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then 72 echo "==> Stopping running ${SERVICE_NAME} service for the update" 73 systemctl stop "$SERVICE_NAME" 74 fi 75 76 mkdir -p "$DEPLOY_DIR" 77 echo "==> Copying tested code into ${DEPLOY_DIR} (.env and *.db left untouched)" 78 rsync -a --delete "${RSYNC_EXCLUDES[@]}" "$STAGE_DIR"/ "$DEPLOY_DIR"/ 79 80 if [[ ! -f "$DEPLOY_DIR/.env" ]]; then 81 echo "==> No .env found -- creating one from .env.example" 82 cp "$DEPLOY_DIR/.env.example" "$DEPLOY_DIR/.env" 83 chmod 600 "$DEPLOY_DIR/.env" 84 fi 85 86 if [[ ! -d "$DEPLOY_DIR/venv" ]]; then 87 echo "==> Creating the deploy virtualenv" 88 python3 -m venv "$DEPLOY_DIR/venv" 89 fi 90 echo "==> Installing/upgrading dependencies in the deploy virtualenv" 91 "$DEPLOY_DIR/venv/bin/pip" install --upgrade -q -r "$DEPLOY_DIR/requirements.txt" 92 93 echo "==> Setting ownership to ${DEPLOY_USER}" 94 chown -R "$DEPLOY_USER":"$DEPLOY_USER" "$DEPLOY_DIR" 95 96 echo "==> Installing systemd unit" 97 sed \ 98 -e "s|^User=.*|User=${DEPLOY_USER}|" \ 99 -e "s|^WorkingDirectory=.*|WorkingDirectory=${DEPLOY_DIR}|" \ 100 -e "s|^EnvironmentFile=.*|EnvironmentFile=${DEPLOY_DIR}/.env|" \ 101 -e "s|^ExecStart=.*|ExecStart=${DEPLOY_DIR}/venv/bin/python ${DEPLOY_DIR}/bot.py|" \ 102 "$DEPLOY_DIR/anton-bot.service" > "/etc/systemd/system/${SERVICE_NAME}.service" 103 systemctl daemon-reload 104 systemctl enable "$SERVICE_NAME" >/dev/null 105 106 TOKEN_VALUE="$(grep -E '^TELEGRAM_BOT_TOKEN=' "$DEPLOY_DIR/.env" | cut -d '=' -f2-)" 107 if [[ -z "$TOKEN_VALUE" ]]; then 108 echo 109 echo "==> ${DEPLOY_DIR}/.env has no TELEGRAM_BOT_TOKEN set." 110 echo " Edit it, then run: systemctl start ${SERVICE_NAME}" 111 else 112 echo "==> Starting ${SERVICE_NAME}" 113 systemctl restart "$SERVICE_NAME" 114 sleep 1 115 systemctl status "$SERVICE_NAME" --no-pager -l | head -10 || true 116 fi