#!/usr/bin/env bash # glchat: обновление стека (AGENT.md 9.3, 10.11). set -euo pipefail GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}" ENV_FILE="${GLCHAT_ETC_DIR}/.env" COMPOSE_FILE="${GLCHAT_ETC_DIR}/compose.yaml" REPO_DIR_DEFAULT="${GLCHAT_REPO_DIR:-/opt/glchat-src}" # app_check_url печатает адрес /readyz с учётом привязки приложения: во внешнем # режиме оно слушает WireGuard-адрес, и проверка localhost всегда падала бы. app_check_url() { local port="$1" host="${2:-}" case "$host" in 0.0.0.0 | "" | "::") host="127.0.0.1" ;; esac printf 'http://%s:%s/readyz' "$host" "$port" } log() { printf '[update] %s\n' "$*"; } die() { printf '[update] ошибка: %s\n' "$*" >&2 exit 1 } TARGET_VERSION="" CHANNEL="" while [ "$#" -gt 0 ]; do case "$1" in --version) TARGET_VERSION="${2:?}"; shift 2 ;; --channel) CHANNEL="${2:?}"; shift 2 ;; *) die "неизвестный аргумент: $1" ;; esac done [ -f "$ENV_FILE" ] || die "не найден ${ENV_FILE}" [ -f "$COMPOSE_FILE" ] || die "не найден ${COMPOSE_FILE}" # shellcheck disable=SC1090 set -a && . "$ENV_FILE" && set +a CURRENT="${APP_VERSION:-unknown}" TARGET="${TARGET_VERSION:-$CURRENT}" [ -n "$CHANNEL" ] && TARGET_CHANNEL="$CHANNEL" || TARGET_CHANNEL="${CHANNEL_NAME:-stable}" log "текущая версия: ${CURRENT} → целевая: ${TARGET} (канал ${TARGET_CHANNEL})" if [ "$CURRENT" = "$TARGET" ] && [ -z "${GLCHAT_FORCE_UPDATE:-}" ]; then log "версия не изменилась; для принудительного обновления задайте GLCHAT_FORCE_UPDATE=1" fi log "бэкап перед обновлением (обязательно)" GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/backup.sh" || die "бэкап не удался; обновление остановлено" compose_args=(-f "$COMPOSE_FILE") [ "${PROXY_MODE:-embedded}" = "embedded" ] && compose_args+=(--profile embedded-proxy) if [ "$APP_IMAGE" = "glchat:local" ] || [ -z "${APP_IMAGE:-}" ]; then REPO_DIR="${GLCHAT_REPO_DIR:-$REPO_DIR_DEFAULT}" if [ -d "${REPO_DIR}/.git" ]; then log "обновляем исходники в ${REPO_DIR}" # Без --depth: частичный fetch переводит обычный клон в shallow, и # следующий `pull --ff-only` падает с «Not possible to fast-forward». git -C "$REPO_DIR" fetch --tags origin || die "не удалось получить обновления; проверьте доступ к ${REPO_DIR}" if [ -n "$TARGET_VERSION" ]; then git -C "$REPO_DIR" checkout "$TARGET_VERSION" || die "не удалось переключиться на ${TARGET_VERSION}" else git -C "$REPO_DIR" pull --ff-only || die "локальные изменения мешают обновлению: git -C ${REPO_DIR} reset --hard origin/main" fi log "пересобираем образ" docker build -t "${APP_IMAGE:-glchat:local}" \ --build-arg "VERSION=${TARGET}" \ --build-arg "COMMIT=$(git -C "$REPO_DIR" rev-parse --short HEAD)" \ --build-arg "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "$REPO_DIR" || die "сборка не удалась" else die "каталог исходников ${REPO_DIR} не найден; выполните git clone или задайте GLCHAT_REPO_DIR" fi else log "скачиваем образ ${APP_IMAGE}" docker pull "$APP_IMAGE" || die "не удалось скачать образ" fi log "применяем новую версию" (cd "$GLCHAT_ETC_DIR" && docker compose "${compose_args[@]}" up -d --remove-orphans) || die "не удалось поднять стек" READY_URL="$(app_check_url "${APP_PORT:-8080}" "${GLCHAT_APP_BIND:-}")" log "ожидаем готовности ${READY_URL} (миграции выполняются на старте приложения)" for _ in $(seq 1 45); do if curl -fsS "$READY_URL" >/dev/null 2>&1; then log "приложение готово" break fi sleep 2 done if ! curl -fsS "$READY_URL" >/dev/null 2>&1; then die "приложение не поднялось; откат: glchat restore <бэкап>, затем docker compose up -d" fi if command -v sed >/dev/null 2>&1; then sed -i "s/^APP_VERSION=.*/APP_VERSION=${TARGET}/" "$ENV_FILE" sed -i "s/^CHANNEL_NAME=.*/CHANNEL_NAME=${TARGET_CHANNEL}/" "$ENV_FILE" 2>/dev/null || printf 'CHANNEL_NAME=%s\n' "$TARGET_CHANNEL" >>"$ENV_FILE" fi GLCHAT_ETC_DIR="$GLCHAT_ETC_DIR" bash "${GLCHAT_ETC_DIR}/smoke.sh" || log "smoke-тест с замечаниями" log "обновление завершено: ${TARGET}"