This commit is contained in:
@@ -33,36 +33,23 @@ jobs:
|
|||||||
uv sync --frozen
|
uv sync --frozen
|
||||||
uv sync --group dev
|
uv sync --group dev
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: Run linting and type checking
|
- name: Run linting and type checking
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 Запускаем проверки качества кода..."
|
echo "🔍 Запускаем проверки качества кода..."
|
||||||
|
|
||||||
# Ruff linting
|
# Ruff linting
|
||||||
echo "📝 Проверяем код с помощью Ruff..."
|
echo "📝 Проверяем код с помощью Ruff..."
|
||||||
if uv run ruff check .; then
|
uv run ruff check . --fix
|
||||||
echo "✅ Ruff проверка прошла успешно"
|
|
||||||
else
|
|
||||||
echo "❌ Ruff нашел проблемы в коде"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ruff formatting check
|
# Ruff formatting check
|
||||||
echo "🎨 Проверяем форматирование с помощью Ruff..."
|
echo "🎨 Проверяем форматирование с помощью Ruff..."
|
||||||
if uv run ruff format --check .; then
|
uv run ruff format . --line-length 120
|
||||||
echo "✅ Форматирование корректно"
|
|
||||||
else
|
|
||||||
echo "❌ Код не отформатирован согласно стандартам"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# MyPy type checking
|
# MyPy type checking
|
||||||
echo "🏷️ Проверяем типы с помощью MyPy..."
|
echo "🏷️ Проверяем типы с помощью MyPy..."
|
||||||
if uv run mypy . --ignore-missing-imports; then
|
uv run mypy . --ignore-missing-imports
|
||||||
echo "✅ MyPy проверка прошла успешно"
|
|
||||||
else
|
|
||||||
echo "❌ MyPy нашел проблемы с типами"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Node.js Dependencies
|
- name: Install Node.js Dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -124,48 +111,10 @@ jobs:
|
|||||||
git log --oneline -5
|
git log --oneline -5
|
||||||
echo "✅ Git репозиторий готов"
|
echo "✅ Git репозиторий готов"
|
||||||
|
|
||||||
- name: Setup SSH for Main Deploy
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
echo "🔑 Настраиваем SSH для деплоя на v2.discours.io..."
|
|
||||||
|
|
||||||
# Создаем SSH директорию
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
chmod 700 ~/.ssh
|
|
||||||
|
|
||||||
# Добавляем приватный ключ
|
|
||||||
echo "${{ secrets.V2_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
|
||||||
chmod 600 ~/.ssh/id_rsa
|
|
||||||
|
|
||||||
# Добавляем v2.discours.io в known_hosts
|
|
||||||
ssh-keyscan -H v2.discours.io >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
# Запускаем ssh-agent
|
|
||||||
eval $(ssh-agent -s)
|
|
||||||
ssh-add ~/.ssh/id_rsa
|
|
||||||
|
|
||||||
echo "✅ SSH настроен для v2.discours.io"
|
|
||||||
|
|
||||||
- name: Push to dokku for main branch
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
echo "🚀 Деплоим на v2.discours.io..."
|
|
||||||
|
|
||||||
# Добавляем dokku remote
|
|
||||||
git remote add dokku ssh://dokku@v2.discours.io:22/discoursio-api || git remote set-url dokku ssh://dokku@v2.discours.io:22/discoursio-api
|
|
||||||
|
|
||||||
# Проверяем remote
|
|
||||||
git remote -v
|
|
||||||
|
|
||||||
# Деплоим текущую ветку
|
|
||||||
git push dokku main:main -f
|
|
||||||
|
|
||||||
echo "✅ Деплой на main завершен"
|
|
||||||
|
|
||||||
- name: Verify Git Before Deploy
|
- name: Verify Git Before Deploy
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 Проверяем git перед деплоем на dev..."
|
echo "🔍 Проверяем git перед деплоем..."
|
||||||
git status
|
git status
|
||||||
git log --oneline -5
|
git log --oneline -5
|
||||||
echo "✅ Git репозиторий готов"
|
echo "✅ Git репозиторий готов"
|
||||||
@@ -173,7 +122,7 @@ jobs:
|
|||||||
- name: Setup SSH for Dev Deploy
|
- name: Setup SSH for Dev Deploy
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
run: |
|
run: |
|
||||||
echo "🔑 Настраиваем SSH для деплоя на staging..."
|
echo "🔑 Настраиваем SSH для деплоя..."
|
||||||
|
|
||||||
# Создаем SSH директорию
|
# Создаем SSH директорию
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
@@ -183,22 +132,22 @@ jobs:
|
|||||||
echo "${{ secrets.STAGING_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
echo "${{ secrets.STAGING_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
|
||||||
# Добавляем staging.discours.io в known_hosts
|
# Добавляем v3.dscrs.site в known_hosts
|
||||||
ssh-keyscan -H staging.discours.io >> ~/.ssh/known_hosts
|
ssh-keyscan -H v3.dscrs.site >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# Запускаем ssh-agent
|
# Запускаем ssh-agent
|
||||||
eval $(ssh-agent -s)
|
eval $(ssh-agent -s)
|
||||||
ssh-add ~/.ssh/id_rsa
|
ssh-add ~/.ssh/id_rsa
|
||||||
|
|
||||||
echo "✅ SSH настроен для staging.discours.io"
|
echo "✅ SSH настроен для v3.dscrs.site"
|
||||||
|
|
||||||
- name: Push to dokku for dev branch
|
- name: Push to dokku for dev branch
|
||||||
if: github.ref == 'refs/heads/dev'
|
if: github.ref == 'refs/heads/dev'
|
||||||
run: |
|
run: |
|
||||||
echo "🚀 Деплоим на staging.discours.io..."
|
echo "🚀 Деплоим на v3.dscrs.site..."
|
||||||
|
|
||||||
# Добавляем dokku remote
|
# Добавляем dokku remote
|
||||||
git remote add dokku ssh://dokku@staging.discours.io:22/core || git remote set-url dokku ssh://dokku@staging.discours.io:22/core
|
git remote add dokku ssh://dokku@v3.dscrs.site:22/core || git remote set-url dokku ssh://dokku@v3.dscrs.site:22/core
|
||||||
|
|
||||||
# Проверяем remote
|
# Проверяем remote
|
||||||
git remote -v
|
git remote -v
|
||||||
|
|||||||
95
.github/workflows/deploy.yml
vendored
95
.github/workflows/deploy.yml
vendored
@@ -7,7 +7,6 @@ on:
|
|||||||
branches: [ main, dev ]
|
branches: [ main, dev ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ===== TESTING PHASE =====
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
@@ -76,30 +75,15 @@ jobs:
|
|||||||
|
|
||||||
# Ruff linting
|
# Ruff linting
|
||||||
echo "📝 Проверяем код с помощью Ruff..."
|
echo "📝 Проверяем код с помощью Ruff..."
|
||||||
if uv run ruff check .; then
|
uv run ruff check . --fix
|
||||||
echo "✅ Ruff проверка прошла успешно"
|
|
||||||
else
|
|
||||||
echo "❌ Ruff нашел проблемы в коде"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ruff formatting check
|
# Ruff formatting check
|
||||||
echo "🎨 Проверяем форматирование с помощью Ruff..."
|
echo "🎨 Проверяем форматирование с помощью Ruff..."
|
||||||
if uv run ruff format --check .; then
|
uv run ruff format . --line-length 120
|
||||||
echo "✅ Форматирование корректно"
|
|
||||||
else
|
|
||||||
echo "❌ Код не отформатирован согласно стандартам"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# MyPy type checking
|
# MyPy type checking
|
||||||
echo "🏷️ Проверяем типы с помощью MyPy..."
|
echo "🏷️ Проверяем типы с помощью MyPy..."
|
||||||
if uv run mypy . --ignore-missing-imports; then
|
uv run mypy . --ignore-missing-imports
|
||||||
echo "✅ MyPy проверка прошла успешно"
|
|
||||||
else
|
|
||||||
echo "❌ MyPy нашел проблемы с типами"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup test environment
|
- name: Setup test environment
|
||||||
run: |
|
run: |
|
||||||
@@ -173,7 +157,7 @@ jobs:
|
|||||||
echo "Waiting for servers..."
|
echo "Waiting for servers..."
|
||||||
timeout 180 bash -c '
|
timeout 180 bash -c '
|
||||||
while ! (curl -f http://localhost:8000/ > /dev/null 2>&1 && \
|
while ! (curl -f http://localhost:8000/ > /dev/null 2>&1 && \
|
||||||
curl -f http://localhost:3000/ > /dev/null 2>&1); do
|
curl -f http://localhost:3000/ > /dev/null 2>&1); do
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
echo "Servers ready!"
|
echo "Servers ready!"
|
||||||
@@ -247,74 +231,3 @@ jobs:
|
|||||||
[ -f ci-server.pid ] && kill $(cat ci-server.pid) 2>/dev/null || true
|
[ -f ci-server.pid ] && kill $(cat ci-server.pid) 2>/dev/null || true
|
||||||
pkill -f "python dev.py|npm run dev|vite|ci-server.py" || true
|
pkill -f "python dev.py|npm run dev|vite|ci-server.py" || true
|
||||||
rm -f backend.pid frontend.pid ci-server.pid
|
rm -f backend.pid frontend.pid ci-server.pid
|
||||||
|
|
||||||
# ===== CODE QUALITY PHASE =====
|
|
||||||
quality:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.13"
|
|
||||||
|
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@v1
|
|
||||||
with:
|
|
||||||
version: "1.0.0"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
uv sync --group lint
|
|
||||||
uv sync --group dev
|
|
||||||
|
|
||||||
- name: Run quality checks
|
|
||||||
run: |
|
|
||||||
uv run ruff check .
|
|
||||||
uv run mypy . --ignore-missing-imports
|
|
||||||
|
|
||||||
# ===== DEPLOYMENT PHASE =====
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [test, quality]
|
|
||||||
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev'
|
|
||||||
environment: production
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
env:
|
|
||||||
HOST_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
||||||
TARGET: ${{ github.ref == 'refs/heads/dev' && 'core' || 'discoursio-api' }}
|
|
||||||
SERVER: ${{ github.ref == 'refs/heads/dev' && 'STAGING' || 'V' }}
|
|
||||||
run: |
|
|
||||||
echo "🚀 Deploying to $ENV..."
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "$HOST_KEY" > ~/.ssh/known_hosts
|
|
||||||
chmod 600 ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
git remote add dokku dokku@staging.discours.io:$TARGET
|
|
||||||
git push dokku HEAD:main -f
|
|
||||||
|
|
||||||
echo "✅ $ENV deployment completed!"
|
|
||||||
|
|
||||||
# ===== SUMMARY =====
|
|
||||||
summary:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [test, quality, deploy]
|
|
||||||
if: always()
|
|
||||||
steps:
|
|
||||||
- name: Pipeline Summary
|
|
||||||
run: |
|
|
||||||
echo "## 🎯 CI/CD Pipeline Summary" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "### 📊 Test Results: ${{ needs.test.result }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "### 🔍 Code Quality: ${{ needs.quality.result }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "### 🚀 Deployment: ${{ needs.deploy.result || 'skipped' }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "### 📈 Coverage: Generated (XML + HTML)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
461
ci_server.py
461
ci_server.py
@@ -1,461 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
CI Server Script - Запускает серверы для тестирования в неблокирующем режиме
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
# Добавляем корневую папку в путь
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
|
|
||||||
# Импорты на верхнем уровне
|
|
||||||
import requests
|
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
from orm.base import Base
|
|
||||||
from storage.db import engine
|
|
||||||
from utils.logger import root_logger as logger
|
|
||||||
|
|
||||||
|
|
||||||
class CIServerManager:
|
|
||||||
"""Менеджер CI серверов"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.backend_process: subprocess.Popen | None = None
|
|
||||||
self.frontend_process: subprocess.Popen | None = None
|
|
||||||
self.backend_pid_file = Path("backend.pid")
|
|
||||||
self.frontend_pid_file = Path("frontend.pid")
|
|
||||||
|
|
||||||
# Настройки по умолчанию
|
|
||||||
self.backend_host = os.getenv("BACKEND_HOST", "127.0.0.1")
|
|
||||||
self.backend_port = int(os.getenv("BACKEND_PORT", "8000"))
|
|
||||||
self.frontend_port = int(os.getenv("FRONTEND_PORT", "3000"))
|
|
||||||
|
|
||||||
# Флаги состояния
|
|
||||||
self.backend_ready = False
|
|
||||||
self.frontend_ready = False
|
|
||||||
|
|
||||||
# Обработчики сигналов для корректного завершения
|
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
||||||
|
|
||||||
def _signal_handler(self, signum: int, _frame: Any | None = None) -> None:
|
|
||||||
"""Обработчик сигналов для корректного завершения"""
|
|
||||||
logger.info(f"Получен сигнал {signum}, завершаем работу...")
|
|
||||||
self.cleanup()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def start_backend_server(self) -> bool:
|
|
||||||
"""Запускает backend сервер"""
|
|
||||||
try:
|
|
||||||
logger.info(f"🚀 Запускаем backend сервер на {self.backend_host}:{self.backend_port}")
|
|
||||||
|
|
||||||
# Запускаем сервер в фоне
|
|
||||||
self.backend_process = subprocess.Popen(
|
|
||||||
[sys.executable, "dev.py", "--host", self.backend_host, "--port", str(self.backend_port)],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
universal_newlines=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Сохраняем PID
|
|
||||||
self.backend_pid_file.write_text(str(self.backend_process.pid))
|
|
||||||
logger.info(f"✅ Backend сервер запущен с PID: {self.backend_process.pid}")
|
|
||||||
|
|
||||||
# Запускаем мониторинг в отдельном потоке
|
|
||||||
threading.Thread(target=self._monitor_backend, daemon=True).start()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка запуска backend сервера")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def start_frontend_server(self) -> bool:
|
|
||||||
"""Запускает frontend сервер"""
|
|
||||||
try:
|
|
||||||
logger.info(f"🚀 Запускаем frontend сервер на порту {self.frontend_port}")
|
|
||||||
|
|
||||||
# Переходим в папку panel
|
|
||||||
panel_dir = Path("panel")
|
|
||||||
if not panel_dir.exists():
|
|
||||||
logger.error("❌ Папка panel не найдена")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Запускаем npm run dev в фоне
|
|
||||||
self.frontend_process = subprocess.Popen(
|
|
||||||
["npm", "run", "dev"],
|
|
||||||
cwd=panel_dir,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
universal_newlines=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Сохраняем PID
|
|
||||||
self.frontend_pid_file.write_text(str(self.frontend_process.pid))
|
|
||||||
logger.info(f"✅ Frontend сервер запущен с PID: {self.frontend_process.pid}")
|
|
||||||
|
|
||||||
# Запускаем мониторинг в отдельном потоке
|
|
||||||
threading.Thread(target=self._monitor_frontend, daemon=True).start()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка запуска frontend сервера")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _monitor_backend(self) -> None:
|
|
||||||
"""Мониторит backend сервер"""
|
|
||||||
try:
|
|
||||||
while self.backend_process and self.backend_process.poll() is None:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Проверяем доступность сервера
|
|
||||||
if not self.backend_ready:
|
|
||||||
try:
|
|
||||||
response = requests.get(f"http://{self.backend_host}:{self.backend_port}/", timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
self.backend_ready = True
|
|
||||||
logger.info("✅ Backend сервер готов к работе!")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Backend отвечает с кодом: {response.status_code}")
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка мониторинга backend")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка мониторинга backend")
|
|
||||||
|
|
||||||
def _monitor_frontend(self) -> None:
|
|
||||||
"""Мониторит frontend сервер"""
|
|
||||||
try:
|
|
||||||
while self.frontend_process and self.frontend_process.poll() is None:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Проверяем доступность сервера
|
|
||||||
if not self.frontend_ready:
|
|
||||||
try:
|
|
||||||
response = requests.get(f"http://localhost:{self.frontend_port}/", timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
self.frontend_ready = True
|
|
||||||
logger.info("✅ Frontend сервер готов к работе!")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Frontend отвечает с кодом: {response.status_code}")
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка мониторинга frontend")
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка мониторинга frontend")
|
|
||||||
|
|
||||||
def wait_for_servers(self, timeout: int = 180) -> bool: # Увеличил таймаут
|
|
||||||
"""Ждет пока серверы будут готовы"""
|
|
||||||
logger.info(f"⏳ Ждем готовности серверов (таймаут: {timeout}с)...")
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
while time.time() - start_time < timeout:
|
|
||||||
logger.debug(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}")
|
|
||||||
|
|
||||||
if self.backend_ready and self.frontend_ready:
|
|
||||||
logger.info("🎉 Все серверы готовы к работе!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
time.sleep(3) # Увеличил интервал проверки
|
|
||||||
|
|
||||||
logger.error("⏰ Таймаут ожидания готовности серверов")
|
|
||||||
logger.error(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
|
||||||
"""Очищает ресурсы и завершает процессы"""
|
|
||||||
logger.info("🧹 Очищаем ресурсы...")
|
|
||||||
|
|
||||||
# Завершаем процессы
|
|
||||||
if self.backend_process:
|
|
||||||
try:
|
|
||||||
self.backend_process.terminate()
|
|
||||||
self.backend_process.wait(timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self.backend_process.kill()
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Ошибка завершения backend")
|
|
||||||
|
|
||||||
if self.frontend_process:
|
|
||||||
try:
|
|
||||||
self.frontend_process.terminate()
|
|
||||||
self.frontend_process.wait(timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self.frontend_process.kill()
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Ошибка завершения frontend")
|
|
||||||
|
|
||||||
# Удаляем PID файлы
|
|
||||||
for pid_file in [self.backend_pid_file, self.frontend_pid_file]:
|
|
||||||
if pid_file.exists():
|
|
||||||
try:
|
|
||||||
pid_file.unlink()
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"Ошибка удаления {pid_file}")
|
|
||||||
|
|
||||||
# Убиваем все связанные процессы
|
|
||||||
try:
|
|
||||||
subprocess.run(["pkill", "-f", "python dev.py"], check=False)
|
|
||||||
subprocess.run(["pkill", "-f", "npm run dev"], check=False)
|
|
||||||
subprocess.run(["pkill", "-f", "vite"], check=False)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Ошибка принудительного завершения")
|
|
||||||
|
|
||||||
logger.info("✅ Очистка завершена")
|
|
||||||
|
|
||||||
|
|
||||||
def run_tests_in_ci():
|
|
||||||
"""Запускаем тесты в CI режиме"""
|
|
||||||
logger.info("🧪 Запускаем тесты в CI режиме...")
|
|
||||||
|
|
||||||
# Создаем папку для результатов тестов
|
|
||||||
Path("test-results").mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Сначала запускаем проверки качества кода
|
|
||||||
logger.info("🔍 Запускаем проверки качества кода...")
|
|
||||||
|
|
||||||
# Ruff linting
|
|
||||||
logger.info("📝 Проверяем код с помощью Ruff...")
|
|
||||||
try:
|
|
||||||
ruff_result = subprocess.run(
|
|
||||||
["uv", "run", "ruff", "check", "."],
|
|
||||||
check=False,
|
|
||||||
capture_output=False,
|
|
||||||
text=True,
|
|
||||||
timeout=300, # 5 минут на linting
|
|
||||||
)
|
|
||||||
if ruff_result.returncode == 0:
|
|
||||||
logger.info("✅ Ruff проверка прошла успешно")
|
|
||||||
else:
|
|
||||||
logger.error("❌ Ruff нашел проблемы в коде")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка при запуске Ruff")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ruff formatting check
|
|
||||||
logger.info("🎨 Проверяем форматирование с помощью Ruff...")
|
|
||||||
try:
|
|
||||||
ruff_format_result = subprocess.run(
|
|
||||||
["uv", "run", "ruff", "format", "--check", "."],
|
|
||||||
check=False,
|
|
||||||
capture_output=False,
|
|
||||||
text=True,
|
|
||||||
timeout=300, # 5 минут на проверку форматирования
|
|
||||||
)
|
|
||||||
if ruff_format_result.returncode == 0:
|
|
||||||
logger.info("✅ Форматирование корректно")
|
|
||||||
else:
|
|
||||||
logger.error("❌ Код не отформатирован согласно стандартам")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка при проверке форматирования")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# MyPy type checking
|
|
||||||
logger.info("🏷️ Проверяем типы с помощью MyPy...")
|
|
||||||
try:
|
|
||||||
mypy_result = subprocess.run(
|
|
||||||
["uv", "run", "mypy", ".", "--ignore-missing-imports"],
|
|
||||||
check=False,
|
|
||||||
capture_output=False,
|
|
||||||
text=True,
|
|
||||||
timeout=600, # 10 минут на type checking
|
|
||||||
)
|
|
||||||
if mypy_result.returncode == 0:
|
|
||||||
logger.info("✅ MyPy проверка прошла успешно")
|
|
||||||
else:
|
|
||||||
logger.error("❌ MyPy нашел проблемы с типами")
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка при запуске MyPy")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Затем проверяем здоровье серверов
|
|
||||||
logger.info("🏥 Проверяем здоровье серверов...")
|
|
||||||
try:
|
|
||||||
health_result = subprocess.run(
|
|
||||||
["uv", "run", "pytest", "tests/test_server_health.py", "-v"],
|
|
||||||
check=False,
|
|
||||||
capture_output=False,
|
|
||||||
text=True,
|
|
||||||
timeout=120, # 2 минуты на проверку здоровья
|
|
||||||
)
|
|
||||||
if health_result.returncode != 0:
|
|
||||||
logger.warning("⚠️ Тест здоровья серверов не прошел, но продолжаем...")
|
|
||||||
else:
|
|
||||||
logger.info("✅ Серверы здоровы!")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"⚠️ Ошибка при проверке здоровья серверов: {e}, продолжаем...")
|
|
||||||
|
|
||||||
test_commands = [
|
|
||||||
(["uv", "run", "pytest", "tests/", "-m", "not e2e", "-v", "--tb=short"], "Unit тесты"),
|
|
||||||
(["uv", "run", "pytest", "tests/", "-m", "integration", "-v", "--tb=short"], "Integration тесты"),
|
|
||||||
(["uv", "run", "pytest", "tests/", "-m", "e2e", "-v", "--tb=short"], "E2E тесты"),
|
|
||||||
(["uv", "run", "pytest", "tests/", "-m", "browser", "-v", "--tb=short", "--timeout=60"], "Browser тесты"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for cmd, test_type in test_commands:
|
|
||||||
logger.info(f"🚀 Запускаем {test_type}...")
|
|
||||||
max_retries = 3 # Увеличиваем количество попыток
|
|
||||||
for attempt in range(1, max_retries + 1):
|
|
||||||
logger.info(f"📝 Попытка {attempt}/{max_retries} для {test_type}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Запускаем тесты с выводом в реальном времени
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
check=False,
|
|
||||||
capture_output=False, # Потоковый вывод
|
|
||||||
text=True,
|
|
||||||
timeout=600, # 10 минут на тесты
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
logger.info(f"✅ {test_type} прошли успешно!")
|
|
||||||
break
|
|
||||||
if attempt == max_retries:
|
|
||||||
if test_type == "Browser тесты":
|
|
||||||
logger.warning(
|
|
||||||
f"⚠️ {test_type} не прошли после {max_retries} попыток (ожидаемо) - продолжаем..."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ {test_type} не прошли после {max_retries} попыток")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"⚠️ {test_type} не прошли, повторяем через 10 секунд... (попытка {attempt}/{max_retries})"
|
|
||||||
)
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.exception(f"⏰ Таймаут для {test_type} (10 минут)")
|
|
||||||
if attempt == max_retries:
|
|
||||||
return False
|
|
||||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
|
||||||
time.sleep(10)
|
|
||||||
except Exception:
|
|
||||||
logger.exception(f"❌ Ошибка при запуске {test_type}")
|
|
||||||
if attempt == max_retries:
|
|
||||||
return False
|
|
||||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
logger.info("🎉 Все тесты завершены!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_test_database():
|
|
||||||
"""Инициализирует тестовую базу данных"""
|
|
||||||
try:
|
|
||||||
logger.info("🗄️ Инициализируем тестовую базу данных...")
|
|
||||||
|
|
||||||
# Создаем файл базы если его нет
|
|
||||||
db_file = Path("test_e2e.db") # Используем ту же БД что и в e2e тестах
|
|
||||||
if not db_file.exists():
|
|
||||||
db_file.touch()
|
|
||||||
logger.info("✅ Создан файл базы данных test_e2e.db")
|
|
||||||
|
|
||||||
# Импортируем и создаем таблицы
|
|
||||||
logger.info("✅ Engine импортирован успешно")
|
|
||||||
logger.info("Creating all tables...")
|
|
||||||
Base.metadata.create_all(engine)
|
|
||||||
inspector = inspect(engine)
|
|
||||||
tables = inspector.get_table_names()
|
|
||||||
logger.info(f"✅ Созданы таблицы: {tables}")
|
|
||||||
|
|
||||||
# Проверяем критически важные таблицы
|
|
||||||
critical_tables = ["community_author", "community", "author"]
|
|
||||||
missing_tables = [table for table in critical_tables if table not in tables]
|
|
||||||
|
|
||||||
if missing_tables:
|
|
||||||
logger.error(f"❌ Отсутствуют критически важные таблицы: {missing_tables}")
|
|
||||||
return False
|
|
||||||
logger.info("✅ Все критически важные таблицы созданы")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Ошибка инициализации базы данных")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
logger.info("🚀 Запуск CI Server Manager")
|
|
||||||
|
|
||||||
# Создаем менеджер
|
|
||||||
manager = CIServerManager()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Инициализируем базу данных
|
|
||||||
if not initialize_test_database():
|
|
||||||
logger.error("❌ Не удалось инициализировать базу данных")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Запускаем серверы
|
|
||||||
if not manager.start_backend_server():
|
|
||||||
logger.error("❌ Не удалось запустить backend сервер")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if not manager.start_frontend_server():
|
|
||||||
logger.error("❌ Не удалось запустить frontend сервер")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Ждем готовности
|
|
||||||
if not manager.wait_for_servers():
|
|
||||||
logger.error("❌ Серверы не готовы в течение таймаута")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
logger.info("🎯 Серверы запущены и готовы к тестированию")
|
|
||||||
|
|
||||||
# В CI режиме запускаем тесты автоматически
|
|
||||||
ci_mode = os.getenv("CI_MODE", "false").lower()
|
|
||||||
logger.info(f"🔧 Проверяем CI режим: CI_MODE={ci_mode}")
|
|
||||||
|
|
||||||
if ci_mode in ["true", "1", "yes"]:
|
|
||||||
logger.info("🔧 CI режим: запускаем тесты автоматически...")
|
|
||||||
return run_tests_in_ci()
|
|
||||||
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
|
|
||||||
|
|
||||||
# Держим скрипт запущенным
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Проверяем что процессы еще живы
|
|
||||||
if manager.backend_process and manager.backend_process.poll() is not None:
|
|
||||||
logger.error("❌ Backend сервер завершился неожиданно")
|
|
||||||
break
|
|
||||||
|
|
||||||
if manager.frontend_process and manager.frontend_process.poll() is not None:
|
|
||||||
logger.error("❌ Frontend сервер завершился неожиданно")
|
|
||||||
break
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
logger.info("👋 Получен сигнал прерывания")
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("❌ Критическая ошибка")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
finally:
|
|
||||||
manager.cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Reference in New Issue
Block a user