grocery-bot

Log | Files | Refs | README

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