update_macos.sh 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env bash
  2. set -Eeuo pipefail
  3. INSTALL_DIR="${INSTALL_DIR:-/opt/bambuddy}"
  4. SERVICE_NAME="${SERVICE_NAME:-com.bambuddy.app}"
  5. PLIST_PATH="${PLIST_PATH:-$HOME/Library/LaunchAgents/com.bambuddy.app.plist}"
  6. BRANCH="${BRANCH:-}"
  7. VENV_PIP="${VENV_PIP:-$INSTALL_DIR/venv/bin/pip}"
  8. FRONTEND_DIR="${FRONTEND_DIR:-$INSTALL_DIR/frontend}"
  9. BACKUP_DIR="${BACKUP_DIR:-$INSTALL_DIR/backups}"
  10. BAMBUDDY_API_URL="${BAMBUDDY_API_URL:-http://127.0.0.1:8000/api/v1}"
  11. BAMBUDDY_API_KEY="${BAMBUDDY_API_KEY:-}"
  12. BACKUP_MODE="${BACKUP_MODE:-auto}" # auto|require|skip
  13. BACKUP_KEEP_COUNT=5
  14. FORCE="${FORCE:-0}"
  15. SERVICE_STOPPED=0
  16. CODE_UPDATED=0
  17. old_commit=""
  18. log() {
  19. printf '[bambuddy-update] %s\n' "$*"
  20. }
  21. warn() {
  22. printf '[bambuddy-update] WARNING: %s\n' "$*" >&2
  23. }
  24. die() {
  25. printf '[bambuddy-update] ERROR: %s\n' "$*" >&2
  26. exit 1
  27. }
  28. require_cmd() {
  29. command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
  30. }
  31. cleanup_old_backups() {
  32. local -a backup_files
  33. local max_count="$1"
  34. [ "$max_count" -gt 0 ] || return 0
  35. mapfile -t backup_files < <(ls -1t "$BACKUP_DIR"/bambuddy-backup-*.zip 2>/dev/null || true)
  36. if [ "${#backup_files[@]}" -le "$max_count" ]; then
  37. return 0
  38. fi
  39. for old_file in "${backup_files[@]:$max_count}"; do
  40. rm -f "$old_file"
  41. done
  42. log "Pruned old backups, kept newest $max_count file(s)"
  43. }
  44. is_service_active() {
  45. launchctl list | grep -q "$SERVICE_NAME"
  46. }
  47. on_error() {
  48. local exit_code="$1"
  49. if [ "$SERVICE_STOPPED" -eq 1 ]; then
  50. if [ "$CODE_UPDATED" -eq 1 ] && [ -n "$old_commit" ]; then
  51. warn "Update failed after code change, attempting rollback to $old_commit"
  52. git reset --hard "$old_commit" || warn "Rollback reset failed"
  53. fi
  54. warn "Update failed, attempting to restart service: $SERVICE_NAME"
  55. launchctl load "$PLIST_PATH" || true
  56. fi
  57. exit "$exit_code"
  58. }
  59. trap 'on_error $?' ERR
  60. create_backup() {
  61. local ts backup_file
  62. local -a auth_args=()
  63. if [ "$BACKUP_MODE" = "skip" ]; then
  64. log "Skipping backup (BACKUP_MODE=skip)"
  65. return 0
  66. fi
  67. if ! is_service_active; then
  68. if [ "$BACKUP_MODE" = "require" ]; then
  69. die "Service is not running; cannot call built-in backup API."
  70. fi
  71. warn "Service is not running; skipping built-in backup API call."
  72. return 0
  73. fi
  74. mkdir -p "$BACKUP_DIR"
  75. ts="$(date +%Y%m%d-%H%M%S)"
  76. backup_file="$BACKUP_DIR/bambuddy-backup-$ts.zip"
  77. [ -n "$BAMBUDDY_API_KEY" ] && auth_args=(-H "X-API-Key: $BAMBUDDY_API_KEY")
  78. log "Creating built-in backup via API: $backup_file"
  79. if curl --silent --show-error --fail --location \
  80. --connect-timeout 5 --max-time 900 \
  81. ${auth_args:+${auth_args[@]}} \
  82. "$BAMBUDDY_API_URL/settings/backup" \
  83. --output "$backup_file"; then
  84. log "Backup created successfully"
  85. cleanup_old_backups "$BACKUP_KEEP_COUNT"
  86. return 0
  87. fi
  88. rm -f "$backup_file"
  89. if [ "$BACKUP_MODE" = "require" ]; then
  90. die "Built-in backup API call failed (BACKUP_MODE=require)."
  91. fi
  92. warn "Built-in backup API call failed. Continuing because BACKUP_MODE=auto."
  93. }
  94. # NOTE: kept root check as-is (you can remove if desired)
  95. #[ "${EUID:-$(id -u)}" -eq 0 ] || die "Run as root (or with sudo)."
  96. case "$BACKUP_MODE" in
  97. auto|require|skip) ;;
  98. *) die "Invalid BACKUP_MODE '$BACKUP_MODE' (expected: auto, require, skip)." ;;
  99. esac
  100. require_cmd git
  101. require_cmd launchctl
  102. require_cmd curl
  103. [ -d "$INSTALL_DIR" ] || die "Install directory not found: $INSTALL_DIR"
  104. [ -f "$PLIST_PATH" ] || die "Service plist not found: $PLIST_PATH"
  105. cd "$INSTALL_DIR"
  106. [ -d .git ] || die "No git repository found in: $INSTALL_DIR"
  107. if [ -z "$BRANCH" ]; then
  108. BRANCH="$(git rev-parse --abbrev-ref HEAD)"
  109. [ "$BRANCH" = "HEAD" ] && BRANCH="main"
  110. fi
  111. # replaced systemctl show check
  112. if ! launchctl list | grep -q "$SERVICE_NAME" && [ ! -f "$PLIST_PATH" ]; then
  113. die "Service not found: $SERVICE_NAME"
  114. fi
  115. old_commit="$(git rev-parse --short HEAD || true)"
  116. log "Fetching latest code from origin/$BRANCH"
  117. git fetch --prune origin
  118. remote_commit="$(git rev-parse --short "origin/$BRANCH" || true)"
  119. log "Current commit: ${old_commit:-unknown}"
  120. log "Remote commit: ${remote_commit:-unknown}"
  121. if git diff --quiet HEAD "origin/$BRANCH"; then
  122. log "You are already running the latest version of Bambuddy."
  123. read -r -p "Do you want to run the update process anyway? [y/N]: " run_anyway
  124. case "${run_anyway:-}" in
  125. y|Y|yes|YES) ;;
  126. *) exit 0 ;;
  127. esac
  128. else
  129. read -r -p "An update for Bambuddy is available. Install now? [y/N]: " install_now
  130. case "${install_now:-}" in
  131. y|Y|yes|YES) ;;
  132. *) exit 0 ;;
  133. esac
  134. fi
  135. if [ -n "$(git status --porcelain)" ]; then
  136. if [ "$FORCE" != "1" ]; then
  137. read -r -p "Local edits were detected in your installation. Updating now will overwrite those edits. Continue? [y/N]: " answer
  138. case "${answer:-}" in
  139. y|Y|yes|YES) ;;
  140. *) die "Update cancelled by user." ;;
  141. esac
  142. else
  143. warn "Proceeding without prompt because FORCE=1."
  144. fi
  145. fi
  146. create_backup
  147. log "Stopping service: $SERVICE_NAME"
  148. launchctl unload "$PLIST_PATH"
  149. SERVICE_STOPPED=1
  150. log "Updating code to origin/$BRANCH"
  151. git reset --hard "origin/$BRANCH"
  152. CODE_UPDATED=1
  153. if [ -x "$VENV_PIP" ] && [ -f requirements.txt ]; then
  154. log "Updating Python dependencies"
  155. "$VENV_PIP" install -r requirements.txt
  156. else
  157. warn "Skipping Python dependency update (venv pip or requirements.txt missing)."
  158. fi
  159. if [ -f "$FRONTEND_DIR/package.json" ]; then
  160. if command -v npm >/dev/null 2>&1; then
  161. log "Building frontend"
  162. (
  163. cd "$FRONTEND_DIR"
  164. npm ci
  165. npm run build
  166. )
  167. else
  168. warn "Skipping frontend build (npm not installed)."
  169. fi
  170. else
  171. warn "Skipping frontend build (frontend/package.json not found)."
  172. fi
  173. log "Starting service: $SERVICE_NAME"
  174. launchctl load "$PLIST_PATH"
  175. SERVICE_STOPPED=0
  176. launchctl list | grep "$SERVICE_NAME" || true
  177. new_commit="$(git rev-parse --short HEAD || true)"
  178. log "Update complete: ${old_commit:-unknown} -> ${new_commit:-unknown}"