242 lines
9.3 KiB
Bash
242 lines
9.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Деплой по SSH: отдельно фронт (сборка + rsync dist), бэк (rsync + npm ci + prisma migrate).
|
|
#
|
|
# Требования: bash, ssh; на Linux/macOS используется rsync, на Git Bash/WIN — tar|ssh (обход проблем cwRsync с путами).
|
|
#
|
|
# Конфиг: переменные окружения или файл scripts/deploy.env (скопируйте из scripts/deploy.env.example).
|
|
#
|
|
# Примеры:
|
|
# ./scripts/deploy-ssh.sh --backend-only
|
|
# ./scripts/deploy-ssh.sh --frontend-only
|
|
# ./scripts/deploy-ssh.sh --all
|
|
# DEPLOY_HOST=10.0.0.5 ./scripts/deploy-ssh.sh -b
|
|
|
|
set -euo pipefail
|
|
|
|
# Git Bash вызывает Win32 ssh.exe: аргументы вроде /opt/... иначе подменяются → «mkdir: missing operand».
|
|
case "$(uname -s 2>/dev/null)" in
|
|
MINGW* | MSYS*) export MSYS2_ARG_CONV_EXCL="*" ;;
|
|
esac
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
# Загрузка deploy.env без перезаписи уже экспортированных переменных
|
|
if [[ -f "$SCRIPT_DIR/deploy.env" ]]; then
|
|
set -a
|
|
# shellcheck source=/dev/null
|
|
source "$SCRIPT_DIR/deploy.env"
|
|
set +a
|
|
fi
|
|
|
|
DEPLOY_HOST="${DEPLOY_HOST:-}"
|
|
DEPLOY_USER="${DEPLOY_USER:-root}"
|
|
DEPLOY_PATH="${DEPLOY_PATH:-/opt/craftshop}"
|
|
DEPLOY_FRONTEND_DIST="${DEPLOY_FRONTEND_DIST:-$DEPLOY_PATH/www}"
|
|
DEPLOY_SSH_IDENTITY="${DEPLOY_SSH_IDENTITY:-}"
|
|
DEPLOY_RESTART_CMD="${DEPLOY_RESTART_CMD:-}" # от root: systemctl restart craftshop-api; от непривилегированного: sudo …
|
|
# При SSH от root файлы после rsync оказываются root:root; если API в systemd под другим пользователем (bootstrap: deploy), нужен chown:
|
|
DEPLOY_SERVER_OWNER="${DEPLOY_SERVER_OWNER:-deploy}"
|
|
DEPLOY_SKIP_CHOWN="${DEPLOY_SKIP_CHOWN:-0}" # 1 — не вызывать chown (например API тоже под root)
|
|
|
|
RSYNC_OPTS=(-az --delete --human-readable --progress)
|
|
SSH_OPTS=()
|
|
if [[ -n "${DEPLOY_SSH_IDENTITY}" ]]; then
|
|
SSH_OPTS+=(-i "$DEPLOY_SSH_IDENTITY")
|
|
fi
|
|
|
|
REMOTE="${DEPLOY_USER}@${DEPLOY_HOST}"
|
|
SSH_BASE=(ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new "$REMOTE")
|
|
|
|
should_use_tar_transport() {
|
|
case "$(uname -s 2>/dev/null)" in
|
|
MINGW*|MSYS*|CYGWIN_NT*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Использование: $(basename "$0") [опции]
|
|
|
|
--frontend-only | -f Деплой только фронта (локальная сборка + rsync dist)
|
|
--backend-only | -b Деплой только бэкенда (rsync server + npm ci + prisma)
|
|
--all | -a Оба компонента (по умолчанию)
|
|
|
|
--dry-run Только показать команды rsync (без записи на сервер)
|
|
--skip-build Не вызывать npm run build (использовать текущий client/dist)
|
|
|
|
Окружение (или scripts/deploy.env):
|
|
DEPLOY_HOST хост SSH (обязательно)
|
|
DEPLOY_USER пользователь SSH (по умолчанию: root)
|
|
DEPLOY_PATH корень приложения на сервере (по умолчанию: /opt/craftshop)
|
|
DEPLOY_FRONTEND_DIST каталог под статику SPA (по умолчанию: \$DEPLOY_PATH/www)
|
|
DEPLOY_SSH_IDENTITY путь к приватному ключу (-i ssh)
|
|
DEPLOY_RESTART_CMD команда после бэкенд-деплоя (опционально; под root без sudo)
|
|
DEPLOY_SERVER_OWNER при DEPLOY_USER=root: владелец \$DEPLOY_PATH/server после деплоя (по умолчанию: deploy)
|
|
DEPLOY_SKIP_CHOWN 1 — не менять владельца каталога server (если процесс под root и т.п.)
|
|
USAGE
|
|
}
|
|
|
|
TARGET="all"
|
|
DRY_RUN=""
|
|
SKIP_BUILD=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--frontend-only | -f) TARGET="frontend" ;;
|
|
--backend-only | -b) TARGET="backend" ;;
|
|
--all | -a) TARGET="all" ;;
|
|
--dry-run) DRY_RUN=1 ;;
|
|
--skip-build) SKIP_BUILD=1 ;;
|
|
-h | --help) usage; exit 0 ;;
|
|
*) echo "Неизвестный аргумент: $1" >&2; usage >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ -z "$DEPLOY_HOST" ]]; then
|
|
echo "Укажите DEPLOY_HOST (или добавьте в scripts/deploy.env)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
remote_exec() {
|
|
"${SSH_BASE[@]}" "$@"
|
|
}
|
|
|
|
if [[ -n "$DRY_RUN" ]]; then
|
|
RSYNC_OPTS+=(--dry-run)
|
|
fi
|
|
|
|
# Строка для rsync -e (одна подкоманда; пути без пробелов в -i надёжнее)
|
|
build_rsync_rsh() {
|
|
printf '%q ' ssh "${SSH_OPTS[@]}" -o StrictHostKeyChecking=accept-new
|
|
}
|
|
|
|
deploy_backend() {
|
|
remote_exec mkdir -p "$DEPLOY_PATH/server"
|
|
|
|
if should_use_tar_transport; then
|
|
echo ">>> Бэкенд: tar|ssh → $REMOTE:$DEPLOY_PATH/server/"
|
|
if [[ -n "$DRY_RUN" ]]; then
|
|
echo "(dry-run) без передачи tar"
|
|
else
|
|
(
|
|
cd "$ROOT/server" || exit 1
|
|
tar -czf - \
|
|
--exclude=node_modules \
|
|
--exclude=uploads \
|
|
--exclude=.git \
|
|
--exclude='*.db' \
|
|
--exclude=.env \
|
|
--exclude=.dev_env \
|
|
.
|
|
) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_PATH}/server && tar xzf - -C ${DEPLOY_PATH}/server"
|
|
fi
|
|
else
|
|
echo ">>> Бэкенд: rsync → $REMOTE:$DEPLOY_PATH/server/"
|
|
local rsh
|
|
rsh="$(build_rsync_rsh)"
|
|
|
|
rsync "${RSYNC_OPTS[@]}" \
|
|
-e "$rsh" \
|
|
--exclude node_modules \
|
|
--exclude uploads \
|
|
--exclude .git \
|
|
--exclude '*.db' \
|
|
--exclude .env \
|
|
--exclude .dev_env \
|
|
"${ROOT}/server/" "${REMOTE}:${DEPLOY_PATH}/server/"
|
|
fi
|
|
|
|
if [[ -n "$DRY_RUN" ]]; then
|
|
echo "(dry-run) пропуск удалённых команд npm/prisma"
|
|
return 0
|
|
fi
|
|
|
|
echo ">>> Бэкенд: npm ci, prisma generate, migrate deploy на сервере"
|
|
remote_exec bash -lc "set -e
|
|
cd \"$DEPLOY_PATH/server\"
|
|
npm ci
|
|
npx prisma generate
|
|
npx prisma migrate deploy
|
|
"
|
|
if [[ "${DEPLOY_USER}" == "root" && "${DEPLOY_SKIP_CHOWN}" != "1" ]]; then
|
|
echo ">>> Права на серверный каталог: chown ${DEPLOY_SERVER_OWNER} (деплой от root)"
|
|
remote_exec chown -R "${DEPLOY_SERVER_OWNER}:${DEPLOY_SERVER_OWNER}" "$DEPLOY_PATH/server"
|
|
fi
|
|
if [[ -n "${DEPLOY_RESTART_CMD}" ]]; then
|
|
echo ">>> Рестарт: $DEPLOY_RESTART_CMD"
|
|
remote_exec bash -lc "$DEPLOY_RESTART_CMD"
|
|
elif [[ -z "$DRY_RUN" ]]; then
|
|
echo "(подсказка) задайте DEPLOY_RESTART_CMD, если нужен перезапуск сервиса"
|
|
fi
|
|
}
|
|
|
|
deploy_frontend() {
|
|
if [[ -z "$SKIP_BUILD" ]]; then
|
|
echo ">>> Фронт: npm ci и npm run build (локально)"
|
|
# Windows: ESLint/typescript-eslint тянут @unrs/*.node — npm ci часто получает EPERM unlink, если файл держит Node/IDE или остался мусор .resolver-binding-* после сбоя.
|
|
if should_use_tar_transport; then
|
|
echo ">>> (Windows/Git Bash) перед npm ci: снимаем блокировки нативных .node (@unrs, @rolldown, .resolver-binding-*)"
|
|
echo ">>> Подсказка: остановите «npm run dev» / dev-серверы и IDE, если EPERM останется."
|
|
rm -rf "$ROOT/client/node_modules/@unrs" "$ROOT/client/node_modules/@rolldown" 2>/dev/null || true
|
|
(
|
|
cd "$ROOT/client/node_modules" 2>/dev/null || exit 0
|
|
shopt -s nullglob
|
|
for x in ./.resolver-binding-*; do
|
|
[[ -d "$x" ]] && rm -rf "$x"
|
|
done
|
|
)
|
|
fi
|
|
(cd "$ROOT/client" && npm ci && npm run build) || {
|
|
echo "" >&2
|
|
echo "Сборка фронта не удалась. На Windows часто EPERM на .node — закройте процессы Node (dev-сервер), повторите." >&2
|
|
echo "Или соберите фронт вручную (cd client && npm run build), затем: $0 --frontend-only --skip-build" >&2
|
|
exit 1
|
|
}
|
|
else
|
|
echo ">>> Фронт: сборка пропущена (--skip-build)"
|
|
if [[ ! -d "$ROOT/client/dist" ]]; then
|
|
echo "Нет $ROOT/client/dist — выполните сборку без --skip-build" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
remote_exec mkdir -p "$DEPLOY_FRONTEND_DIST"
|
|
|
|
if should_use_tar_transport; then
|
|
echo ">>> Фронт: tar|ssh dist → $REMOTE:$DEPLOY_FRONTEND_DIST/"
|
|
if [[ -n "$DRY_RUN" ]]; then
|
|
echo "(dry-run) без передачи tar (www)"
|
|
else
|
|
remote_exec "mkdir -p ${DEPLOY_FRONTEND_DIST} && find ${DEPLOY_FRONTEND_DIST} -mindepth 1 -delete 2>/dev/null || true"
|
|
(
|
|
cd "$ROOT/client/dist" || exit 1
|
|
tar -czf - .
|
|
) | "${SSH_BASE[@]}" "mkdir -p ${DEPLOY_FRONTEND_DIST} && tar xzf - -C ${DEPLOY_FRONTEND_DIST}"
|
|
fi
|
|
else
|
|
echo ">>> Фронт: rsync dist → $REMOTE:$DEPLOY_FRONTEND_DIST/"
|
|
local rsh
|
|
rsh="$(build_rsync_rsh)"
|
|
rsync "${RSYNC_OPTS[@]}" \
|
|
-e "$rsh" \
|
|
"${ROOT}/client/dist/" "${REMOTE}:${DEPLOY_FRONTEND_DIST}/"
|
|
fi
|
|
|
|
echo ">>> Фронт готов (проверьте nginx/root на путь $DEPLOY_FRONTEND_DIST)"
|
|
}
|
|
|
|
case "$TARGET" in
|
|
backend) deploy_backend ;;
|
|
frontend) deploy_frontend ;;
|
|
all)
|
|
deploy_backend
|
|
deploy_frontend
|
|
;;
|
|
*) echo "internal: bad TARGET=$TARGET" >&2; exit 1 ;;
|
|
esac
|
|
|
|
echo "Готово."
|