deploy
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#!/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: удалить client/node_modules/@unrs и мусор .resolver-binding-* (EPERM unlink)"
|
||||
rm -rf "$ROOT/client/node_modules/@unrs" 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)
|
||||
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 "Готово."
|
||||
Reference in New Issue
Block a user