126 lines
3.9 KiB
Bash
Executable File
126 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Zero-downtime prod deploy: pull, sync deps, build front, migrate, then
|
|
# gracefully reload gunicorn (see polylan-submitter.service, installed and
|
|
# restarted instead when it changed) and restart the RQ worker (see
|
|
# polylan-submitter-worker.conf).
|
|
#
|
|
# Run from anywhere as the user owning the checkout, needs sudo for
|
|
# systemctl/supervisorctl (no-op when already root):
|
|
# ./deploy/systemd/deploy.sh
|
|
#
|
|
# Migrations run while the old code still serves requests, so keep them
|
|
# backward compatible (nullable/default columns, drop/rename in a later deploy).
|
|
|
|
set -euo pipefail
|
|
|
|
SERVICE="${SERVICE:-polylan-submitter}"
|
|
WORKER="${WORKER:-polylan-submitter-worker}"
|
|
SUPERVISOR_CONF_DIR="${SUPERVISOR_CONF_DIR:-/etc/supervisor/conf.d}"
|
|
WEB_URL="${WEB_URL:-https://polylan-submitter.legrems.xyz/}"
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
DEPLOY_DIR="$REPO_DIR/deploy/systemd"
|
|
APP_DIR="$REPO_DIR/polylan_submitter"
|
|
PYTHON="$REPO_DIR/.venv/bin/python"
|
|
# Same as the unit's Environment=, so migrate/collectstatic see the prod settings.
|
|
# Relative to $APP_DIR, where manage.py runs.
|
|
export SETTINGS_ENV_FILE_LOCATION="${SETTINGS_ENV_FILE_LOCATION:-polylan_submitter/.env}"
|
|
|
|
# gunicorn's master doesn't reload itself on HUP, so a server upgrade needs a
|
|
# full restart instead.
|
|
server_versions() {
|
|
"$PYTHON" - <<'EOF' 2>/dev/null || true
|
|
import importlib.metadata as m
|
|
for pkg in ("gunicorn", "uvicorn", "uvicorn-worker"):
|
|
try:
|
|
print(pkg, m.version(pkg))
|
|
except m.PackageNotFoundError:
|
|
print(pkg, "missing")
|
|
EOF
|
|
}
|
|
|
|
# wait_for <description> <command...>: retry for ~30s, fail the deploy if it
|
|
# never succeeds.
|
|
wait_for() {
|
|
local what="$1"
|
|
shift
|
|
for _ in $(seq 1 30); do
|
|
if "$@" >/dev/null 2>&1; then
|
|
echo " $what: ok"
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo " $what: FAILED" >&2
|
|
return 1
|
|
}
|
|
|
|
web_responds() {
|
|
# 000 = nothing listening, 5xx = Caddy up but gunicorn down (502) or broken.
|
|
local code
|
|
code="$(curl -s -o /dev/null -w '%{http_code}' "$WEB_URL")"
|
|
[ "$code" != "000" ] && [ "$code" -lt 500 ]
|
|
}
|
|
|
|
worker_running() {
|
|
sudo supervisorctl status "$WORKER" | grep -q RUNNING
|
|
}
|
|
|
|
cd "$REPO_DIR"
|
|
|
|
echo "==> git pull"
|
|
git pull --ff-only
|
|
|
|
before="$(server_versions)"
|
|
|
|
echo "==> uv sync"
|
|
uv sync --frozen --no-dev
|
|
|
|
after="$(server_versions)"
|
|
|
|
echo "==> frontend build"
|
|
cd "$APP_DIR"
|
|
pnpm install --frozen-lockfile
|
|
pnpm build
|
|
|
|
echo "==> migrate"
|
|
"$PYTHON" manage.py migrate --noinput
|
|
|
|
echo "==> collectstatic"
|
|
"$PYTHON" manage.py collectstatic --noinput
|
|
|
|
echo "==> $SERVICE (systemd)"
|
|
unit_changed=false
|
|
if ! cmp -s "$DEPLOY_DIR/$SERVICE.service" "/etc/systemd/system/$SERVICE.service"; then
|
|
sudo install -m 644 "$DEPLOY_DIR/$SERVICE.service" "/etc/systemd/system/$SERVICE.service"
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable "$SERVICE"
|
|
unit_changed=true
|
|
fi
|
|
|
|
# HUP re-forks workers with the master's original args, so a new ExecStart
|
|
# (unit change) or a new gunicorn/uvicorn needs a full restart.
|
|
if [ "$unit_changed" = true ]; then
|
|
echo "==> unit file changed, full restart of $SERVICE"
|
|
sudo systemctl restart "$SERVICE"
|
|
elif [ "$before" != "$after" ]; then
|
|
echo "==> server packages changed, full restart of $SERVICE"
|
|
sudo systemctl restart "$SERVICE"
|
|
else
|
|
echo "==> graceful reload of $SERVICE"
|
|
sudo systemctl reload "$SERVICE"
|
|
fi
|
|
wait_for "$SERVICE active" systemctl is-active --quiet "$SERVICE"
|
|
wait_for "$SERVICE responds on $WEB_URL" web_responds
|
|
|
|
echo "==> $WORKER (supervisor)"
|
|
sudo install -m 644 "$DEPLOY_DIR/$WORKER.conf" "$SUPERVISOR_CONF_DIR/$WORKER.conf"
|
|
# reread+update picks up config changes (and starts it on first install),
|
|
# restart loads the new code - TERM is a warm shutdown, the running job ends first.
|
|
sudo supervisorctl reread
|
|
sudo supervisorctl update "$WORKER"
|
|
sudo supervisorctl restart "$WORKER"
|
|
wait_for "$WORKER running" worker_running
|
|
|
|
echo "==> done"
|