#!/bin/sh # Battani güncelleme betiği — https://battani.dev/update.sh # # sudo sh /opt/battani/update.sh # curl -fsSL https://battani.dev/update.sh | sudo sh # # Seçenekler (ortam değişkeni karşılığı parantez içinde): # --version X.Y.Z Hedef sürüm; varsayılan en yeni sürüm (BATTANI_VERSION) # --dir YOL Kurulum klasörü; varsayılan /opt/battani (BATTANI_DIR) # --no-backup Veritabanı yedeği alma (BATTANI_BACKUP=0) # --force Aynı sürüm olsa da yeniden başlat (BATTANI_FORCE=1) # --yes Onay sorma (BATTANI_YES=1) # # Sıra: yeni imajı indir → PostgreSQL yedeği al (backups/) → sürümü .env'de # değiştir → servisleri yeniden oluştur → sağlık kontrolü. Yeni sürüm açılmazsa # eski sürüme geri döner. Veritabanı geçişleri yeni sürümün ilk açılışında # çalışır; geri dönüş sonrası sorun olursa alınan yedekten dönülebilir. set -eu PORTAL_URL="${BATTANI_PORTAL_URL:-https://battani.dev}" INSTALL_DIR="${BATTANI_DIR:-/opt/battani}" VERSION="${BATTANI_VERSION:-}" BACKUP="${BATTANI_BACKUP:-1}" FORCE="${BATTANI_FORCE:-0}" ASSUME_YES="${BATTANI_YES:-0}" SKIP_PULL="${BATTANI_SKIP_PULL:-0}" if [ -t 1 ]; then C_RED=$(printf '\033[31m'); C_GRN=$(printf '\033[32m'); C_YEL=$(printf '\033[33m'); C_BLD=$(printf '\033[1m'); C_OFF=$(printf '\033[0m') else C_RED=''; C_GRN=''; C_YEL=''; C_BLD=''; C_OFF='' fi say() { printf '%s==>%s %s\n' "$C_GRN" "$C_OFF" "$*"; } warn() { printf '%s!!%s %s\n' "$C_YEL" "$C_OFF" "$*" >&2; } die() { printf '%sHATA:%s %s\n' "$C_RED" "$C_OFF" "$*" >&2; exit 1; } ask_yes() { [ "$ASSUME_YES" = "1" ] && return 0 if [ -r /dev/tty ] && { : /dev/null; then printf '%s [E/h] ' "$1" >/dev/tty read -r answer /dev/null 2>&1; then curl -fsSL "$@" elif command -v wget >/dev/null 2>&1; then wget -qO- "$@" else die "curl ya da wget gerekli." fi } fetch_bearer() { if command -v curl >/dev/null 2>&1; then curl -fsSL -H "Authorization: Bearer $2" "$1" else wget -qO- --header="Authorization: Bearer $2" "$1" fi } env_get() { sed -n "s/^$1=//p" "$INSTALL_DIR/.env" | tail -n 1; } env_set() { # KEY VALUE — değer yalnız sürüm karakterleri içerir if grep -q "^$1=" "$INSTALL_DIR/.env"; then sed -i "s|^$1=.*|$1=$2|" "$INSTALL_DIR/.env" else printf '%s=%s\n' "$1" "$2" >>"$INSTALL_DIR/.env" fi } latest_version() { case "$IMAGE" in ghcr.io/*) ;; *) return 1 ;; esac repo="${IMAGE#ghcr.io/}" token=$(fetch "https://ghcr.io/token?scope=repository:${repo}:pull" 2>/dev/null | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') [ -n "$token" ] || return 1 fetch_bearer "https://ghcr.io/v2/${repo}/tags/list?n=10000" "$token" 2>/dev/null \ | tr ',[]{}' '\n\n\n\n\n' | tr -d ' "' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1 } compose() { docker compose --project-directory "$INSTALL_DIR" "$@"; } wait_healthy() { # container, saniye — sağlıklıysa 0; durduysa, çöküp duruyorsa ya da süre dolduysa 1 i=0 while [ "$i" -lt "$2" ]; do info=$(docker inspect -f '{{.State.Status}} {{.RestartCount}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$1" 2>/dev/null || echo 'missing 0 none') state=${info%% *}; rest=${info#* }; restarts=${rest%% *}; health=${rest#* } [ "$health" = "healthy" ] && return 0 case "$state" in exited|dead|missing) return 1 ;; esac [ "$restarts" -ge 3 ] && return 1 sleep 5 i=$((i + 5)) done return 1 } while [ $# -gt 0 ]; do case "$1" in --version) VERSION="${2:?--version bir değer ister}"; shift 2 ;; --dir) INSTALL_DIR="${2:?--dir bir değer ister}"; shift 2 ;; --no-backup) BACKUP=0; shift ;; --force) FORCE=1; shift ;; -y|--yes) ASSUME_YES=1; shift ;; -h|--help) sed -n '2,18p' "$0" 2>/dev/null || true; exit 0 ;; *) die "Bilinmeyen seçenek: $1 (yardım için --help)" ;; esac done VERSION="${VERSION#v}" printf '\n%sBattani güncelleme%s\n\n' "$C_BLD" "$C_OFF" [ -f "$INSTALL_DIR/.env" ] && [ -f "$INSTALL_DIR/docker-compose.yml" ] \ || die "$INSTALL_DIR içinde kurulum bulunamadı. Kurmak için: curl -fsSL $PORTAL_URL/install.sh | sudo sh" docker info >/dev/null 2>&1 || die "Docker'a erişilemiyor. Betiği sudo ile çalıştırın." IMAGE="$(env_get BATTANI_IMAGE)" CURRENT="$(env_get BATTANI_IMAGE_TAG)" [ -n "$IMAGE" ] || die ".env içinde BATTANI_IMAGE yok; bu kurulum install.sh ile yapılmamış." if [ -z "$VERSION" ]; then say "En yeni sürüm aranıyor…" VERSION=$(latest_version || true) [ -n "$VERSION" ] || die "En yeni sürüm bulunamadı; --version ile belirtin." fi say "Kurulu sürüm: ${C_BLD}${CURRENT:-?}${C_OFF} → hedef: ${C_BLD}${VERSION}${C_OFF}" if [ "$VERSION" = "$CURRENT" ] && [ "$FORCE" != "1" ]; then say "Zaten güncel. (Yeniden başlatmak için --force)" exit 0 fi ask_yes "Güncelleme başlasın mı?" || die "İptal edildi." if [ "$SKIP_PULL" != "1" ]; then say "Yeni imaj indiriliyor: $IMAGE:$VERSION" docker pull --quiet "$IMAGE:$VERSION" >/dev/null || die "İmaj indirilemedi: $IMAGE:$VERSION" fi backup_file='' if [ "$BACKUP" = "1" ]; then mkdir -p "$INSTALL_DIR/backups" backup_file="$INSTALL_DIR/backups/battani-$(date -u '+%Y%m%d-%H%M%S')-${CURRENT:-unknown}.sql.gz" say "Veritabanı yedeği alınıyor: $backup_file" umask 077 # Container içinde pipefail: başarısız bir pg_dump gzip'in arkasında kaybolmasın. # shellcheck disable=SC2016 if ! compose exec -T postgres sh -c 'set -o pipefail; pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --no-owner | gzip' >"$backup_file" \ || ! gzip -t "$backup_file" 2>/dev/null; then rm -f "$backup_file" die "Yedek alınamadı; güncelleme yapılmadı. (Yedeksiz devam için --no-backup)" fi fi # Eski kurulumlar: lisans kurulum kimliği için sunucu kimliği bağlantısını ekle. if ! grep -q 'host-machine-id' "$INSTALL_DIR/docker-compose.yml"; then say "Sunucu kimliği bağlantısı ekleniyor (lisans kurulum kimliği için)…" if [ -s /etc/machine-id ]; then host_id_file=/etc/machine-id else host_id_file="$INSTALL_DIR/host-id" [ -s "$host_id_file" ] || { cat /sys/class/dmi/id/product_uuid 2>/dev/null || cat /proc/sys/kernel/random/uuid; } | head -n 1 | tr -d ' \n' >"$host_id_file" fi grep -q '^BATTANI_HOST_ID_FILE=' "$INSTALL_DIR/.env" || env_set BATTANI_HOST_ID_FILE "$host_id_file" sed -i 's#^ - app-config:/app/config$# - app-config:/app/config\n - ${BATTANI_HOST_ID_FILE}:/etc/host-machine-id:ro#' "$INSTALL_DIR/docker-compose.yml" fi # Eski kurulumlar: elle girilen veri servisi kimlik bilgileri için şifreleme anahtarı. if ! grep -q '^INTEGRATION_SECRETS_KEY=.' "$INSTALL_DIR/.env"; then env_set INTEGRATION_SECRETS_KEY "$(LC_ALL=C tr -dc 'A-Za-z0-9' &1 || true if [ -n "$CURRENT" ]; then warn "Eski sürüme ($CURRENT) dönülüyor…" env_set BATTANI_IMAGE_TAG "$CURRENT" compose up -d || true if wait_healthy battani-app 300; then warn "$CURRENT sürümüne geri dönüldü ve çalışıyor." else warn "Eski sürüm de açılamadı." fi fi if [ -n "$backup_file" ]; then warn "Gerekirse veritabanını güncelleme öncesine döndürmek için:" warn " gunzip -c $backup_file | docker compose --project-directory $INSTALL_DIR exec -T postgres sh -c 'psql -U \"\$POSTGRES_USER\" -d \"\$POSTGRES_DB\"'" warn " (önce 'docker compose stop app' ve boş bir veritabanı gerekir)" fi die "Güncelleme başarısız. Loglar: cd $INSTALL_DIR && docker compose logs -f app"