Browse Source

feat: add native updater script with backup and safety checks

uefigs139 3 months ago
parent
commit
d12aaa222e
2 changed files with 194 additions and 6 deletions
  1. 25 6
      install/README.md
  2. 169 0
      install/update.sh

+ 25 - 6
install/README.md

@@ -26,6 +26,7 @@ curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/insta
 |--------|----------|--------|
 |--------|----------|--------|
 | `install.sh` | Linux, macOS | Native (Python venv) |
 | `install.sh` | Linux, macOS | Native (Python venv) |
 | `docker-install.sh` | Linux, macOS | Docker |
 | `docker-install.sh` | Linux, macOS | Docker |
+| `update.sh` | Linux (systemd) | Native update helper |
 
 
 ---
 ---
 
 
@@ -159,12 +160,30 @@ docker compose logs -f      # View logs
 
 
 **Native installation:**
 **Native installation:**
 ```bash
 ```bash
-cd /opt/bambuddy
-git pull
-source venv/bin/activate
-pip install -r requirements.txt
-cd frontend && npm ci && npm run build
-sudo systemctl restart bambuddy  # Linux
+curl -fsSL https://raw.githubusercontent.com/maziggy/bambuddy/main/install/update.sh -o update.sh
+chmod +x update.sh
+sudo ./update.sh
+```
+
+The updater performs:
+- Root permission check (fails fast before any work)
+- Optional built-in backup API call (`/api/v1/settings/backup`) before update
+- Local-change warning + confirmation before `git reset --hard`
+- Service stop/start with rollback attempt if update fails
+
+Useful environment overrides:
+```bash
+# Typical native install defaults
+INSTALL_DIR=/opt/bambuddy SERVICE_NAME=bambuddy sudo ./update.sh
+
+# Require backup to succeed (abort update if backup fails)
+BACKUP_MODE=require sudo ./update.sh
+
+# Skip backup API call
+BACKUP_MODE=skip sudo ./update.sh
+
+# Auth-enabled instances: provide API key for backup endpoint
+BAMBUDDY_API_KEY=bb_xxx BACKUP_MODE=require sudo ./update.sh
 ```
 ```
 
 
 **Docker (pre-built image):**
 **Docker (pre-built image):**

+ 169 - 0
install/update.sh

@@ -0,0 +1,169 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+INSTALL_DIR="${INSTALL_DIR:-/opt/bambuddy}"
+SERVICE_NAME="${SERVICE_NAME:-bambuddy}"
+BRANCH="${BRANCH:-}"
+VENV_PIP="${VENV_PIP:-$INSTALL_DIR/venv/bin/pip}"
+FRONTEND_DIR="${FRONTEND_DIR:-$INSTALL_DIR/frontend}"
+BACKUP_DIR="${BACKUP_DIR:-$INSTALL_DIR/backups}"
+BAMBUDDY_API_URL="${BAMBUDDY_API_URL:-http://127.0.0.1:8000/api/v1}"
+BAMBUDDY_API_KEY="${BAMBUDDY_API_KEY:-}"
+BACKUP_MODE="${BACKUP_MODE:-auto}" # auto|require|skip
+FORCE="${FORCE:-0}"
+
+SERVICE_STOPPED=0
+
+log() {
+  printf '[bambuddy-update] %s\n' "$*"
+}
+
+warn() {
+  printf '[bambuddy-update] WARNING: %s\n' "$*" >&2
+}
+
+die() {
+  printf '[bambuddy-update] ERROR: %s\n' "$*" >&2
+  exit 1
+}
+
+require_cmd() {
+  command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
+}
+
+on_error() {
+  local exit_code="$1"
+  if [ "$SERVICE_STOPPED" -eq 1 ]; then
+    warn "Update failed, attempting to restart service: $SERVICE_NAME"
+    systemctl start "$SERVICE_NAME" || true
+  fi
+  exit "$exit_code"
+}
+trap 'on_error $?' ERR
+
+create_backup() {
+  local ts backup_file
+
+  if [ "$BACKUP_MODE" = "skip" ]; then
+    log "Skipping backup (BACKUP_MODE=skip)"
+    return 0
+  fi
+
+  if ! systemctl is-active --quiet "$SERVICE_NAME"; then
+    if [ "$BACKUP_MODE" = "require" ]; then
+      die "Service is not running; cannot call built-in backup API."
+    fi
+    warn "Service is not running; skipping built-in backup API call."
+    return 0
+  fi
+
+  mkdir -p "$BACKUP_DIR"
+  ts="$(date +%Y%m%d-%H%M%S)"
+  backup_file="$BACKUP_DIR/bambuddy-backup-$ts.zip"
+
+  log "Creating built-in backup via API: $backup_file"
+  if [ -n "$BAMBUDDY_API_KEY" ]; then
+    if curl --silent --show-error --fail --location \
+      --connect-timeout 5 --max-time 900 \
+      -H "X-API-Key: $BAMBUDDY_API_KEY" \
+      "$BAMBUDDY_API_URL/settings/backup" \
+      --output "$backup_file"; then
+      log "Backup created successfully"
+      return 0
+    fi
+  else
+    if curl --silent --show-error --fail --location \
+      --connect-timeout 5 --max-time 900 \
+      "$BAMBUDDY_API_URL/settings/backup" \
+      --output "$backup_file"; then
+      log "Backup created successfully"
+      return 0
+    fi
+  fi
+
+  rm -f "$backup_file"
+  if [ "$BACKUP_MODE" = "require" ]; then
+    die "Built-in backup API call failed (BACKUP_MODE=require)."
+  fi
+  warn "Built-in backup API call failed. Continuing because BACKUP_MODE=auto."
+}
+
+[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root (or with sudo)."
+
+case "$BACKUP_MODE" in
+  auto|require|skip) ;;
+  *) die "Invalid BACKUP_MODE '$BACKUP_MODE' (expected: auto, require, skip)." ;;
+esac
+
+require_cmd git
+require_cmd systemctl
+require_cmd curl
+
+[ -d "$INSTALL_DIR" ] || die "Install directory not found: $INSTALL_DIR"
+cd "$INSTALL_DIR"
+[ -d .git ] || die "No git repository found in: $INSTALL_DIR"
+
+if [ -z "$BRANCH" ]; then
+  BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+  [ "$BRANCH" = "HEAD" ] && BRANCH="main"
+fi
+
+if ! systemctl list-unit-files --type=service | grep -q "^${SERVICE_NAME}\.service"; then
+  die "Service not found: ${SERVICE_NAME}.service"
+fi
+
+if [ -n "$(git status --porcelain)" ]; then
+  warn "Local changes detected in $INSTALL_DIR."
+  warn "This update uses: git reset --hard origin/$BRANCH"
+  if [ "$FORCE" != "1" ]; then
+    read -r -p "Discard local changes and continue? [y/N]: " answer
+    case "${answer:-}" in
+      y|Y|yes|YES) ;;
+      *) die "Update cancelled by user." ;;
+    esac
+  else
+    warn "Proceeding without prompt because FORCE=1."
+  fi
+fi
+
+create_backup
+
+old_commit="$(git rev-parse --short HEAD || true)"
+
+log "Stopping service: $SERVICE_NAME"
+systemctl stop "$SERVICE_NAME"
+SERVICE_STOPPED=1
+
+log "Updating code from origin/$BRANCH"
+git fetch --prune origin
+git reset --hard "origin/$BRANCH"
+
+if [ -x "$VENV_PIP" ] && [ -f requirements.txt ]; then
+  log "Updating Python dependencies"
+  "$VENV_PIP" install -r requirements.txt
+else
+  warn "Skipping Python dependency update (venv pip or requirements.txt missing)."
+fi
+
+if [ -f "$FRONTEND_DIR/package.json" ]; then
+  if command -v npm >/dev/null 2>&1; then
+    log "Building frontend"
+    (
+      cd "$FRONTEND_DIR"
+      npm ci
+      npm run build
+    )
+  else
+    warn "Skipping frontend build (npm not installed)."
+  fi
+else
+  warn "Skipping frontend build (frontend/package.json not found)."
+fi
+
+log "Starting service: $SERVICE_NAME"
+systemctl start "$SERVICE_NAME"
+SERVICE_STOPPED=0
+systemctl --no-pager --lines=8 status "$SERVICE_NAME"
+
+new_commit="$(git rev-parse --short HEAD || true)"
+log "Update complete: ${old_commit:-unknown} -> ${new_commit:-unknown}"