Ctrl K
ComunidadGuías…

update.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -Eeuo pipefail

if (($# > 1)) || { (($# == 1)) && [[ $1 != --standalone ]]; }; then
  printf 'Usage: bash update.sh [--standalone]\n' >&2
  exit 2
fi

repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
cd "$repo_root"

# Re-execute the freshly pulled script so this remains the only command needed
# for both a code update and a restart after editing .env.
if [[ ${LINK_BOT_UPDATE_REEXEC:-} != 1 ]]; then
  # A host-side lock also coordinates manual and Mini App updates. The open
  # descriptor survives re-exec and is released when the script finishes.
  exec 9>"$repo_root/.git/link-bot-update.lock"
  if ! flock -n 9; then
    printf 'Обновление Link-Bot уже выполняется.\n' >&2
    exit 1
  fi
  bash ./scripts/update-source.sh
  exec env LINK_BOT_UPDATE_REEXEC=1 bash "$repo_root/update.sh" "$@"
fi

bash ./scripts/ensure-env.sh .env

read_env_value() {
  local key=$1 line value=""
  while IFS= read -r line || [[ -n $line ]]; do
    line=${line%$'\r'}
    if [[ $line =~ ^[[:space:]]*${key}[[:space:]]*=(.*)$ ]]; then
      value=${BASH_REMATCH[1]}
    fi
  done < .env
  value=${value%%#*}
  value=${value#"${value%%[![:space:]]*}"}
  value=${value%"${value##*[![:space:]]}"}
  if [[ $value == \"*\" || $value == \'*\' ]]; then
    value=${value:1:${#value}-2}
  fi
  printf '%s' "$value"
}

public_host=$(read_env_value PUBLIC_HOST)
public_base_url=$(read_env_value PUBLIC_BASE_URL)
cabinet_subdomain=$(read_env_value CABINET_SUBDOMAIN)
admin_subdomain=$(read_env_value ADMIN_SUBDOMAIN)
if [[ -n $admin_subdomain ]]; then
  if [[ ! $admin_subdomain =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]] ||
     ((${#admin_subdomain} > 63)) || [[ ${admin_subdomain,,} == ${cabinet_subdomain,,} ]]; then
    printf 'ADMIN_SUBDOMAIN must be a DNS label different from CABINET_SUBDOMAIN\n' >&2
    exit 1
  fi
  admin_subdomain=${admin_subdomain,,}
fi
if [[ ! $public_host =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$ ]] ||
   { [[ -n $cabinet_subdomain ]] && {
     [[ ! $cabinet_subdomain =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]] ||
     ((${#cabinet_subdomain} > 63));
   }; }; then
  printf 'Invalid CABINET_SUBDOMAIN or PUBLIC_HOST in .env\n' >&2
  exit 1
fi
if [[ $public_base_url != "https://${public_host}" ]]; then
  printf 'PUBLIC_BASE_URL must be https://%s when PUBLIC_HOST is %s. Update both values in .env.\n' "$public_host" "$public_host" >&2
  exit 1
fi

# A Caddy container can have the same name and Caddyfile mount while belonging
# to a different Compose project. Recreate it with its actual project name.
caddy_container_exists=false
caddyfile_mount=""
caddy_project=""
caddy_service=""
if docker inspect link-bot-caddy >/dev/null 2>&1; then
  caddy_container_exists=true
  caddyfile_mount=$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/etc/caddy/Caddyfile"}}{{.Source}}{{end}}{{if eq .Destination "/etc/caddy"}}{{.Source}}/Caddyfile{{end}}{{end}}' link-bot-caddy)
  caddy_project=$(docker inspect --format '{{index .Config.Labels "com.docker.compose.project"}}' link-bot-caddy 2>/dev/null || true)
  caddy_service=$(docker inspect --format '{{index .Config.Labels "com.docker.compose.service"}}' link-bot-caddy 2>/dev/null || true)
fi

mode=bot
if [[ -n $caddyfile_mount &&
      $(readlink -f -- "$caddyfile_mount") == $(readlink -f -- ./Caddyfile) &&
      $caddy_service == caddy && -n $caddy_project &&
      $caddy_project != '<no value>' && $caddy_project != '<nil>' ]]; then
  mode=managed
elif [[ -n $caddyfile_mount ]]; then
  mode=shared
elif ! "$caddy_container_exists"; then
  mode=new
else
  printf 'Cannot locate the active Caddyfile; refusing to start a second Caddy.\n' >&2
  exit 1
fi

if [[ $mode == managed || $mode == new ]] &&
   ! grep -Fq '{$PUBLIC_HOST' ./Caddyfile &&
   ! grep -Fq "$public_host" ./Caddyfile; then
  printf 'Bundled Caddyfile does not include PUBLIC_HOST or %s; update its site address before restarting.\n' "$public_host" >&2
  exit 1
fi

if [[ $mode == shared && ( -n $cabinet_subdomain || -n $admin_subdomain ) ]]; then
  printf 'Shared Caddy detected; updating its existing site instead of starting bundled Caddy.\n'
  if [[ ! -f $caddyfile_mount || ! -w $caddyfile_mount ||
        $(docker inspect --format '{{.State.Running}}' link-bot-caddy) != true ]]; then
    printf 'The shared Caddyfile is unavailable or its container is stopped.\n' >&2
    exit 1
  fi

  backup=$(mktemp "${caddyfile_mount}.link-bot-backup.XXXXXX")
  cp -p -- "$caddyfile_mount" "$backup"

  restore_caddyfile() {
    cat "$backup" > "$caddyfile_mount"
    docker exec link-bot-caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null 2>&1 || true
  }

  for extra_subdomain in "$cabinet_subdomain" "$admin_subdomain"; do
    [[ -n $extra_subdomain ]] || continue
    if ! bash ./scripts/ensure-caddy-host.sh "$caddyfile_mount" "$public_host" "${extra_subdomain}.${public_host}"; then
      restore_caddyfile
      printf 'Shared Caddyfile was not changed.\n' >&2
      exit 1
    fi
  done
  if ! docker exec link-bot-caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile ||
     ! docker exec link-bot-caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile; then
    restore_caddyfile
    printf 'Caddy rejected the updated config; restored backup %s\n' "$backup" >&2
    exit 1
  fi
  if cmp -s "$backup" "$caddyfile_mount"; then
    rm -f -- "$backup"
  else
    printf 'Shared Caddyfile backup: %s\n' "$backup"
  fi
fi

export LINK_BOT_VERSION=$(git describe --tags --always 2>/dev/null || printf 'dev')
export LINK_BOT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || printf 'none')

# Keep Docker's detailed output available on failure, while showing concise
# progress in an interactive terminal. CI and verbose runs keep raw output.
phase_total=5
if [[ $mode == managed || $mode == new ]]; then ((phase_total += 1)); fi
if [[ -n $cabinet_subdomain ]]; then ((phase_total += 1)); fi
if [[ -n $admin_subdomain ]]; then ((phase_total += 1)); fi
phase_current=0
animate_update=false
if [[ -t 1 && ${TERM:-dumb} != dumb && ${LINK_BOT_UPDATE_VERBOSE:-0} != 1 ]]; then
  animate_update=true
fi

run_phase() {
  local label=$1 log pid started status frame=0
  local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
  shift
  ((phase_current += 1))

  if ! "$animate_update"; then
    printf '[%d/%d] %s\n' "$phase_current" "$phase_total" "$label"
    if "$@"; then
      printf '✓ %s\n' "$label"
      return 0
    else
      status=$?
    fi
    printf '✗ %s\n' "$label" >&2
    return "$status"
  fi

  log=$(mktemp "${TMPDIR:-/tmp}/link-bot-update.XXXXXX")
  started=$SECONDS
  "$@" >"$log" 2>&1 &
  pid=$!
  while kill -0 "$pid" 2>/dev/null; do
    printf '\r\033[2K  \033[36m%s\033[0m [%d/%d] %s · %d с' \
      "${frames[frame]}" "$phase_current" "$phase_total" "$label" "$((SECONDS - started))"
    frame=$(((frame + 1) % ${#frames[@]}))
    sleep 0.12
  done

  if wait "$pid"; then
    printf '\r\033[2K  \033[32m✓\033[0m [%d/%d] %s · %d с\n' \
      "$phase_current" "$phase_total" "$label" "$((SECONDS - started))"
    rm -f -- "$log"
    return 0
  else
    status=$?
  fi
  printf '\r\033[2K  \033[31m✗\033[0m [%d/%d] %s\n' \
    "$phase_current" "$phase_total" "$label" >&2
  printf 'Последние строки журнала:\n' >&2
  tail -n 80 -- "$log" >&2
  printf 'Полный журнал: %s\n' "$log" >&2
  return "$status"
}

if "$animate_update"; then
  printf '\n\033[1;36mLink-Bot\033[0m · обновление %s\n\n' "$LINK_BOT_VERSION"
fi

run_phase 'Подготовка базы данных' docker compose up -d --no-recreate db
run_phase 'Сборка новой версии' docker compose build bot
run_phase 'Подключение обновлений из Mini App' bash ./scripts/install-update-agent.sh
run_phase 'Перезапуск бота' docker compose up -d --force-recreate --no-deps bot

if [[ $mode == managed ]]; then
  run_phase 'Обновление веб-сервера' docker compose -p "$caddy_project" --profile standalone up -d --force-recreate --no-deps caddy
elif [[ $mode == new ]]; then
  run_phase 'Запуск веб-сервера' docker compose --profile standalone up -d --no-deps caddy
fi

if ! command -v curl >/dev/null 2>&1; then
  printf 'curl is required to verify the HTTPS endpoints.\n' >&2
  exit 1
fi

verify_https() {
  local host=$1 path=$2 label=$3 attempt last_error=""
  for ((attempt = 1; attempt <= 6; attempt++)); do
    if last_error=$(curl --noproxy '*' --silent --show-error --fail --output /dev/null --max-time 4 \
      --resolve "${host}:443:127.0.0.1" "https://${host}${path}" 2>&1); then
      printf '%s HTTPS is ready: https://%s%s\n' "$label" "$host" "$path"
      return 0
    fi
    if ((attempt < 6)); then sleep 3; fi
  done
  printf '%s HTTPS is not ready at https://%s%s.\n' "$label" "$host" "$path" >&2
  printf 'Last HTTPS check: %s\n' "$last_error" >&2
  printf 'Check application startup: docker logs --tail 100 link-bot\n' >&2
  printf 'Check proxy/TLS: docker logs --tail 100 link-bot-caddy\n' >&2
  return 1
}

run_phase 'Проверка HTTPS лендинга' verify_https "$public_host" / Landing
if [[ -n $cabinet_subdomain ]]; then
  run_phase 'Проверка HTTPS кабинета' verify_https "${cabinet_subdomain}.${public_host}" /mini-app/ Cabinet
fi
if [[ -n $admin_subdomain ]]; then
  run_phase 'Проверка HTTPS админки' verify_https "${admin_subdomain}.${public_host}" / Admin
fi