diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml index e9834d5a..bb652b84 100644 --- a/.gitea/workflows/main.yml +++ b/.gitea/workflows/main.yml @@ -33,36 +33,23 @@ jobs: uv sync --frozen uv sync --group dev + + - name: Run linting and type checking run: | echo "🔍 Запускаем проверки качества кода..." # Ruff linting echo "📝 Проверяем код с помощью Ruff..." - if uv run ruff check .; then - echo "✅ Ruff проверка прошла успешно" - else - echo "❌ Ruff нашел проблемы в коде" - exit 1 - fi + uv run ruff check . --fix # Ruff formatting check echo "🎨 Проверяем форматирование с помощью Ruff..." - if uv run ruff format --check .; then - echo "✅ Форматирование корректно" - else - echo "❌ Код не отформатирован согласно стандартам" - exit 1 - fi + uv run ruff format . --line-length 120 # MyPy type checking echo "🏷️ Проверяем типы с помощью MyPy..." - if uv run mypy . --ignore-missing-imports; then - echo "✅ MyPy проверка прошла успешно" - else - echo "❌ MyPy нашел проблемы с типами" - exit 1 - fi + uv run mypy . --ignore-missing-imports - name: Install Node.js Dependencies run: | @@ -124,48 +111,10 @@ jobs: git log --oneline -5 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 if: github.ref == 'refs/heads/dev' run: | - echo "🔍 Проверяем git перед деплоем на dev..." + echo "🔍 Проверяем git перед деплоем..." git status git log --oneline -5 echo "✅ Git репозиторий готов" @@ -173,7 +122,7 @@ jobs: - name: Setup SSH for Dev Deploy if: github.ref == 'refs/heads/dev' run: | - echo "🔑 Настраиваем SSH для деплоя на staging..." + echo "🔑 Настраиваем SSH для деплоя..." # Создаем SSH директорию mkdir -p ~/.ssh @@ -183,22 +132,22 @@ jobs: echo "${{ secrets.STAGING_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - # Добавляем staging.discours.io в known_hosts - ssh-keyscan -H staging.discours.io >> ~/.ssh/known_hosts + # Добавляем v3.dscrs.site в known_hosts + ssh-keyscan -H v3.dscrs.site >> ~/.ssh/known_hosts # Запускаем ssh-agent eval $(ssh-agent -s) ssh-add ~/.ssh/id_rsa - echo "✅ SSH настроен для staging.discours.io" + echo "✅ SSH настроен для v3.dscrs.site" - name: Push to dokku for dev branch if: github.ref == 'refs/heads/dev' run: | - echo "🚀 Деплоим на staging.discours.io..." + echo "🚀 Деплоим на v3.dscrs.site..." # Добавляем 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 git remote -v diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e17720ba..20b302e9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,7 +7,6 @@ on: branches: [ main, dev ] jobs: - # ===== TESTING PHASE ===== test: runs-on: ubuntu-latest services: @@ -76,30 +75,15 @@ jobs: # Ruff linting echo "📝 Проверяем код с помощью Ruff..." - if uv run ruff check .; then - echo "✅ Ruff проверка прошла успешно" - else - echo "❌ Ruff нашел проблемы в коде" - exit 1 - fi + uv run ruff check . --fix # Ruff formatting check echo "🎨 Проверяем форматирование с помощью Ruff..." - if uv run ruff format --check .; then - echo "✅ Форматирование корректно" - else - echo "❌ Код не отформатирован согласно стандартам" - exit 1 - fi + uv run ruff format . --line-length 120 # MyPy type checking echo "🏷️ Проверяем типы с помощью MyPy..." - if uv run mypy . --ignore-missing-imports; then - echo "✅ MyPy проверка прошла успешно" - else - echo "❌ MyPy нашел проблемы с типами" - exit 1 - fi + uv run mypy . --ignore-missing-imports - name: Setup test environment run: | @@ -173,7 +157,7 @@ jobs: echo "Waiting for servers..." timeout 180 bash -c ' 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 done echo "Servers ready!" @@ -247,74 +231,3 @@ jobs: [ -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 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 diff --git a/ci_server.py b/ci_server.py deleted file mode 100755 index 2d06f384..00000000 --- a/ci_server.py +++ /dev/null @@ -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())