попытка отката 2
This commit is contained in:
parent
e94e1a3faa
commit
5770829021
7 changed files with 297 additions and 6 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -17,4 +17,7 @@
|
|||
# Прочее частое
|
||||
*.secret
|
||||
secrets.json
|
||||
config.local.*
|
||||
config.local.*
|
||||
|
||||
#
|
||||
intradbot-docker.zip
|
||||
96
.woodpecker.yml
Normal file
96
.woodpecker.yml
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Woodpecker CI Pipeline
|
||||
#
|
||||
# Шаги:
|
||||
# 1. docker-build — сборка и пуш образа (push в main)
|
||||
# 2. docker-build-tag — сборка и пуш образа с semver-тегом (git tag v*)
|
||||
# 3. deploy — деплой на сервер (выполняется на агенте role=work_woodpecker)
|
||||
#
|
||||
# Секреты (Settings → Secrets репозитория):
|
||||
# forgejo_host — хост реестра, например git.example.com
|
||||
# forgejo_image — полный путь образа, например git.example.com/user/intradbot
|
||||
# forgejo_username — логин (обычно = gitea.actor)
|
||||
# forgejo_token — токен с правом package:write
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
steps:
|
||||
|
||||
# ── 1а. Сборка при push в main ─────────────────────────────────────────────
|
||||
- name: docker-build
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry:
|
||||
from_secret: forgejo_host
|
||||
repo:
|
||||
from_secret: forgejo_image
|
||||
username:
|
||||
from_secret: forgejo_username
|
||||
password:
|
||||
from_secret: forgejo_token
|
||||
tags:
|
||||
- latest
|
||||
- "sha-${CI_COMMIT_SHA:0:8}"
|
||||
cache_from: "git.ihateamerica.ru/dv/intrabot:latest"
|
||||
build_args:
|
||||
- "GIT_COMMIT=${CI_COMMIT_SHA:0:8}"
|
||||
- "BUILDKIT_INLINE_CACHE=1"
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
|
||||
# ── 1б. Сборка при git tag v* (semver) ────────────────────────────────────
|
||||
- name: docker-build-tag
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
settings:
|
||||
registry:
|
||||
from_secret: forgejo_host
|
||||
repo:
|
||||
from_secret: forgejo_image
|
||||
username:
|
||||
from_secret: forgejo_username
|
||||
password:
|
||||
from_secret: forgejo_token
|
||||
tags:
|
||||
- "${CI_COMMIT_TAG}" # v1.2.3
|
||||
- latest
|
||||
- "sha-${CI_COMMIT_SHA:0:8}" # всегда — нужно для отката
|
||||
cache_from: "git.ihateamerica.ru/dv/intrabot:latest"
|
||||
build_args:
|
||||
- "GIT_COMMIT=${CI_COMMIT_SHA:0:8}"
|
||||
- "BUILDKIT_INLINE_CACHE=1"
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
# ── 2. Деплой на продакшн-сервер ──────────────────────────────────────────
|
||||
# Выполняется на Woodpecker-агенте с меткой role=work_woodpecker.
|
||||
# Требует: репозиторий помечен «trusted» в настройках Woodpecker-сервера
|
||||
# (иначе монтирование volumes запрещено).
|
||||
- name: deploy
|
||||
image: docker:cli
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /opt/intrabot:/deploy
|
||||
commands:
|
||||
- |
|
||||
set -e
|
||||
cd /deploy
|
||||
|
||||
# Сохраняем ID текущего контейнерного образа (для отката)
|
||||
CONTAINER=$(docker compose ps -q intrabot 2>/dev/null | head -1)
|
||||
if [ -n "$CONTAINER" ]; then
|
||||
OLD_IMAGE=$(docker inspect --format='{{.Image}}' "$CONTAINER")
|
||||
printf '%s' "$OLD_IMAGE" > .previous_image
|
||||
echo "Snapshot for rollback: $OLD_IMAGE"
|
||||
fi
|
||||
|
||||
# Обновляем образ и перезапускаем
|
||||
docker compose pull
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
echo "Deployed: ${CI_COMMIT_SHA:0:8} (${CI_COMMIT_MESSAGE})"
|
||||
labels:
|
||||
role: work_woodpecker
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: tag
|
||||
13
Dockerfile
13
Dockerfile
|
|
@ -7,9 +7,14 @@ RUN mkdir -p /app/data
|
|||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
ARG GIT_COMMIT=dev
|
||||
ARG DOCKER_IMAGE_CREATED=unknown
|
||||
ENV GIT_COMMIT=$GIT_COMMIT
|
||||
ENV BUILD_TIME=$DOCKER_IMAGE_CREATED
|
||||
|
||||
COPY . .
|
||||
|
||||
# Gunicorn запускает Flask в продакшене.
|
||||
# intradbot.py сам запускает бота и шедулер в отдельных потоках,
|
||||
# а Gunicorn обслуживает HTTP — один worker достаточно для webhook-нагрузки.
|
||||
CMD ["python", "intradbot.py"]
|
||||
# Продакшен: Gunicorn (gthread, 1 worker) обслуживает Flask.
|
||||
# Telegram-бот и шедулер стартуют внутри воркера через gunicorn.conf.py (post_fork).
|
||||
# Локальная разработка: python intradbot.py
|
||||
CMD ["gunicorn", "-c", "gunicorn.conf.py", "web.server:create_app()"]
|
||||
|
|
|
|||
44
deploy/docker-compose.yml
Normal file
44
deploy/docker-compose.yml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ПРОДАКШН docker-compose.yml — размещать на сервере в /opt/intradbot/
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Первоначальный запуск:
|
||||
# mkdir -p /opt/intradbot/{data,ssl}
|
||||
# cp deploy/docker-compose.yml /opt/intradbot/docker-compose.yml
|
||||
# cp .env.example /opt/intradbot/.env && nano /opt/intradbot/.env
|
||||
# cp /path/to/cert.pem /opt/intradbot/ssl/
|
||||
# cp /path/to/key.pem /opt/intradbot/ssl/
|
||||
# docker login git.example.com
|
||||
# cd /opt/intradbot && docker compose up -d
|
||||
#
|
||||
# Обновление (вручную, если нет CI):
|
||||
# cd /opt/intradbot && docker compose pull && docker compose up -d
|
||||
#
|
||||
# Полезные команды:
|
||||
# docker compose logs -f — логи в реальном времени
|
||||
# docker compose ps — статус
|
||||
# docker compose exec intradbot bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
services:
|
||||
intradbot:
|
||||
image: git.ihateamerica.ru/dv/intrabot:latest # ← замени на свой хост/юзер
|
||||
restart: always
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/app/data # SQLite БД
|
||||
- ./ssl:/app/ssl:ro # cert.pem + key.pem
|
||||
pull_policy: always
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
|
|
@ -7,4 +7,10 @@ services:
|
|||
- "8080:8080"
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./ssl:/app/ssl:ro # ← реальные файлы без симлинков
|
||||
- ./ssl:/app/ssl:ro # ← реальные файлы без симлинков
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
78
gunicorn.conf.py
Normal file
78
gunicorn.conf.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Конфигурация Gunicorn для продакшена.
|
||||
|
||||
Архитектура:
|
||||
- 1 воркер (workers=1) — обязательно, т.к.:
|
||||
* SQLite не поддерживает несколько независимых процессов без WAL
|
||||
* SSE event bus (events.py) хранит состояние в памяти одного процесса
|
||||
* Notifier (poster.py) инициализируется один раз в памяти воркера
|
||||
- gthread: несколько потоков внутри воркера — позволяет обслуживать
|
||||
одновременно SSE-поток дашборда + вебхуки + API-запросы
|
||||
- post_fork: стартует Telegram-бот и шедулер внутри воркера (не в мастере)
|
||||
|
||||
Локальная разработка: python intradbot.py (Flask dev server)
|
||||
Продакшен (Docker): gunicorn -c gunicorn.conf.py "web.server:create_app()"
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
# ── Сервер ────────────────────────────────────────────────────────────────────
|
||||
|
||||
bind = f"0.0.0.0:{os.environ.get('PORT', '8080')}"
|
||||
workers = 1 # строго 1 — см. комментарий выше
|
||||
worker_class = "gthread" # многопоточный воркер для SSE + вебхуков одновременно
|
||||
threads = 4 # потоков: SSE-клиенты + API-запросы + запас
|
||||
timeout = 120 # достаточно для долгих SSE-соединений
|
||||
keepalive = 5
|
||||
|
||||
# ── Логирование ───────────────────────────────────────────────────────────────
|
||||
|
||||
accesslog = "-" # stdout
|
||||
errorlog = "-" # stderr
|
||||
loglevel = "info"
|
||||
access_log_format = '%(h)s "%(r)s" %(s)s %(b)s %(D)sµs'
|
||||
|
||||
# ── Хуки жизненного цикла ────────────────────────────────────────────────────
|
||||
|
||||
def on_starting(server):
|
||||
"""Инициализация до форка воркеров (выполняется в мастер-процессе)."""
|
||||
import logging
|
||||
import config
|
||||
import database
|
||||
from intradesk import reference
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
handlers=[logging.StreamHandler()],
|
||||
)
|
||||
|
||||
os.makedirs(os.path.dirname(config.DB_FILE), exist_ok=True)
|
||||
database.init_db()
|
||||
reference.seed_static()
|
||||
reference.load_all()
|
||||
server.log.info("Database and reference data initialized.")
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""
|
||||
Выполняется в каждом воркер-процессе после fork.
|
||||
Запускает Telegram-бот и шедулер как daemon-потоки внутри воркера.
|
||||
"""
|
||||
import schedule
|
||||
from intradesk import reference
|
||||
from bot.telegram import start_bot
|
||||
|
||||
threading.Thread(target=start_bot, daemon=True, name="telegram-bot").start()
|
||||
|
||||
schedule.every().monday.at("10:00").do(reference.load_all)
|
||||
|
||||
def _run_scheduler():
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=_run_scheduler, daemon=True, name="scheduler").start()
|
||||
worker.log.info("Telegram bot and scheduler started in worker.")
|
||||
59
scripts/rollback.sh
Normal file
59
scripts/rollback.sh
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Откат к предыдущему образу
|
||||
#
|
||||
# Использование (на сервере):
|
||||
# bash /opt/intradbot/scripts/rollback.sh
|
||||
#
|
||||
# Как работает:
|
||||
# Перед каждым деплоем Woodpecker сохраняет ID текущего образа
|
||||
# в /opt/intradbot/.previous_image.
|
||||
# Этот скрипт перетегирует сохранённый образ как :latest и перезапускает
|
||||
# контейнер без обращения к registry.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
set -e
|
||||
|
||||
DEPLOY_DIR="/opt/intradbot"
|
||||
COMPOSE_FILE="$DEPLOY_DIR/docker-compose.yml"
|
||||
PREV_FILE="$DEPLOY_DIR/.previous_image"
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
if [ ! -f "$PREV_FILE" ]; then
|
||||
echo "ERROR: файл $PREV_FILE не найден — предыдущий деплой не был записан"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREV_IMAGE=$(cat "$PREV_FILE" | tr -d '[:space:]')
|
||||
if [ -z "$PREV_IMAGE" ]; then
|
||||
echo "ERROR: $PREV_FILE пустой"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверяем, что образ ещё есть локально (не был удалён docker prune)
|
||||
if ! docker image inspect "$PREV_IMAGE" > /dev/null 2>&1; then
|
||||
echo "ERROR: образ $PREV_IMAGE не найден локально — используй ручной откат:"
|
||||
echo " docker compose pull # затем подставь нужный sha-тег в docker-compose.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Определяем полное имя образа из docker-compose.yml
|
||||
TARGET_IMAGE=$(grep 'image:' "$COMPOSE_FILE" | awk '{print $2}' | head -1)
|
||||
if [ -z "$TARGET_IMAGE" ]; then
|
||||
echo "ERROR: не удалось извлечь image из $COMPOSE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Откат:"
|
||||
echo " образ сейчас : $(docker compose ps -q intradbot 2>/dev/null | xargs -r docker inspect --format='{{.Image}}' | head -1 || echo 'нет контейнера')"
|
||||
echo " откатываемся : $PREV_IMAGE"
|
||||
|
||||
# Перетегируем старый образ как :latest локально
|
||||
docker tag "$PREV_IMAGE" "$TARGET_IMAGE"
|
||||
|
||||
# Перезапускаем без pull (используем локальный образ)
|
||||
docker compose up -d --no-build
|
||||
|
||||
echo ""
|
||||
echo "Откат выполнен. Текущий образ: $TARGET_IMAGE"
|
||||
echo "Для проверки: docker compose ps"
|
||||
Loading…
Add table
Reference in a new issue