deploy
This commit is contained in:
261
deploy.sh
Normal file
261
deploy.sh
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Hockey Web / Stat2TV deploy
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
REPO_URL="https://git.tvstart.ru/ychernenko/hockey_new.git"
|
||||||
|
TARGET_DIR="/root/KHL"
|
||||||
|
SERVICE_NAME="khl-data.service"
|
||||||
|
ENV_FILE="/mnt/khl/.env"
|
||||||
|
PORT="8000"
|
||||||
|
RELEASE="main"
|
||||||
|
|
||||||
|
log() { printf '\033[0;32m[INFO]\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*" >&2; }
|
||||||
|
err() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Использование:
|
||||||
|
sudo $0 [-r branch|tag|commit]
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
sudo $0 -r main
|
||||||
|
sudo $0 -r Barabanov_TEST
|
||||||
|
sudo $0 -r v1.2.3
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while getopts ":r:h" opt; do
|
||||||
|
case "$opt" in
|
||||||
|
r) RELEASE="$OPTARG" ;;
|
||||||
|
h) usage; exit 0 ;;
|
||||||
|
:) err "Для -$OPTARG требуется значение"; exit 2 ;;
|
||||||
|
\?) err "Неизвестный параметр: -$OPTARG"; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
|
||||||
|
err "Запусти deploy от root: sudo $0 -r $RELEASE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_system_dependencies() {
|
||||||
|
local missing=()
|
||||||
|
for cmd in git python3 curl systemctl; do
|
||||||
|
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
|
||||||
|
done
|
||||||
|
|
||||||
|
if ! python3 -m venv --help >/dev/null 2>&1; then
|
||||||
|
missing+=("python3-venv")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((${#missing[@]} > 0)); then
|
||||||
|
log "Устанавливаю системные зависимости: ${missing[*]}"
|
||||||
|
apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||||
|
git python3 python3-venv python3-pip curl ca-certificates
|
||||||
|
else
|
||||||
|
log "Системные зависимости уже установлены"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_env() {
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
err "Не найден файл окружения: $ENV_FILE"
|
||||||
|
err "Создай его до deploy. Минимально нужны HOCKEY_DATABASE_URL и WFL_DATABASE_URL."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod 600 "$ENV_FILE" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
checkout_release() {
|
||||||
|
if [[ -d "$TARGET_DIR/.git" ]]; then
|
||||||
|
log "Обновляю существующий репозиторий в $TARGET_DIR"
|
||||||
|
git -C "$TARGET_DIR" remote set-url origin "$REPO_URL"
|
||||||
|
git -C "$TARGET_DIR" fetch origin --prune --tags
|
||||||
|
else
|
||||||
|
log "Клонирую $REPO_URL -> $TARGET_DIR"
|
||||||
|
mkdir -p "$(dirname "$TARGET_DIR")"
|
||||||
|
git clone "$REPO_URL" "$TARGET_DIR"
|
||||||
|
git -C "$TARGET_DIR" fetch origin --prune --tags
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ветка origin/<release>
|
||||||
|
if git -C "$TARGET_DIR" show-ref --verify --quiet "refs/remotes/origin/$RELEASE"; then
|
||||||
|
log "Deploy ветки: $RELEASE"
|
||||||
|
git -C "$TARGET_DIR" checkout -B "$RELEASE" "origin/$RELEASE"
|
||||||
|
git -C "$TARGET_DIR" reset --hard "origin/$RELEASE"
|
||||||
|
|
||||||
|
# Git tag
|
||||||
|
elif git -C "$TARGET_DIR" show-ref --verify --quiet "refs/tags/$RELEASE"; then
|
||||||
|
log "Deploy тега: $RELEASE"
|
||||||
|
git -C "$TARGET_DIR" checkout --detach "refs/tags/$RELEASE"
|
||||||
|
|
||||||
|
# SHA / локально разрешимый commit-ish
|
||||||
|
elif git -C "$TARGET_DIR" rev-parse --verify --quiet "$RELEASE^{commit}" >/dev/null; then
|
||||||
|
log "Deploy commit: $RELEASE"
|
||||||
|
git -C "$TARGET_DIR" checkout --detach "$RELEASE"
|
||||||
|
|
||||||
|
else
|
||||||
|
err "Не найдена ветка, тег или commit: $RELEASE"
|
||||||
|
echo ""
|
||||||
|
echo "Удалённые ветки:"
|
||||||
|
git -C "$TARGET_DIR" branch -r || true
|
||||||
|
echo ""
|
||||||
|
echo "Теги:"
|
||||||
|
git -C "$TARGET_DIR" tag -l | tail -50 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Удаляем только обычные untracked-файлы. Игнорируемые .env/settings не трогаются.
|
||||||
|
git -C "$TARGET_DIR" clean -fd
|
||||||
|
|
||||||
|
log "Commit: $(git -C "$TARGET_DIR" log -1 --oneline)"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_project_files() {
|
||||||
|
local required=("app.py" "requirements.txt" "check_remote_database.py" "hockey_data/__init__.py" "ui_builder/__init__.py" "khl_site/khl_data_center.py")
|
||||||
|
local f
|
||||||
|
for f in "${required[@]}"; do
|
||||||
|
if [[ ! -f "$TARGET_DIR/$f" ]]; then
|
||||||
|
err "В релизе отсутствует обязательный файл: $f"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_env_link() {
|
||||||
|
# Приложение само читает .env из корня проекта. Оставляем секреты вне Git,
|
||||||
|
# но делаем ссылку, чтобы одинаково работал systemd и ручной запуск.
|
||||||
|
if [[ -e "$TARGET_DIR/.env" && ! -L "$TARGET_DIR/.env" ]]; then
|
||||||
|
warn "$TARGET_DIR/.env уже существует обычным файлом — оставляю его как есть"
|
||||||
|
else
|
||||||
|
ln -sfn "$ENV_FILE" "$TARGET_DIR/.env"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_venv() {
|
||||||
|
if [[ ! -x "$TARGET_DIR/.venv/bin/python" ]]; then
|
||||||
|
log "Создаю virtualenv"
|
||||||
|
python3 -m venv "$TARGET_DIR/.venv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Обновляю Python-зависимости"
|
||||||
|
"$TARGET_DIR/.venv/bin/python" -m pip install --upgrade pip setuptools wheel
|
||||||
|
"$TARGET_DIR/.venv/bin/python" -m pip install -r "$TARGET_DIR/requirements.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_code() {
|
||||||
|
log "Проверяю Python-код"
|
||||||
|
"$TARGET_DIR/.venv/bin/python" -m py_compile \
|
||||||
|
"$TARGET_DIR/app.py" \
|
||||||
|
"$TARGET_DIR/check_remote_database.py"
|
||||||
|
|
||||||
|
# Проверка БД тем же кодом, который использует проект.
|
||||||
|
log "Проверяю PostgreSQL"
|
||||||
|
(
|
||||||
|
cd "$TARGET_DIR"
|
||||||
|
"$TARGET_DIR/.venv/bin/python" "$TARGET_DIR/check_remote_database.py"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
write_systemd_service() {
|
||||||
|
log "Создаю systemd unit: /etc/systemd/system/$SERVICE_NAME"
|
||||||
|
|
||||||
|
cat > "/etc/systemd/system/$SERVICE_NAME" <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Hockey Stat2TV / UI Builder
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
StartLimitIntervalSec=300
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=root
|
||||||
|
WorkingDirectory=$TARGET_DIR
|
||||||
|
EnvironmentFile=$ENV_FILE
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
Environment=PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Перед стартом убеждаемся, что PostgreSQL доступен.
|
||||||
|
ExecStartPre=$TARGET_DIR/.venv/bin/python $TARGET_DIR/check_remote_database.py
|
||||||
|
|
||||||
|
# ГЛАВНАЯ КОМАНДА ЗАПУСКА ПРОЕКТА:
|
||||||
|
ExecStart=$TARGET_DIR/.venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port $PORT
|
||||||
|
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStopSec=20
|
||||||
|
KillSignal=SIGINT
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=KHL
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
chmod 644 "/etc/systemd/system/$SERVICE_NAME"
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "$SERVICE_NAME" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_service() {
|
||||||
|
log "Перезапускаю $SERVICE_NAME"
|
||||||
|
systemctl restart "$SERVICE_NAME"
|
||||||
|
|
||||||
|
log "Жду HTTP на 127.0.0.1:$PORT"
|
||||||
|
local ok=0
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
if curl -fsS --max-time 2 "http://127.0.0.1:$PORT/api/hockey/database/status" >/tmp/khl-health.json 2>/dev/null; then
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$ok" -ne 1 ]]; then
|
||||||
|
err "Приложение не прошло HTTP-проверку"
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager -l || true
|
||||||
|
echo ""
|
||||||
|
journalctl -u "$SERVICE_NAME" -n 100 --no-pager || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "HTTP OK: $(cat /tmp/khl-health.json)"
|
||||||
|
rm -f /tmp/khl-health.json
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
log "Deploy Hockey: release=$RELEASE"
|
||||||
|
install_system_dependencies
|
||||||
|
check_env
|
||||||
|
checkout_release
|
||||||
|
check_project_files
|
||||||
|
|
||||||
|
# Не меняем активный venv, пока код ещё не скачан/проверен по структуре.
|
||||||
|
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||||
|
|
||||||
|
prepare_env_link
|
||||||
|
prepare_venv
|
||||||
|
validate_code
|
||||||
|
write_systemd_service
|
||||||
|
restart_service
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log "Deploy завершён"
|
||||||
|
log "Service: $SERVICE_NAME"
|
||||||
|
log "URL: http://<SERVER_IP>:$PORT/"
|
||||||
|
log "Status: systemctl status $SERVICE_NAME"
|
||||||
|
log "Logs: journalctl -u $SERVICE_NAME -f"
|
||||||
|
log "Restart: systemctl restart $SERVICE_NAME"
|
||||||
|
echo ""
|
||||||
|
log "Команда запуска приложения:"
|
||||||
|
echo " $TARGET_DIR/.venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port $PORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user