diff --git a/.env.example b/.env.example index e03cf69..bd241ec 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,7 @@ ADMIN_IDS=comma_separated_admin_ids # # PostgreSQL Database Connection Settings POSTGRES_USER= # Required: database user name POSTGRES_PASSWORD= # Required: database password -POSTGRES_HOST=remnawave-tg-shop-db # Database container name +POSTGRES_HOST=remnawave-minishop-db # Database container name POSTGRES_PORT=5432 # Port POSTGRES_DB=postgres # Database name diff --git a/Caddyfile b/Caddyfile index b060d3f..0b1d726 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,10 +1,10 @@ # Replace the example domains below with your real webhook and Mini App hostnames. webhook.domain.com { encode zstd gzip - reverse_proxy remnawave-tg-shop:{$WEB_SERVER_PORT:8080} + reverse_proxy remnawave-minishop:{$WEB_SERVER_PORT:8080} } app.domain.com { encode zstd gzip - reverse_proxy remnawave-tg-shop:{$WEBAPP_SERVER_PORT:8081} + reverse_proxy remnawave-minishop:{$WEBAPP_SERVER_PORT:8081} } diff --git a/README.md b/README.md index cc4cb25..0838f9f 100644 --- a/README.md +++ b/README.md @@ -213,24 +213,24 @@ Remnawave Minishop — это Telegram-бот **и** Web App (Mini App) для Вам понадобится обратный прокси (например, Nginx) для обработки HTTPS-трафика и перенаправления запросов на контейнер с ботом. **Пути для перенаправления:** - - `https://<ваш_домен>/webhook/yookassa` → `http://remnawave-tg-shop:/webhook/yookassa` - - `https://<ваш_домен>/webhook/freekassa` → `http://remnawave-tg-shop:/webhook/freekassa` - - `https://<ваш_домен>/webhook/platega` → `http://remnawave-tg-shop:/webhook/platega` - - `https://<ваш_домен>/webhook/severpay` → `http://remnawave-tg-shop:/webhook/severpay` - - `https://<ваш_домен>/webhook/cryptopay` → `http://remnawave-tg-shop:/webhook/cryptopay` - - `https://<ваш_домен>/webhook/panel` → `http://remnawave-tg-shop:/webhook/panel` + - `https://<ваш_домен>/webhook/yookassa` → `http://remnawave-minishop:/webhook/yookassa` + - `https://<ваш_домен>/webhook/freekassa` → `http://remnawave-minishop:/webhook/freekassa` + - `https://<ваш_домен>/webhook/platega` → `http://remnawave-minishop:/webhook/platega` + - `https://<ваш_домен>/webhook/severpay` → `http://remnawave-minishop:/webhook/severpay` + - `https://<ваш_домен>/webhook/cryptopay` → `http://remnawave-minishop:/webhook/cryptopay` + - `https://<ваш_домен>/webhook/panel` → `http://remnawave-minishop:/webhook/panel` - **Для Telegram:** Бот автоматически установит вебхук, если в `.env` указан `WEBHOOK_BASE_URL`. Путь будет `https://<ваш_домен>/`. - Где `remnawave-tg-shop` — это имя сервиса из `docker-compose.yml`, а `` — порт, указанный в `.env`. + Где `remnawave-minishop` — это имя сервиса из `docker-compose.yml`, а `` — порт, указанный в `.env`. **Отдельный порт Web App:** - - `https://<домен_web_app>/` → `http://remnawave-tg-shop:/` + - `https://<домен_web_app>/` → `http://remnawave-minishop:/` Web App не должен проксироваться на `WEB_SERVER_PORT`: этот порт оставьте для Telegram, платежных и Remnawave webhooks. 6. **Просмотр логов:** ```bash - docker compose logs -f remnawave-tg-shop + docker compose logs -f remnawave-minishop ``` > 💡 Если включена проверка подписки на канал (`REQUIRED_CHANNEL_ID`), добавьте бота администратором в этот канал. Пользователь увидит кнопку «Проверить подписку», и, после первого успешного подтверждения, дальнейшие действия блокироваться не будут. @@ -270,8 +270,8 @@ Web App запускается в том же контейнере, что и б 3. Проксируйте отдельный домен или location на порт Web App: ```nginx - upstream remnawave-tg-shop-webapp { - server remnawave-tg-shop:8081; + upstream remnawave-minishop-webapp { + server remnawave-minishop:8081; } server { @@ -283,7 +283,7 @@ Web App запускается в том же контейнере, что и б ssl_certificate_key "/etc/nginx/ssl/app_privkey.key"; location / { - proxy_pass http://remnawave-tg-shop-webapp; + proxy_pass http://remnawave-minishop-webapp; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -352,8 +352,8 @@ cd /opt/remnawave/nginx && nano nginx.conf Добавьте в `nginx.conf` следующую конфигурацию: ```nginx -upstream remnawave-tg-shop { - server remnawave-tg-shop:8080; +upstream remnawave-minishop { + server remnawave-minishop:8080; } map $http_upgrade $connection_upgrade { @@ -385,7 +385,7 @@ server { error_page 400 404 500 502 @redirect; location / { - proxy_pass http://remnawave-tg-shop$request_uri; + proxy_pass http://remnawave-minishop$request_uri; } location @redirect { @@ -491,12 +491,12 @@ IMAGE_TAG=3.0.0 docker compose -f docker-compose-remote-server.yml up -d ```caddyfile webhook.domain.com { encode zstd gzip - reverse_proxy remnawave-tg-shop:{$WEB_SERVER_PORT:8080} + reverse_proxy remnawave-minishop:{$WEB_SERVER_PORT:8080} } app.domain.com { encode zstd gzip - reverse_proxy remnawave-tg-shop:{$WEBAPP_SERVER_PORT:8081} + reverse_proxy remnawave-minishop:{$WEBAPP_SERVER_PORT:8081} } ``` @@ -514,6 +514,10 @@ docker compose -f docker-compose-caddy.yml up -d --build После этого Caddy сам выпустит сертификаты и будет проксировать webhook'и на порт `8080`, а Mini App - на `8081`. +## 🔄 Миграция с `remnawave-tg-shop` на `remnawave-minishop` + +Короткая инструкция, автоматический запуск helper'а из `raw` и ручной вариант переноса вынесены в отдельный документ: [docs/migration-to-minishop.md](docs/migration-to-minishop.md). + ## 📁 Структура проекта ``` @@ -540,7 +544,7 @@ docker compose -f docker-compose-caddy.yml up -d --build │ ├── migrator.py # Миграции схемы при старте │ └── models.py # Модели SQLAlchemy ├── locales/ # Файлы локализации (ru.json, en.json) -├── scripts/ # Сборка JS Web App и обновление копий Telegram JS +├── scripts/ # Сборка JS Web App, обновление копий Telegram JS и миграционный helper ├── tests/ # Pytest-тесты ├── .env.example # Пример файла с переменными окружения ├── Caddyfile # Пример конфигурации Caddy diff --git a/docker-compose-caddy.yml b/docker-compose-caddy.yml index dcfd79e..3fe359c 100644 --- a/docker-compose-caddy.yml +++ b/docker-compose-caddy.yml @@ -1,8 +1,8 @@ services: - remnawave-tg-shop: + remnawave-minishop: build: . - container_name: remnawave-tg-shop - hostname: remnawave-tg-shop + container_name: remnawave-minishop + hostname: remnawave-minishop env_file: - .env environment: @@ -11,19 +11,19 @@ services: - ./locales:/app/locales restart: unless-stopped depends_on: - remnawave-tg-shop-db: + remnawave-minishop-db: condition: service_healthy - remnawave-tg-shop-db: + remnawave-minishop-db: image: postgres:17 - container_name: remnawave-tg-shop-db - hostname: remnawave-tg-shop-db + container_name: remnawave-minishop-db + hostname: remnawave-minishop-db env_file: - .env environment: - TZ=UTC volumes: - - remnawave-tg-shop-db-data:/var/lib/postgresql/data + - remnawave-minishop-db-data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] @@ -33,14 +33,14 @@ services: caddy: image: caddy:2-alpine - container_name: remnawave-tg-shop-caddy - hostname: remnawave-tg-shop-caddy + container_name: remnawave-minishop-caddy + hostname: remnawave-minishop-caddy environment: - TZ=UTC - WEB_SERVER_PORT=${WEB_SERVER_PORT:-8080} - WEBAPP_SERVER_PORT=${WEBAPP_SERVER_PORT:-8081} depends_on: - - remnawave-tg-shop + - remnawave-minishop ports: - "80:80" - "443:443" @@ -51,9 +51,9 @@ services: restart: unless-stopped volumes: - remnawave-tg-shop-db-data: - name: remnawave-tg-shop-db-data + remnawave-minishop-db-data: + name: remnawave-minishop-db-data caddy_data: - name: remnawave-tg-shop-caddy-data + name: remnawave-minishop-caddy-data caddy_config: - name: remnawave-tg-shop-caddy-config + name: remnawave-minishop-caddy-config diff --git a/docker-compose-remote-server.yml b/docker-compose-remote-server.yml index 4837fa3..9b9fd9b 100644 --- a/docker-compose-remote-server.yml +++ b/docker-compose-remote-server.yml @@ -1,8 +1,8 @@ services: - remnawave-tg-shop: + remnawave-minishop: image: ghcr.io/3252a8/remnawave-tg-shop:${IMAGE_TAG:-3.0.0} - container_name: remnawave-tg-shop - hostname: remnawave-tg-shop + container_name: remnawave-minishop + hostname: remnawave-minishop env_file: - .env ports: @@ -16,17 +16,17 @@ services: - ./locales:/app/locales restart: unless-stopped depends_on: - - remnawave-tg-shop-db - remnawave-tg-shop-db: + - remnawave-minishop-db + remnawave-minishop-db: image: postgres:17 - container_name: remnawave-tg-shop-db - hostname: remnawave-tg-shop-db + container_name: remnawave-minishop-db + hostname: remnawave-minishop-db env_file: - .env environment: - TZ=UTC volumes: - - remnawave-tg-shop-db-data:/var/lib/postgresql/data + - remnawave-minishop-db-data:/var/lib/postgresql/data networks: - remnawave-network restart: unless-stopped @@ -38,5 +38,5 @@ services: networks: remnawave-network: null volumes: - remnawave-tg-shop-db-data: - name: remnawave-tg-shop-db-data + remnawave-minishop-db-data: + name: remnawave-minishop-db-data diff --git a/docker-compose.yml b/docker-compose.yml index 4539c56..71bddab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,15 @@ services: - remnawave-tg-shop: + remnawave-minishop: build: . - container_name: remnawave-tg-shop - hostname: remnawave-tg-shop + container_name: remnawave-minishop + hostname: remnawave-minishop env_file: - .env networks: - remnawave-network restart: unless-stopped depends_on: - remnawave-tg-shop-db: + remnawave-minishop-db: condition: service_healthy ports: - 127.0.0.1:8080:8080 @@ -21,14 +21,14 @@ services: retries: 5 start_period: 30s - remnawave-tg-shop-db: + remnawave-minishop-db: image: postgres:17 - container_name: remnawave-tg-shop-db - hostname: remnawave-tg-shop-db + container_name: remnawave-minishop-db + hostname: remnawave-minishop-db env_file: - .env volumes: - - remnawave-tg-shop-db-data:/var/lib/postgresql/data + - remnawave-minishop-db-data:/var/lib/postgresql/data networks: - remnawave-network restart: unless-stopped @@ -43,5 +43,5 @@ networks: external: true volumes: - remnawave-tg-shop-db-data: - name: remnawave-tg-shop-db-data + remnawave-minishop-db-data: + name: remnawave-minishop-db-data diff --git a/docs/migration-to-minishop.md b/docs/migration-to-minishop.md new file mode 100644 index 0000000..ef81d6d --- /dev/null +++ b/docs/migration-to-minishop.md @@ -0,0 +1,137 @@ +# Миграция с `remnawave-tg-shop` на `remnawave-minishop` + +Начиная с этой версии контейнеры и тома названы `remnawave-minishop*` вместо `remnawave-tg-shop*`. Старый и новый стеки используют **разные имена томов**, поэтому простой `docker compose up -d` после `git pull` создаст пустую БД. Эта инструкция описывает, как перенести данные. + +Есть два пути: + +- [Автоматический](#автоматический-способ-через-скрипт) — один скрипт, идемпотентный, проверяет состояние на каждом шаге. +- [Ручной](#ручной-способ) — команды, которые делает скрипт, если хочется понимать происходящее или выполнить выборочно. + +В обоих случаях: +- старые тома **не удаляются** автоматически — это безопасный бэкап на случай отката; +- сертификаты Caddy (если используется `docker-compose-caddy.yml`) тоже переносятся, чтобы Let's Encrypt не выписывал их заново и не упереться в rate limit. + +## Автоматический способ (через скрипт) + +Если helper ещё не лежит у вас локально, запускайте его прямо из `raw` из корня старого репозитория: + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) +``` + +> Команда выше рассчитана на `bash` / Git Bash / WSL. Если вы запускаете из PowerShell, удобнее сначала открыть Git Bash. + +Если вы уже подтянули новую версию и файл есть локально, можно запускать и так: + +```bash +bash scripts/migrate_to_minishop.sh +``` + +По умолчанию скрипт работает с `docker-compose.yml` и переключается на ветку `main`. Можно переопределить через переменные окружения: + +| Переменная | Назначение | По умолчанию | +| ----------------- | ----------------------------------------------------------------------- | ---------------------- | +| `PROJECT_ROOT` | Явный путь к корню старого репозитория, если запуск не из него | текущая директория | +| `COMPOSE_FILE` | Какой compose-файл стартовать в конце | `docker-compose.yml` | +| `TARGET_BRANCH` | На какую ветку переключаться и подтягивать обновления | `main` | +| `GIT_REMOTE` | Какой remote использовать для `fetch`/`pull` | `origin` | +| `NEW_ORIGIN_URL` | Если задано и не совпадает с URL выбранного remote — он будет обновлён | (не меняется) | +| `ASSUME_YES` | `1` — не задавать интерактивных вопросов | `0` | + +Примеры: + +```bash +# Caddy-вариант из raw +COMPOSE_FILE=docker-compose-caddy.yml \ + bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) + +# С переключением origin на форк 3252a8 +NEW_ORIGIN_URL=https://github.com/3252a8/remnawave-minishop.git \ + bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) + +# Без интерактива +ASSUME_YES=1 \ + bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh) +``` + +Что делает скрипт: + +1. **Останавливает текущий стек**: проверяет известные контейнеры старой и новой схемы и останавливает их, если они запущены. +2. **Переключает `origin`**, если задана переменная `NEW_ORIGIN_URL`, иначе оставляет как есть. +3. **Подтягивает целевую ветку** (`git fetch` + `git switch` + `git pull --ff-only`). Прерывается, если в рабочем дереве есть незакоммиченные изменения. +4. **Переносит тома** `remnawave-tg-shop-*` → `remnawave-minishop-*` через одноразовый `alpine`-контейнер. Если новый том уже непустой, копирование пропускается. Заодно правится `POSTGRES_HOST` в `.env` (с бэкапом в `.env.bak`). +5. **Стартует новый стек** (`docker compose -f $COMPOSE_FILE up -d --remove-orphans`, а для локальной сборки ещё и `--build`) и печатает `docker compose ps`. + +Скрипт идемпотентен: повторный запуск ничего не сломает, просто пропустит уже выполненные шаги. + +После того как убедитесь, что бот работает и данные на месте, удалите старые тома: + +```bash +docker volume rm remnawave-tg-shop-db-data +docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true +``` + +> ⚠️ Если у вас есть внешний reverse proxy (Nginx и т.п.), не забудьте поправить в его конфиге `upstream`/`proxy_pass`: имя хоста контейнера изменилось с `remnawave-tg-shop` на `remnawave-minishop`. Скрипт не трогает внешние конфиги. + +## Ручной способ + +1. **Остановите старый стек и обновите код:** + + ```bash + docker compose down + git fetch origin + git checkout main + git pull --ff-only origin main + ``` + +2. **Перенесите том БД в новое имя:** + + ```bash + docker volume create remnawave-minishop-db-data + docker run --rm \ + -v remnawave-tg-shop-db-data:/from:ro \ + -v remnawave-minishop-db-data:/to \ + alpine sh -c "cd /from && cp -a . /to" + ``` + +3. **Обновите `.env`:** + + ```bash + sed -i.bak 's/^POSTGRES_HOST=remnawave-tg-shop-db$/POSTGRES_HOST=remnawave-minishop-db/' .env + ``` + +4. **(Только для Caddy)** перенесите тома Caddy с TLS-сертификатами и состоянием ACME: + + ```bash + for v in caddy-data caddy-config; do + docker volume create "remnawave-minishop-$v" + docker run --rm \ + -v "remnawave-tg-shop-$v":/from:ro \ + -v "remnawave-minishop-$v":/to \ + alpine sh -c "cd /from && cp -a . /to" + done + ``` + +5. **Запустите новый стек:** + + ```bash + docker compose up -d + # или + docker compose -f docker-compose-caddy.yml up -d --build + # или + docker compose -f docker-compose-remote-server.yml up -d + ``` + +6. **Проверьте:** + + ```bash + docker compose ps + docker compose logs -f remnawave-minishop + ``` + +7. **(Опционально) удалите старые тома**, когда убедитесь, что новый стек стабилен: + + ```bash + docker volume rm remnawave-tg-shop-db-data + docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true + ``` diff --git a/scripts/migrate_to_minishop.sh b/scripts/migrate_to_minishop.sh new file mode 100644 index 0000000..08a6eea --- /dev/null +++ b/scripts/migrate_to_minishop.sh @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="" + +OLD_PREFIX="remnawave-tg-shop" +NEW_PREFIX="remnawave-minishop" +OLD_DB_VOLUME="${OLD_PREFIX}-db-data" +NEW_DB_VOLUME="${NEW_PREFIX}-db-data" +OLD_CADDY_VOLUMES=("${OLD_PREFIX}-caddy-data" "${OLD_PREFIX}-caddy-config") +NEW_CADDY_VOLUMES=("${NEW_PREFIX}-caddy-data" "${NEW_PREFIX}-caddy-config") +KNOWN_CONTAINERS=( + "${OLD_PREFIX}" + "${OLD_PREFIX}-db" + "${OLD_PREFIX}-caddy" + "${NEW_PREFIX}" + "${NEW_PREFIX}-db" + "${NEW_PREFIX}-caddy" +) + +log() { + printf '%s\n' "$*" +} + +die() { + printf 'Ошибка: %s\n' "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "Не найдено обязательное средство \`$1\` в PATH." +} + +resolve_root() { + if [[ -n "${PROJECT_ROOT:-}" ]]; then + [[ -d "$PROJECT_ROOT" ]] || die "PROJECT_ROOT не существует: $PROJECT_ROOT" + (cd -- "$PROJECT_ROOT" >/dev/null && pwd -P) + return + fi + + local git_root + if git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "$git_root" + return + fi + + pwd -P +} + +run() { + log "+ $*" + "$@" +} + +container_exists() { + docker inspect "$1" >/dev/null 2>&1 +} + +container_running() { + [[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" == "true" ]] +} + +stop_container() { + local name="$1" + + if ! container_exists "$name"; then + return 1 + fi + + if container_running "$name"; then + docker stop "$name" >/dev/null + fi + docker rm "$name" >/dev/null +} + +volume_exists() { + docker volume inspect "$1" >/dev/null 2>&1 +} + +volume_is_empty() { + docker run --rm -v "$1:/data" alpine sh -c 'test -z "$(find /data -mindepth 1 -print -quit)"' >/dev/null 2>&1 +} + +copy_volume() { + local source="$1" + local target="$2" + + if ! volume_exists "$source"; then + log " - Пропускаю том \`$source\`: исходный том не найден." + return 1 + fi + + if volume_exists "$target" && ! volume_is_empty "$target"; then + log " - Пропускаю том \`$target\`: он уже не пустой." + return 1 + fi + + docker volume create "$target" >/dev/null + docker run --rm -v "$source:/from:ro" -v "$target:/to" alpine sh -c 'cd /from && cp -a . /to/' +} + +is_old_postgres_host() { + grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${OLD_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env" +} + +is_new_postgres_host() { + grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${NEW_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env" +} + +update_postgres_host() { + if is_old_postgres_host; then + sed -i.bak -E "s|^([[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*)${OLD_PREFIX}-db([[:space:]]*(#.*)?)$|\\1${NEW_PREFIX}-db\\2|" "$ROOT/.env" + log " - \`.env\` обновлён, резервная копия сохранена в \`.env.bak\`." + elif is_new_postgres_host; then + log " - \`POSTGRES_HOST\` уже указывает на новый контейнер, ничего менять не нужно." + else + log " - \`POSTGRES_HOST\` не похож на старую схему, пропускаю изменение." + fi +} + +main() { + require_cmd git + require_cmd docker + docker info >/dev/null + + ROOT="$(resolve_root)" + + local compose_file="${COMPOSE_FILE:-docker-compose.yml}" + local target_branch="${TARGET_BRANCH:-main}" + local git_remote="${GIT_REMOTE:-origin}" + local new_origin_url="${NEW_ORIGIN_URL:-}" + local assume_yes="${ASSUME_YES:-0}" + local current_origin + local current_branch + local remote_ref + local head_commit + local compose_has_build=0 + local compose_has_caddy=0 + local -a compose_cmd + local -a running_containers=() + local -a summary=() + local -a up_args + local name + local source + local target + local answer + + if [[ $compose_file != /* ]]; then + compose_file="$ROOT/$compose_file" + fi + [[ -f "$compose_file" ]] || die "Compose-файл не найден: $compose_file" + [[ -e "$ROOT/.git" ]] || die "Скрипт нужно запускать из корня git-репозитория." + [[ -f "$ROOT/.env" ]] || die "Не найден \`.env\` в корне репозитория." + + if docker compose version >/dev/null 2>&1; then + compose_cmd=(docker compose) + elif command -v docker-compose >/dev/null 2>&1; then + compose_cmd=(docker-compose) + else + die "Не найден ни \`docker compose\`, ни \`docker-compose\`." + fi + + if [[ -n "$(git -C "$ROOT" status --porcelain=v1)" ]]; then + die "В рабочем дереве есть незакоммиченные изменения. Сначала сохраните их, чтобы миграция не затёрла чужие правки." + fi + + if grep -Eq '^[[:space:]]*build:[[:space:]]*' "$compose_file"; then + compose_has_build=1 + fi + if grep -Eq '^[[:space:]]*caddy:[[:space:]]*$' "$compose_file"; then + compose_has_caddy=1 + fi + + for name in "${KNOWN_CONTAINERS[@]}"; do + if container_exists "$name"; then + running_containers+=("$name") + fi + done + + current_origin="$(git -C "$ROOT" remote get-url "$git_remote")" + if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then + summary+=( "обновить $git_remote с \`$current_origin\` на \`$new_origin_url\`" ) + fi + summary+=( "скачать ветку \`$target_branch\` из \`$git_remote\`" ) + if volume_exists "$OLD_DB_VOLUME"; then + summary+=( "проверить том БД \`$OLD_DB_VOLUME\` и перенести в \`$NEW_DB_VOLUME\` при необходимости" ) + fi + if ((compose_has_caddy)); then + for i in 0 1; do + source="${OLD_CADDY_VOLUMES[$i]}" + target="${NEW_CADDY_VOLUMES[$i]}" + if volume_exists "$source"; then + summary+=( "проверить том \`$source\` и перенести в \`$target\` при необходимости" ) + fi + done + fi + if is_old_postgres_host; then + summary+=( "обновить \`POSTGRES_HOST\` в \`.env\`" ) + fi + summary+=( "запустить compose-файл \`$(basename "$compose_file")\`" ) + + if [[ "$assume_yes" != "1" ]]; then + if [[ ! -t 0 ]]; then + die "Скрипт ожидает интерактивное подтверждение. Запустите с \`ASSUME_YES=1\` для неинтерактивного режима." + fi + log "План миграции:" + for name in "${summary[@]}"; do + log " - $name" + done + read -r -p "Продолжить? [y/N]: " answer + case "$answer" in + y|Y|yes|YES|Yes) + ;; + *) + die "Миграция отменена пользователем." + ;; + esac + fi + + log "1. Останавливаю старый стек" + if ((${#running_containers[@]})); then + for name in "${running_containers[@]}"; do + stop_container "$name" + log " - контейнер \`$name\` остановлен/удалён" + done + else + log " - запущенных контейнеров старой схемы не найдено" + fi + + if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then + log "2. Обновляю origin" + run git -C "$ROOT" remote set-url "$git_remote" "$new_origin_url" + else + log "2. Origin уже актуален, пропускаю" + fi + + log "3. Обновляю git до ветки \`$target_branch\`" + run git -C "$ROOT" fetch "$git_remote" "$target_branch" + + current_branch="$(git -C "$ROOT" branch --show-current || true)" + if [[ -z "$current_branch" ]]; then + current_branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)" + fi + + if [[ "$current_branch" != "$target_branch" ]]; then + if git -C "$ROOT" show-ref --verify --quiet "refs/heads/$target_branch"; then + run git -C "$ROOT" switch "$target_branch" + else + run git -C "$ROOT" switch -c "$target_branch" --track "$git_remote/$target_branch" + fi + else + log " - уже на ветке \`$target_branch\`" + fi + + remote_ref="$(git -C "$ROOT" rev-parse "$git_remote/$target_branch")" + head_commit="$(git -C "$ROOT" rev-parse HEAD)" + if [[ "$head_commit" != "$remote_ref" ]]; then + run git -C "$ROOT" pull --ff-only "$git_remote" "$target_branch" + else + log " - локальная ветка уже совпадает с удалённой, \`git pull\` не нужен" + fi + + log "4. Переношу тома" + if copy_volume "$OLD_DB_VOLUME" "$NEW_DB_VOLUME"; then + log " - БД перенесена в \`$NEW_DB_VOLUME\`" + fi + if ((compose_has_caddy)); then + for i in 0 1; do + source="${OLD_CADDY_VOLUMES[$i]}" + target="${NEW_CADDY_VOLUMES[$i]}" + if copy_volume "$source" "$target"; then + log " - \`$source\` перенесён в \`$target\`" + fi + done + fi + + log "5. Обновляю \`.env\`" + update_postgres_host + + log "6. Запускаю новый стек" + if ((compose_has_build)); then + up_args=(up -d --build --remove-orphans) + else + up_args=(up -d --remove-orphans) + fi + run "${compose_cmd[@]}" -f "$compose_file" "${up_args[@]}" + run "${compose_cmd[@]}" -f "$compose_file" ps + + log "Готово." +} + +main "$@"