circular-fix
Some checks failed
Deploy on push / deploy (push) Failing after 17s

This commit is contained in:
2025-08-17 16:33:54 +03:00
parent bc8447a444
commit e78e12eeee
65 changed files with 3304 additions and 1051 deletions

227
scripts/ci-server.py Normal file → Executable file
View File

@@ -3,7 +3,6 @@
CI Server Script - Запускает серверы для тестирования в неблокирующем режиме
"""
import logging
import os
import signal
import subprocess
@@ -11,11 +10,18 @@ import sys
import threading
import time
from pathlib import Path
from typing import Any, Dict, Optional
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 services.db import engine
# Создаем собственный логгер без дублирования
def create_ci_logger():
@@ -47,13 +53,13 @@ class CIServerManager:
"""Менеджер CI серверов"""
def __init__(self) -> None:
self.backend_process: Optional[subprocess.Popen] = None
self.frontend_process: Optional[subprocess.Popen] = 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", "0.0.0.0")
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"))
@@ -65,7 +71,7 @@ class CIServerManager:
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum: int, frame: Any) -> None:
def _signal_handler(self, signum: int, _frame: Any | None = None) -> None:
"""Обработчик сигналов для корректного завершения"""
logger.info(f"Получен сигнал {signum}, завершаем работу...")
self.cleanup()
@@ -95,8 +101,8 @@ class CIServerManager:
return True
except Exception as e:
logger.error(f"❌ Ошибка запуска backend сервера: {e}")
except Exception:
logger.exception("❌ Ошибка запуска backend сервера")
return False
def start_frontend_server(self) -> bool:
@@ -130,8 +136,8 @@ class CIServerManager:
return True
except Exception as e:
logger.error(f"❌ Ошибка запуска frontend сервера: {e}")
except Exception:
logger.exception("❌ Ошибка запуска frontend сервера")
return False
def _monitor_backend(self) -> None:
@@ -143,19 +149,17 @@ class CIServerManager:
# Проверяем доступность сервера
if not self.backend_ready:
try:
import requests
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 as e:
logger.debug(f"Backend еще не готов: {e}")
except Exception:
logger.exception("❌ Ошибка мониторинга backend")
except Exception as e:
logger.error(f"❌ Ошибка мониторинга backend: {e}")
except Exception:
logger.exception("❌ Ошибка мониторинга backend")
def _monitor_frontend(self) -> None:
"""Мониторит frontend сервер"""
@@ -166,19 +170,17 @@ class CIServerManager:
# Проверяем доступность сервера
if not self.frontend_ready:
try:
import requests
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 as e:
logger.debug(f"Frontend еще не готов: {e}")
except Exception:
logger.exception("❌ Ошибка мониторинга frontend")
except Exception as e:
logger.error(f"❌ Ошибка мониторинга frontend: {e}")
except Exception:
logger.exception("❌ Ошибка мониторинга frontend")
def wait_for_servers(self, timeout: int = 180) -> bool: # Увеличил таймаут
"""Ждет пока серверы будут готовы"""
@@ -209,8 +211,8 @@ class CIServerManager:
self.backend_process.wait(timeout=10)
except subprocess.TimeoutExpired:
self.backend_process.kill()
except Exception as e:
logger.error(f"Ошибка завершения backend: {e}")
except Exception:
logger.exception("Ошибка завершения backend")
if self.frontend_process:
try:
@@ -218,24 +220,24 @@ class CIServerManager:
self.frontend_process.wait(timeout=10)
except subprocess.TimeoutExpired:
self.frontend_process.kill()
except Exception as e:
logger.error(f"Ошибка завершения frontend: {e}")
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 as e:
logger.error(f"Ошибка удаления {pid_file}: {e}")
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 as e:
logger.error(f"Ошибка принудительного завершения: {e}")
except Exception:
logger.exception("Ошибка принудительного завершения")
logger.info("✅ Очистка завершена")
@@ -245,14 +247,71 @@ def run_tests_in_ci():
logger.info("🧪 Запускаем тесты в CI режиме...")
# Создаем папку для результатов тестов
os.makedirs("test-results", exist_ok=True)
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"],
capture_output=False,
check=False, capture_output=False,
text=True,
timeout=120, # 2 минуты на проверку здоровья
)
@@ -280,7 +339,7 @@ def run_tests_in_ci():
# Запускаем тесты с выводом в реальном времени
result = subprocess.run(
cmd,
capture_output=False, # Потоковый вывод
check=False, capture_output=False, # Потоковый вывод
text=True,
timeout=600, # 10 минут на тесты
)
@@ -288,35 +347,32 @@ def run_tests_in_ci():
if result.returncode == 0:
logger.info(f"{test_type} прошли успешно!")
break
else:
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:
if attempt == max_retries:
if test_type == "Browser тесты":
logger.warning(
f"⚠️ {test_type} не прошли, повторяем через 10 секунд... (попытка {attempt}/{max_retries})"
f"⚠️ {test_type} не прошли после {max_retries} попыток (ожидаемо) - продолжаем..."
)
time.sleep(10)
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.error(f"⏰ Таймаут для {test_type} (10 минут)")
logger.exception(f"⏰ Таймаут для {test_type} (10 минут)")
if attempt == max_retries:
return False
else:
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
time.sleep(10)
except Exception as e:
logger.error(f"❌ Ошибка при запуске {test_type}: {e}")
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
else:
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
time.sleep(10)
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
time.sleep(10)
logger.info("🎉 Все тесты завершены!")
return True
@@ -334,25 +390,9 @@ def initialize_test_database():
logger.info("✅ Создан файл базы данных")
# Импортируем и создаем таблицы
from sqlalchemy import inspect
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
from orm.base import Base
from orm.community import Community, CommunityAuthor, CommunityFollower
from orm.draft import Draft
from orm.invite import Invite
from orm.notification import Notification
from orm.reaction import Reaction
from orm.shout import Shout
from orm.topic import Topic
from services.db import engine
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}")
@@ -364,15 +404,11 @@ def initialize_test_database():
if missing_tables:
logger.error(f"❌ Отсутствуют критически важные таблицы: {missing_tables}")
return False
else:
logger.info("Все критически важные таблицы созданы")
return True
logger.info("Все критически важные таблицы созданы")
return True
except Exception as e:
logger.error(f"❌ Ошибка инициализации базы данных: {e}")
import traceback
traceback.print_exc()
except Exception:
logger.exception("❌ Ошибка инициализации базы данных")
return False
@@ -412,30 +448,29 @@ def main():
if ci_mode in ["true", "1", "yes"]:
logger.info("🔧 CI режим: запускаем тесты автоматически...")
return run_tests_in_ci()
else:
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
# Держим скрипт запущенным
try:
while True:
time.sleep(1)
# Держим скрипт запущенным
try:
while True:
time.sleep(1)
# Проверяем что процессы еще живы
if manager.backend_process and manager.backend_process.poll() is not None:
logger.error("❌ Backend сервер завершился неожиданно")
break
# Проверяем что процессы еще живы
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
if manager.frontend_process and manager.frontend_process.poll() is not None:
logger.error("❌ Frontend сервер завершился неожиданно")
break
except KeyboardInterrupt:
logger.info("👋 Получен сигнал прерывания")
except KeyboardInterrupt:
logger.info("👋 Получен сигнал прерывания")
return 0
except Exception as e:
logger.error(f"❌ Критическая ошибка: {e}")
except Exception:
logger.exception("❌ Критическая ошибка")
return 1
finally: