mediapi/deploy.sh
Jean-Michel Tremblay ed9da9a065 Extract config into .env; add deploy.sh with overlay guard + rollback
- Move user-specific config (login, media roots, port, AP ssid/psk, service
  user) into a gitignored .env; add .env.example template.
- config.py reads .env at startup via a tiny zero-dependency parser (works for
  both `uv run` and systemd), and fails loudly if required vars are unset.
- load_secret_key falls back to an ephemeral key on a read-only filesystem
  instead of crashing.
- systemd units become .template files; deploy.sh renders them with the
  .env-derived user/paths.
- deploy.sh: refuses to run on a read-only overlay, git pull --ff-only,
  uv sync, ensure secret key, re-apply the AP from .env, install units,
  restart, health-check with automatic rollback to the previous commit.
- Scrub literal credentials/ssid out of the README; document .env + deploy.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:06:47 -04:00

139 lines
4.9 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# deploy.sh -- pull the latest mediapi and (re)deploy it on the Raspberry Pi.
#
# Runs ON the Pi. It:
# 0. refuses to run if the root filesystem is a read-only overlay
# 1. pulls the latest commit from origin (forgejo) with --ff-only
# 2. syncs Python deps with uv
# 3. ensures the session secret key exists (while the card is writable)
# 4. re-applies the WiFi country + AP connection from .env
# 5. renders + installs the systemd unit templates
# 6. restarts the services
# 7. health-checks the app, and rolls back to the previous commit if it
# fails to come up
#
# Config comes from .env (copy .env.example -> .env first). Needs sudo for
# nmcli / systemctl / writing to /etc; you'll be prompted as needed.
#
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$PROJECT_DIR"
# --- load config ----------------------------------------------------
if [[ ! -f .env ]]; then
echo "ERROR: .env not found. Copy .env.example to .env and fill it in." >&2
exit 1
fi
set -a
# shellcheck disable=SC1091
source .env
set +a
MEDIAPI_USER="${MEDIAPI_USER:-$(id -un)}"
MEDIAPI_PORT="${MEDIAPI_PORT:-8080}"
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
# --- 0. refuse to run on a read-only overlay ------------------------
if findmnt -no FSTYPE / | grep -q overlay; then
cat >&2 <<EOF
ERROR: the root filesystem is a read-only overlay -- any changes would vanish
on the next reboot. Disable the overlay, reboot, re-run this script, then
re-enable it:
sudo raspi-config nonint do_overlayfs 1 # disable overlay
sudo reboot
# ... after reboot:
cd $PROJECT_DIR && ./deploy.sh
sudo raspi-config nonint do_overlayfs 0 # re-enable overlay
sudo reboot
EOF
exit 1
fi
# --- 1. pull latest -------------------------------------------------
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
PREV_REF="$(git rev-parse HEAD)"
echo "==> Pulling latest from origin/$BRANCH ..."
if ! git pull --ff-only origin "$BRANCH"; then
echo "ERROR: git pull failed (uncommitted changes or non-fast-forward)." >&2
exit 1
fi
NEW_REF="$(git rev-parse HEAD)"
echo " $PREV_REF -> $NEW_REF"
# --- render + install systemd units (used again on rollback) --------
install_units() {
for unit in mediapi-mpv mediapi-app; do
sed -e "s|\${MEDIAPI_USER}|${MEDIAPI_USER}|g" \
-e "s|\${PROJECT_DIR}|${PROJECT_DIR}|g" \
-e "s|\${UV}|${UV}|g" \
"systemd/${unit}.service.template" \
| sudo tee "/etc/systemd/system/${unit}.service" >/dev/null
done
sudo systemctl daemon-reload
}
# --- 2. sync python deps -------------------------------------------
echo "==> Syncing dependencies (uv sync) ..."
"$UV" sync
# --- 3. ensure session secret key exists ---------------------------
if [[ ! -f instance/secret_key ]]; then
echo "==> Generating instance/secret_key ..."
mkdir -p instance
python3 -c "import os; print(os.urandom(32).hex())" > instance/secret_key
chmod 600 instance/secret_key
fi
# --- 4. wifi country + AP connection -------------------------------
echo "==> Applying WiFi country + AP config ..."
sudo raspi-config nonint do_wifi_country "${MEDIAPI_WIFI_COUNTRY}"
if nmcli -g NAME con show | grep -qx "${MEDIAPI_AP_CONN_NAME}"; then
echo " updating existing AP connection '${MEDIAPI_AP_CONN_NAME}'"
sudo nmcli con modify "${MEDIAPI_AP_CONN_NAME}" \
802-11-wireless.ssid "${MEDIAPI_AP_SSID}" \
wifi-sec.key-mgmt wpa-psk \
wifi-sec.psk "${MEDIAPI_AP_PASSWORD}"
else
echo " creating AP connection '${MEDIAPI_AP_CONN_NAME}'"
sudo nmcli con add type wifi ifname wlan0 con-name "${MEDIAPI_AP_CONN_NAME}" \
autoconnect yes connection.autoconnect-priority 100 save yes \
802-11-wireless.mode ap 802-11-wireless.band bg \
802-11-wireless.ssid "${MEDIAPI_AP_SSID}" \
wifi-sec.key-mgmt wpa-psk wifi-sec.psk "${MEDIAPI_AP_PASSWORD}" \
ipv4.method shared
fi
# --- 5 & 6. install units + restart --------------------------------
echo "==> Installing systemd units + restarting services ..."
install_units
sudo systemctl enable mediapi-mpv mediapi-app >/dev/null 2>&1 || true
sudo systemctl restart mediapi-mpv
sudo systemctl restart mediapi-app
# --- 7. health check + rollback ------------------------------------
echo "==> Health check on http://127.0.0.1:${MEDIAPI_PORT}/login ..."
healthy=0
for _ in $(seq 1 15); do
if curl -fsS -o /dev/null "http://127.0.0.1:${MEDIAPI_PORT}/login"; then
healthy=1
break
fi
sleep 1
done
if [[ "$healthy" -ne 1 ]]; then
echo "ERROR: app did not become healthy -- rolling back to $PREV_REF" >&2
git reset --hard "$PREV_REF"
"$UV" sync
install_units
sudo systemctl restart mediapi-mpv mediapi-app
echo "--- last 30 lines of mediapi-app log: ---" >&2
sudo journalctl -u mediapi-app -n 30 --no-pager >&2 || true
exit 1
fi
echo "==> Deploy OK. Services healthy on port ${MEDIAPI_PORT}."
systemctl --no-pager --lines=0 status mediapi-mpv mediapi-app || true