diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7d06489 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI pipeline + +on: + push: + branches: [ 'dev-*', 'feature/**' ] + workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: true + type: choice + +jobs: + test: + runs-on: ubuntu-latest + name: Test & Code Quality + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/infra/requirements-test.txt + pip install flake8 black isort mypy || true + + - name: Code formatting check (Black) + run: | + echo "🔍 Checking code formatting with Black..." + black --check . || (echo "❌ Code formatting issues found. Run 'black .' to fix." && exit 1) + + - name: Import sorting check (isort) + run: | + echo "🔍 Checking import sorting with isort..." + isort --check-only . || (echo "❌ Import sorting issues found. Run 'isort .' to fix." && exit 1) + + - name: Linting (flake8) - Critical errors + run: | + echo "🔍 Running flake8 linter (critical errors only)..." + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + + - name: Linting (flake8) - Warnings + run: | + echo "🔍 Running flake8 linter (warnings)..." + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics || true + continue-on-error: true + + - name: Run infrastructure tests + run: | + python -m pytest tests/infra/ -v --tb=short + + - name: Validate Prometheus config + run: | + python -m pytest tests/infra/test_prometheus_config.py -v + + - name: Send test success notification + if: success() + uses: appleboy/telegram-action@v1.0.0 + with: + to: ${{ secrets.TELEGRAM_CHAT_ID }} + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + message: | + ✅ CI Tests Passed + + 📦 Repository: prod + 🌿 Branch: ${{ github.ref_name }} + 📝 Commit: ${{ github.sha }} + 👤 Author: ${{ github.actor }} + + ✅ All tests passed! Code quality checks completed successfully. + + 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + continue-on-error: true + + - name: Send test failure notification + if: failure() + uses: appleboy/telegram-action@v1.0.0 + with: + to: ${{ secrets.TELEGRAM_CHAT_ID }} + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + message: | + ❌ CI Tests Failed + + 📦 Repository: prod + 🌿 Branch: ${{ github.ref_name }} + 📝 Commit: ${{ github.sha }} + 👤 Author: ${{ github.actor }} + + ❌ Tests failed! Deployment blocked. Please fix the issues and try again. + + 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + continue-on-error: true diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 452ba24..36e2285 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,21 +1,32 @@ name: Deploy to Production on: - pull_request: - types: [closed] + push: branches: [ main ] workflow_dispatch: + inputs: + action: + description: 'Action to perform' + required: true + type: choice + options: + - deploy + - rollback + rollback_commit: + description: 'Commit hash to rollback to (optional, uses last successful if empty)' + required: false + type: string jobs: deploy: runs-on: ubuntu-latest name: Deploy to Production + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'deploy') concurrency: group: production-deploy cancel-in-progress: false - if: | - (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || - github.event_name == 'workflow_dispatch' environment: name: production @@ -25,101 +36,6 @@ jobs: with: ref: main - - name: Debug secrets availability - run: | - echo "🔍 Checking secrets availability..." - echo "TELEGRAM_BOT_TOKEN: $([ -n '${{ secrets.TELEGRAM_BOT_TOKEN }}' ] && echo '✅ Set' || echo '❌ Not set')" - echo "TELEGRAM_TEST_BOT_TOKEN: $([ -n '${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}' ] && echo '✅ Set' || echo '⚠️ Not set (optional)')" - echo "ANON_BOT_TOKEN: $([ -n '${{ secrets.ANON_BOT_TOKEN }}' ] && echo '✅ Set' || echo '⚠️ Not set (optional)')" - echo "SSH_PRIVATE_KEY: $([ -n '${{ secrets.SSH_PRIVATE_KEY }}' ] && echo '✅ Set' || echo '❌ Not set')" - echo "SERVER_HOST: $([ -n '${{ vars.SERVER_HOST || secrets.SERVER_HOST }}' ] && echo '✅ Set' || echo '❌ Not set')" - echo "SERVER_USER: $([ -n '${{ vars.SERVER_USER || secrets.SERVER_USER }}' ] && echo '✅ Set' || echo '❌ Not set')" - - - name: Validate Telegram Bot Tokens - uses: appleboy/ssh-action@v1.0.0 - with: - host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} - username: ${{ vars.SERVER_USER || secrets.SERVER_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} - script: | - set -e - export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" - export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}" - export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}" - - echo "🔍 Debug: Checking environment variables..." - echo "TELEGRAM_BOT_TOKEN length: ${#TELEGRAM_BOT_TOKEN}" - echo "TELEGRAM_TEST_BOT_TOKEN length: ${#TELEGRAM_TEST_BOT_TOKEN}" - echo "ANON_BOT_TOKEN length: ${#ANON_BOT_TOKEN}" - echo "TELEGRAM_BOT_TOKEN is empty: $([ -z "$TELEGRAM_BOT_TOKEN" ] && echo 'YES' || echo 'NO')" - - echo "🔍 Validating Telegram Bot tokens from GitHub Secrets..." - - # Функция для проверки токена с retry - validate_token() { - local token_name=$1 - local token=$2 - local max_retries=3 - local retry=1 - - while [ $retry -le $max_retries ]; do - echo "🔍 Checking $token_name (attempt $retry/$max_retries)..." - - response=$(curl -s --max-time 10 "https://api.telegram.org/bot${token}/getMe" || echo "") - - if echo "$response" | grep -q '"ok":true'; then - bot_username=$(echo "$response" | grep -o '"username":"[^"]*"' | cut -d'"' -f4 || echo "unknown") - echo "✅ $token_name is valid (bot: @$bot_username)" - return 0 - else - if [ $retry -lt $max_retries ]; then - echo "⏳ $token_name validation failed, retrying in 5 seconds..." - sleep 5 - else - echo "❌ $token_name is invalid or unreachable" - echo "Response: $response" - return 1 - fi - fi - - retry=$((retry + 1)) - done - - return 1 - } - - # Проверяем Telegram Helper Bot токен из Secrets - if [ -z "$TELEGRAM_BOT_TOKEN" ]; then - echo "❌ TELEGRAM_BOT_TOKEN not found in GitHub Secrets" - echo "💡 Make sure the secret is added to the 'production' environment or repository secrets" - exit 1 - fi - - if ! validate_token "Telegram Helper Bot Token" "$TELEGRAM_BOT_TOKEN"; then - exit 1 - fi - - # Проверяем TELEGRAM_TEST_BOT_TOKEN (опционально) - if [ -n "$TELEGRAM_TEST_BOT_TOKEN" ]; then - if ! validate_token "Telegram Test Bot Token" "$TELEGRAM_TEST_BOT_TOKEN"; then - echo "⚠️ Test bot token validation failed, but continuing..." - fi - else - echo "ℹ️ TELEGRAM_TEST_BOT_TOKEN not set, skipping" - fi - - # Проверяем AnonBot токен из Secrets - if [ -z "$ANON_BOT_TOKEN" ]; then - echo "⚠️ ANON_BOT_TOKEN not found in GitHub Secrets, skipping validation" - else - if ! validate_token "AnonBot Token" "$ANON_BOT_TOKEN"; then - exit 1 - fi - fi - - echo "✅ All token validations passed!" - - name: Deploy to server uses: appleboy/ssh-action@v1.0.0 with: @@ -133,214 +49,60 @@ jobs: export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}" export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}" - echo "🔍 Debug: Checking environment variables in Deploy step..." - echo "TELEGRAM_BOT_TOKEN is set: $([ -n "$TELEGRAM_BOT_TOKEN" ] && echo 'YES' || echo 'NO')" - echo "TELEGRAM_TEST_BOT_TOKEN is set: $([ -n "$TELEGRAM_TEST_BOT_TOKEN" ] && echo 'YES' || echo 'NO')" - echo "ANON_BOT_TOKEN is set: $([ -n "$ANON_BOT_TOKEN" ] && echo 'YES' || echo 'NO')" - echo "🚀 Starting deployment to production..." - # Функция для безопасной записи в историю деплоев с использованием flock - safe_write_history() { - local entry="$1" - local history_file="/home/prod/.deploy_history.txt" - local lock_file="${history_file}.lock" - local history_size="${DEPLOY_HISTORY_SIZE:-10}" - - if command -v flock > /dev/null 2>&1; then - # Используем flock напрямую с файлом (работает в zsh и bash) - flock -x "$lock_file" sh -c " - # Записываем новую запись - echo \"$entry\" >> \"$history_file\" - - # Обрезаем файл атомарно - tail -n $history_size \"$history_file\" > \"${history_file}.tmp\" - mv \"${history_file}.tmp\" \"$history_file\" - - echo \"✅ History updated safely\" - " - else - # Fallback: простая запись без блокировки - echo "$entry" >> "$history_file" - tail -n "$history_size" "$history_file" > "${history_file}.tmp" - mv "${history_file}.tmp" "$history_file" - echo "✅ History updated (fallback method)" - fi - } - - # Переходим в директорию проекта под пользователем deploy cd /home/prod - # Сохраняем текущий коммит для отката + # Сохраняем информацию о коммите CURRENT_COMMIT=$(git rev-parse HEAD) COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" || echo "Unknown") COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an" || echo "Unknown") TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") - # Сохраняем для быстрого доступа - echo "$CURRENT_COMMIT" > /tmp/last_deploy_commit.txt echo "📝 Current commit: $CURRENT_COMMIT" echo "📝 Commit message: $COMMIT_MESSAGE" echo "📝 Author: $COMMIT_AUTHOR" - # Сохраняем в файл истории деплоев безопасно - DEPLOY_HISTORY="/home/prod/.deploy_history.txt" - DEPLOY_HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" + # Записываем в историю деплоев + HISTORY_FILE="/home/prod/.deploy_history.txt" + HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" + echo "${TIMESTAMP}|${CURRENT_COMMIT}|${COMMIT_MESSAGE}|${COMMIT_AUTHOR}|deploying" >> "$HISTORY_FILE" + tail -n "$HISTORY_SIZE" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" - # Добавляем запись о начале деплоя с блокировкой - safe_write_history "${TIMESTAMP}|${CURRENT_COMMIT}|${COMMIT_MESSAGE}|${COMMIT_AUTHOR}|deploying" - - # Обновляем код из main + # Обновляем код echo "📥 Pulling latest changes from main..." - - # Исправляем права на файлы в bots директории перед обновлением - fix_bots_permissions() { - local bots_dir="/home/prod/bots" - - if [ ! -d "$bots_dir" ]; then - echo "⚠️ Bots directory not found, skipping permissions fix" - return 0 - fi - - echo "🔧 Fixing permissions for bots directory..." - sudo chown -R deploy:deploy "$bots_dir" || true - echo "✅ Permissions fixed" - } - - fix_bots_permissions - - # Проверяем наличие локальных изменений перед reset - check_local_changes() { - echo "🔍 Checking for local changes..." - - # Сохраняем текущее состояние - git fetch origin main - - # Проверяем, есть ли локальные изменения - if ! git diff --quiet HEAD origin/main 2>/dev/null; then - echo "⚠️ Local changes detected! They will be overwritten by git reset --hard" - echo "📋 Diff summary:" - git diff --stat HEAD origin/main || true - fi - - # Проверяем uncommitted changes - if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then - echo "⚠️ Uncommitted changes detected! They will be lost." - git status --short || true - fi - - echo "✅ Proceeding with git reset --hard" - } - - check_local_changes - + sudo chown -R deploy:deploy /home/prod/bots || true git fetch origin main git reset --hard origin/main + sudo chown -R deploy:deploy /home/prod/bots || true - # Исправляем права на файлы в bots директории после обновления - fix_bots_permissions - - # Проверяем, что изменения есть NEW_COMMIT=$(git rev-parse HEAD) - if [ "$CURRENT_COMMIT" = "$NEW_COMMIT" ]; then - echo "ℹ️ No new changes to deploy" - else - echo "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT" + echo "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT" + + # Валидация docker-compose + echo "🔍 Validating docker-compose configuration..." + docker-compose config > /dev/null || exit 1 + echo "✅ docker-compose.yml is valid" + + # Проверка дискового пространства + MIN_FREE_GB=5 + AVAILABLE_SPACE=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") + echo "💾 Available disk space: ${AVAILABLE_SPACE}GB" + + if [ "$AVAILABLE_SPACE" -lt "$MIN_FREE_GB" ]; then + echo "⚠️ Insufficient disk space! Cleaning up Docker resources..." + docker system prune -f --volumes || true fi - # Проверяем docker-compose файл - validate_docker_compose() { - local compose_file="docker-compose.yml" - - echo "🔍 Validating docker-compose configuration..." - - if [ ! -f "$compose_file" ]; then - echo "❌ $compose_file not found!" - exit 1 - fi - - if ! docker-compose config > /dev/null 2>&1; then - echo "❌ Invalid docker-compose.yml syntax!" - docker-compose config # Показываем ошибки - exit 1 - fi - - echo "✅ docker-compose.yml is valid" - } + # Сборка и запуск контейнеров (кроме ботов для ускорения деплоя) + echo "🔨 Rebuilding infrastructure containers (excluding bots)..." + docker-compose stop prometheus grafana uptime-kuma alertmanager || true - validate_docker_compose + export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN ANON_BOT_TOKEN + docker-compose build --pull prometheus grafana uptime-kuma alertmanager + docker-compose up -d prometheus grafana uptime-kuma alertmanager - # Проверяем дисковое пространство перед сборкой - check_disk_space() { - local min_free_gb=5 - local available_space=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") - - echo "💾 Checking disk space..." - echo "Available space: ${available_space}GB" - - if [ "$available_space" -lt "$min_free_gb" ]; then - echo "⚠️ Insufficient disk space! Need at least ${min_free_gb}GB, but only ${available_space}GB available" - echo "🧹 Attempting to clean up unused Docker resources..." - docker system prune -f --volumes || true - - # Проверяем снова после очистки - available_space=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") - echo "Available space after cleanup: ${available_space}GB" - - if [ "$available_space" -lt "$min_free_gb" ]; then - echo "❌ Still insufficient disk space after cleanup!" - exit 1 - fi - fi - - echo "✅ Sufficient disk space available" - } - - # Проверяем доступность памяти и CPU (опционально) - check_resources() { - echo "💻 Checking system resources..." - - # Проверяем доступную память (в MB) - available_mem=$(free -m 2>/dev/null | awk '/^Mem:/ {print $7}' || echo "0") - min_mem_mb=512 - - if [ "$available_mem" -lt "$min_mem_mb" ] && [ "$available_mem" -gt 0 ]; then - echo "⚠️ Low available memory: ${available_mem}MB (recommended: ${min_mem_mb}MB+)" - else - echo "✅ Available memory: ${available_mem}MB" - fi - - # Проверяем загрузку CPU (опционально) - load_avg=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//' || echo "0") - echo "📊 System load average: ${load_avg}" - } - - check_disk_space - check_resources - - # Пересобираем все контейнеры с обновлением базовых образов и кешированием - echo "🔨 Rebuilding all containers with --pull (updating base images, using cache)..." - cd /home/prod - - # Останавливаем все контейнеры с graceful shutdown (30 секунд на остановку) - echo "🛑 Stopping containers gracefully..." - docker-compose down -t 30 || true - - # Пересобираем все контейнеры с --pull (обновляет базовые образы, использует кеш слоев) - # Передаем токены из GitHub Secrets через переменные окружения - TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \ - TELEGRAM_TEST_BOT_TOKEN="$TELEGRAM_TEST_BOT_TOKEN" \ - ANON_BOT_TOKEN="$ANON_BOT_TOKEN" \ - docker-compose build --pull - - # Запускаем все контейнеры с токенами из Secrets - echo "🚀 Starting all containers..." - TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \ - TELEGRAM_TEST_BOT_TOKEN="$TELEGRAM_TEST_BOT_TOKEN" \ - ANON_BOT_TOKEN="$ANON_BOT_TOKEN" \ - docker-compose up -d - - echo "✅ Containers rebuilt and started" + echo "✅ Infrastructure containers rebuilt and started (bots remain running)" - name: Update deploy history if: always() @@ -351,52 +113,16 @@ jobs: key: ${{ secrets.SSH_PRIVATE_KEY }} port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} script: | - # Функция для безопасной записи в историю деплоев с использованием flock - # С fallback для файловых систем, которые не поддерживают flock (например, NFS) - safe_update_history_status() { - local new_status="$1" - local history_file="/home/prod/.deploy_history.txt" - local lock_file="${history_file}.lock" - - if command -v flock > /dev/null 2>&1; then - # Используем flock напрямую с файлом (работает в zsh и bash) - flock -x "$lock_file" sh -c " - if [ -f \"$history_file\" ]; then - # Заменяем последнюю строку со статусом deploying на финальный статус - sed -i '\$s/|deploying\$/|$new_status/' \"$history_file\" - echo \"✅ Deploy history updated with status: $new_status (with flock)\" - else - echo \"⚠️ History file not found, skipping update\" - fi - " || { - echo "⚠️ Failed to acquire lock, using fallback method" - # Fallback: простая запись без блокировки - if [ -f "$history_file" ]; then - sed -i '$s/|deploying$/|'"$new_status"'/' "$history_file" - echo "✅ Deploy history updated (fallback method)" - fi - } - else - # Fallback: если flock недоступен - echo "⚠️ flock not available, using simple update" - if [ -f "$history_file" ]; then - sed -i '$s/|deploying$/|'"$new_status"'/' "$history_file" - echo "✅ Deploy history updated (simple method)" - fi - fi - } + HISTORY_FILE="/home/prod/.deploy_history.txt" - DEPLOY_HISTORY="/home/prod/.deploy_history.txt" - - if [ -f "$DEPLOY_HISTORY" ]; then - # Обновляем последнюю запись со статусом deploying на success или failed - deploy_status="failed" + if [ -f "$HISTORY_FILE" ]; then + DEPLOY_STATUS="failed" if [ "${{ job.status }}" = "success" ]; then - deploy_status="success" + DEPLOY_STATUS="success" fi - # Обновляем статус безопасно - safe_update_history_status "$deploy_status" + sed -i '$s/|deploying$/|'"$DEPLOY_STATUS"'/' "$HISTORY_FILE" + echo "✅ Deploy history updated: $DEPLOY_STATUS" fi - name: Send deployment notification @@ -419,118 +145,12 @@ jobs: 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} continue-on-error: true - smoke-tests: + rollback: runs-on: ubuntu-latest - name: Smoke Tests - needs: deploy + name: Rollback to Previous Version if: | - always() && - needs.deploy.result == 'success' - - steps: - - name: Run Smoke Tests - uses: appleboy/ssh-action@v1.0.0 - with: - host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} - username: ${{ vars.SERVER_USER || secrets.SERVER_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} - script: | - set -e - export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" - export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}" - echo "🧪 Running smoke tests..." - - SMOKE_TEST_CHAT_ID="${SMOKE_TEST_CHAT_ID:--898316252}" - echo "📝 Using test chat ID: $SMOKE_TEST_CHAT_ID" - - # Проверка health endpoints - echo "🔍 Checking health endpoints..." - - if ! curl -f -s --max-time 10 "http://localhost:8080/health" > /dev/null 2>&1; then - echo "❌ Telegram Bot health endpoint failed" - exit 1 - fi - echo "✅ Telegram Bot health endpoint OK" - - if ! curl -f -s --max-time 10 "http://localhost:8081/health" > /dev/null 2>&1; then - echo "❌ AnonBot health endpoint failed" - exit 1 - fi - echo "✅ AnonBot health endpoint OK" - - # Проверка метрик (опционально) - echo "🔍 Checking metrics endpoints..." - curl -f -s --max-time 10 "http://localhost:8080/metrics" > /dev/null 2>&1 && echo "✅ Telegram Bot metrics OK" || echo "⚠️ Telegram Bot metrics not available" - curl -f -s --max-time 10 "http://localhost:8081/metrics" > /dev/null 2>&1 && echo "✅ AnonBot metrics OK" || echo "⚠️ AnonBot metrics not available" - - # Smoke-тест Telegram Helper Bot (используем токен из GitHub Secrets) - echo "🔍 Testing Telegram Helper Bot..." - if [ -z "$TELEGRAM_BOT_TOKEN" ]; then - echo "❌ TELEGRAM_BOT_TOKEN not found in GitHub Secrets" - exit 1 - fi - - # Отправляем сообщение "ping" в тестовый чат - response=$(curl -s --max-time 30 -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ - -d "chat_id=${SMOKE_TEST_CHAT_ID}" \ - -d "text=ping" || echo "") - - if echo "$response" | grep -q '"ok":true'; then - echo "✅ Telegram Helper Bot smoke test passed (message sent successfully)" - else - echo "❌ Telegram Helper Bot smoke test failed" - echo "Response: $response" - exit 1 - fi - - # Smoke-тест AnonBot (используем токен из GitHub Secrets) - echo "🔍 Testing AnonBot..." - if [ -n "$ANON_BOT_TOKEN" ]; then - response=$(curl -s --max-time 30 -X POST "https://api.telegram.org/bot${ANON_BOT_TOKEN}/sendMessage" \ - -d "chat_id=${SMOKE_TEST_CHAT_ID}" \ - -d "text=ping" || echo "") - - if echo "$response" | grep -q '"ok":true'; then - echo "✅ AnonBot smoke test passed (message sent successfully)" - else - echo "⚠️ AnonBot smoke test failed (non-critical)" - echo "Response: $response" - fi - else - echo "ℹ️ ANON_BOT_TOKEN not set, skipping smoke test" - fi - - echo "✅ All smoke tests passed!" - - - name: Send smoke tests notification - if: always() - uses: appleboy/telegram-action@v1.0.0 - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - message: | - ${{ job.status == 'success' && '✅' || '❌' }} Smoke Tests: ${{ job.status }} - - 📦 Repository: prod - 🌿 Branch: main - 📝 Commit: ${{ github.event.pull_request.merge_commit_sha || github.sha }} - - ${{ job.status == 'success' && '✅ All smoke tests passed! Bots are working correctly.' || '❌ Smoke tests failed! Auto-rollback will be triggered.' }} - - 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - continue-on-error: true - - auto-rollback: - runs-on: ubuntu-latest - name: Auto Rollback - concurrency: - group: production-rollback - cancel-in-progress: false - needs: [deploy, smoke-tests] - if: | - always() && - needs.smoke-tests.result == 'failure' + github.event_name == 'workflow_dispatch' && + github.event.inputs.action == 'rollback' environment: name: production @@ -540,7 +160,7 @@ jobs: with: ref: main - - name: Auto Rollback + - name: Rollback on server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} @@ -552,351 +172,87 @@ jobs: export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}" export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}" - echo "🔄 Starting automatic rollback after smoke tests failure..." - # Функция для безопасного чтения истории деплоев с использованием flock - # С fallback для файловых систем, которые не поддерживают flock (например, NFS) - safe_read_history() { - local history_file="/home/prod/.deploy_history.txt" - local lock_file="${history_file}.lock" - - if command -v flock > /dev/null 2>&1; then - # Используем flock напрямую с файлом для чтения - flock -s "$lock_file" sh -c " - if [ -f \"$history_file\" ]; then - cat \"$history_file\" - else - echo \"\" - fi - " || { - echo "⚠️ Failed to acquire lock, using fallback method" - # Fallback: простое чтение без блокировки - if [ -f "$history_file" ]; then - cat "$history_file" - else - echo "" - fi - } - else - # Fallback: если flock недоступен - if [ -f "$history_file" ]; then - cat "$history_file" - else - echo "" - fi - fi - } - - # Функция для безопасной записи в историю деплоев с использованием flock - safe_write_history() { - local entry="$1" - local history_file="/home/prod/.deploy_history.txt" - local lock_file="${history_file}.lock" - local history_size="${DEPLOY_HISTORY_SIZE:-10}" - - if command -v flock > /dev/null 2>&1; then - # Используем flock напрямую с файлом (работает в zsh и bash) - flock -x "$lock_file" sh -c " - # Записываем новую запись - echo \"$entry\" >> \"$history_file\" - - # Обрезаем файл атомарно - tail -n $history_size \"$history_file\" > \"${history_file}.tmp\" - mv \"${history_file}.tmp\" \"$history_file\" - - echo \"✅ History updated safely\" - " - else - # Fallback: простая запись без блокировки - echo "$entry" >> "$history_file" - tail -n "$history_size" "$history_file" > "${history_file}.tmp" - mv "${history_file}.tmp" "$history_file" - echo "✅ History updated (fallback method)" - fi - } - - # Функция для безопасного изменения прав на bots директорию - fix_bots_permissions() { - local bots_dir="/home/prod/bots" - - if [ ! -d "$bots_dir" ]; then - echo "⚠️ Bots directory not found, skipping permissions fix" - return 0 - fi - - echo "🔧 Fixing permissions for bots directory..." - sudo chown -R deploy:deploy "$bots_dir" || true - echo "✅ Permissions fixed" - } + echo "🔄 Starting rollback..." cd /home/prod - DEPLOY_HISTORY="/home/prod/.deploy_history.txt" - DEPLOY_HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" - # Находим последний успешный деплой из истории (безопасно) - HISTORY_CONTENT=$(safe_read_history) - LAST_SUCCESSFUL_COMMIT=$(echo "$HISTORY_CONTENT" | grep "|success" | tail -1 | cut -d'|' -f2 || echo "") + # Определяем коммит для отката + ROLLBACK_COMMIT="${{ github.event.inputs.rollback_commit }}" + HISTORY_FILE="/home/prod/.deploy_history.txt" - # Если нет успешного деплоя в истории, используем сохраненный коммит - if [ -z "$LAST_SUCCESSFUL_COMMIT" ]; then - if [ -f "/tmp/last_deploy_commit.txt" ]; then - LAST_SUCCESSFUL_COMMIT=$(cat /tmp/last_deploy_commit.txt) - echo "📝 Using saved commit from /tmp/last_deploy_commit.txt: $LAST_SUCCESSFUL_COMMIT" - else - echo "❌ No previous successful deploy found in history and no saved commit!" + if [ -z "$ROLLBACK_COMMIT" ]; then + echo "📝 No commit specified, finding last successful deploy..." + if [ -f "$HISTORY_FILE" ]; then + ROLLBACK_COMMIT=$(grep "|success$" "$HISTORY_FILE" | tail -1 | cut -d'|' -f2 || echo "") + fi + + if [ -z "$ROLLBACK_COMMIT" ]; then + echo "❌ No successful deploy found in history!" + echo "💡 Please specify commit hash manually or check deploy history" exit 1 fi - else - echo "📝 Found last successful deploy in history: $LAST_SUCCESSFUL_COMMIT" fi + echo "📝 Rolling back to commit: $ROLLBACK_COMMIT" + + # Проверяем, что коммит существует + if ! git cat-file -e "$ROLLBACK_COMMIT" 2>/dev/null; then + echo "❌ Commit $ROLLBACK_COMMIT not found!" + exit 1 + fi + + # Сохраняем текущий коммит + CURRENT_COMMIT=$(git rev-parse HEAD) + COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" "$ROLLBACK_COMMIT" || echo "Rollback") + TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") + + echo "📝 Current commit: $CURRENT_COMMIT" + echo "📝 Target commit: $ROLLBACK_COMMIT" + echo "📝 Commit message: $COMMIT_MESSAGE" + + # Исправляем права перед откатом + sudo chown -R deploy:deploy /home/prod/bots || true + # Откатываем код - echo "🔄 Rolling back to commit: $LAST_SUCCESSFUL_COMMIT" - - # Исправляем права на файлы в bots директории - fix_bots_permissions - - # Проверяем наличие локальных изменений перед reset - echo "🔍 Checking for local changes before rollback..." + echo "🔄 Rolling back code..." git fetch origin main + git reset --hard "$ROLLBACK_COMMIT" - # Проверяем uncommitted changes - if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then - echo "⚠️ Uncommitted changes detected! They will be lost during rollback." - git status --short || true + # Исправляем права после отката + sudo chown -R deploy:deploy /home/prod/bots || true + + echo "✅ Code rolled back: $CURRENT_COMMIT → $ROLLBACK_COMMIT" + + # Валидация docker-compose + echo "🔍 Validating docker-compose configuration..." + docker-compose config > /dev/null || exit 1 + echo "✅ docker-compose.yml is valid" + + # Проверка дискового пространства + MIN_FREE_GB=5 + AVAILABLE_SPACE=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") + echo "💾 Available disk space: ${AVAILABLE_SPACE}GB" + + if [ "$AVAILABLE_SPACE" -lt "$MIN_FREE_GB" ]; then + echo "⚠️ Insufficient disk space! Cleaning up Docker resources..." + docker system prune -f --volumes || true fi - git fetch origin main - git reset --hard "$LAST_SUCCESSFUL_COMMIT" + # Пересобираем и запускаем контейнеры (кроме ботов для ускорения отката) + echo "🔨 Rebuilding infrastructure containers (excluding bots)..." + docker-compose stop prometheus grafana uptime-kuma alertmanager || true - # Устанавливаем правильные права после отката - fix_bots_permissions + export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN ANON_BOT_TOKEN + docker-compose build --pull prometheus grafana uptime-kuma alertmanager + docker-compose up -d prometheus grafana uptime-kuma alertmanager - echo "✅ Code rolled back to: $LAST_SUCCESSFUL_COMMIT" + echo "✅ Infrastructure containers rebuilt and started (bots remain running)" - # Проверяем docker-compose файл - validate_docker_compose() { - local compose_file="docker-compose.yml" - - echo "🔍 Validating docker-compose configuration..." - - if [ ! -f "$compose_file" ]; then - echo "❌ $compose_file not found!" - exit 1 - fi - - if ! docker-compose config > /dev/null 2>&1; then - echo "❌ Invalid docker-compose.yml syntax!" - docker-compose config # Показываем ошибки - exit 1 - fi - - echo "✅ docker-compose.yml is valid" - } - - validate_docker_compose - - # Проверяем дисковое пространство перед сборкой - check_disk_space() { - local min_free_gb=5 - local available_space=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") - - echo "💾 Checking disk space..." - echo "Available space: ${available_space}GB" - - if [ "$available_space" -lt "$min_free_gb" ]; then - echo "⚠️ Insufficient disk space! Need at least ${min_free_gb}GB, but only ${available_space}GB available" - echo "🧹 Attempting to clean up unused Docker resources..." - docker system prune -f --volumes || true - - # Проверяем снова после очистки - available_space=$(df -BG /home/prod 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0") - echo "Available space after cleanup: ${available_space}GB" - - if [ "$available_space" -lt "$min_free_gb" ]; then - echo "❌ Still insufficient disk space after cleanup!" - exit 1 - fi - fi - - echo "✅ Sufficient disk space available" - } - - # Проверяем доступность памяти и CPU (опционально) - check_resources() { - echo "💻 Checking system resources..." - - # Проверяем доступную память (в MB) - available_mem=$(free -m 2>/dev/null | awk '/^Mem:/ {print $7}' || echo "0") - min_mem_mb=512 - - if [ "$available_mem" -lt "$min_mem_mb" ] && [ "$available_mem" -gt 0 ]; then - echo "⚠️ Low available memory: ${available_mem}MB (recommended: ${min_mem_mb}MB+)" - else - echo "✅ Available memory: ${available_mem}MB" - fi - - # Проверяем загрузку CPU (опционально) - load_avg=$(uptime 2>/dev/null | awk -F'load average:' '{print $2}' | awk '{print $1}' | sed 's/,//' || echo "0") - echo "📊 System load average: ${load_avg}" - } - - check_disk_space - check_resources - - # Пересобираем все контейнеры с обновлением базовых образов и кешированием - echo "🔨 Rebuilding all containers with --pull (updating base images, using cache)..." - - # Останавливаем все контейнеры с graceful shutdown (30 секунд на остановку) - echo "🛑 Stopping containers gracefully..." - docker-compose down -t 30 || true - - # Пересобираем с токенами из Secrets - TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \ - TELEGRAM_TEST_BOT_TOKEN="$TELEGRAM_TEST_BOT_TOKEN" \ - ANON_BOT_TOKEN="$ANON_BOT_TOKEN" \ - docker-compose build --pull - - # Запускаем с токенами из Secrets - echo "🚀 Starting all containers..." - TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" \ - TELEGRAM_TEST_BOT_TOKEN="$TELEGRAM_TEST_BOT_TOKEN" \ - ANON_BOT_TOKEN="$ANON_BOT_TOKEN" \ - docker-compose up -d - - echo "✅ Containers rebuilt and started" - - # Проверяем доступность сети - check_network_availability() { - echo "🔍 Checking network availability..." - - if ! ping -c 1 -W 2 localhost > /dev/null 2>&1; then - echo "❌ Localhost not reachable! Network issue detected." - return 1 - fi - - echo "✅ Network is available" - return 0 - } - - # Адаптивное ожидание готовности контейнеров - wait_for_containers_ready() { - local max_wait=180 # 3 минуты максимум - local check_interval=5 - local elapsed=0 - - echo "⏳ Waiting for containers to be ready..." - - while [ $elapsed -lt $max_wait ]; do - if docker-compose ps 2>/dev/null | grep -q "Exit\|Restarting"; then - echo "⏳ Some containers not ready yet, waiting ${check_interval}s... (${elapsed}/${max_wait}s)" - sleep $check_interval - elapsed=$((elapsed + check_interval)) - else - local running_count=$(docker-compose ps 2>/dev/null | grep -c "Up" || echo "0") - if [ "$running_count" -gt 0 ]; then - echo "✅ All containers are ready! (waited ${elapsed}s, ${running_count} containers running)" - return 0 - else - echo "⏳ Waiting for containers to start... (${elapsed}/${max_wait}s)" - sleep $check_interval - elapsed=$((elapsed + check_interval)) - fi - fi - done - - echo "⚠️ Containers not fully ready after ${max_wait}s, but continuing with health checks..." - return 0 - } - - # Проверяем сеть перед health checks - if ! check_network_availability; then - echo "⚠️ Network check failed, but continuing with health checks..." - fi - - # Ждем готовности контейнеров адаптивно - wait_for_containers_ready - - # Функция для проверки с экспоненциальным retry - check_health() { - local service=$1 - local url=$2 - local attempt=1 - local delays=(5 15 45) - local max_attempts=${#delays[@]} - - echo "🔍 Checking $service health..." - - while [ $attempt -le $max_attempts ]; do - if curl -f -s --max-time 10 "$url" > /dev/null 2>&1; then - echo "✅ $service is healthy (attempt $attempt/$max_attempts)" - return 0 - else - if [ $attempt -lt $max_attempts ]; then - delay=${delays[$((attempt - 1))]} - echo "⏳ $service not ready yet (attempt $attempt/$max_attempts), waiting ${delay} seconds..." - sleep $delay - else - echo "❌ $service health check failed after $max_attempts attempts" - return 1 - fi - fi - attempt=$((attempt + 1)) - done - - return 1 - } - - # Общая функция для проверки всех сервисов - run_health_checks() { - local failed=0 - local services=( - "Prometheus:http://localhost:9090/-/healthy:prometheus" - "Grafana:http://localhost:3000/api/health:grafana" - "Telegram Bot:http://localhost:8080/health:telegram-bot" - "AnonBot:http://localhost:8081/health:anon-bot" - ) - - for service_info in "${services[@]}"; do - IFS=':' read -r service_name url container_name <<< "$service_info" - echo "🔍 Checking $service_name..." - if ! check_health "$service_name" "$url"; then - echo "⚠️ $service_name health check failed" - docker-compose logs --tail=30 "$container_name" || true - failed=1 - fi - done - - return $failed - } - - HEALTH_CHECK_FAILED=0 - if ! run_health_checks; then - HEALTH_CHECK_FAILED=1 - fi - - # Проверяем статус всех контейнеров - echo "📊 Final container status:" - docker-compose ps - - # Проверяем, что все контейнеры запущены - FAILED_CONTAINERS=$(docker-compose ps | grep -c "Exit\|Restarting" || true) - if [ "$FAILED_CONTAINERS" -gt 0 ]; then - echo "❌ Some containers are not running properly" - docker-compose ps - HEALTH_CHECK_FAILED=1 - fi - - if [ $HEALTH_CHECK_FAILED -eq 1 ]; then - echo "⚠️ Some health checks failed, but rollback completed" - else - echo "✅ All health checks passed after rollback!" - fi - - # Обновляем историю безопасно - TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") - COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" "$LAST_SUCCESSFUL_COMMIT" 2>/dev/null || echo "Auto-rollback") - safe_write_history "${TIMESTAMP}|${LAST_SUCCESSFUL_COMMIT}|Auto-rollback after smoke tests failure|github-actions|rolled_back" + # Записываем в историю + echo "${TIMESTAMP}|${ROLLBACK_COMMIT}|Rollback to: ${COMMIT_MESSAGE}|github-actions|rolled_back" >> "$HISTORY_FILE" + HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" + tail -n "$HISTORY_SIZE" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE" echo "✅ Rollback completed successfully" @@ -907,16 +263,14 @@ jobs: to: ${{ secrets.TELEGRAM_CHAT_ID }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }} message: | - 🔄 Automatic Rollback: ${{ job.status }} + ${{ job.status == 'success' && '🔄' || '❌' }} Rollback: ${{ job.status }} 📦 Repository: prod 🌿 Branch: main - 📝 Rolled back to previous successful commit - ${{ github.event.pull_request.number && format('🔀 PR: #{0}', github.event.pull_request.number) || '' }} + 📝 Rolled back to: ${{ github.event.inputs.rollback_commit || 'Last successful commit' }} + 👤 Triggered by: ${{ github.actor }} - ⚠️ Rollback was triggered automatically due to smoke tests failure. - - ${{ job.status == 'success' && '✅ Rollback completed successfully! Services restored to previous version.' || '❌ Rollback failed! Manual intervention required.' }} + ${{ job.status == 'success' && '✅ Rollback completed successfully! Services restored to previous version.' || '❌ Rollback failed! Check logs for details.' }} 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} continue-on-error: true diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml deleted file mode 100644 index be698e9..0000000 --- a/.github/workflows/pipeline.yml +++ /dev/null @@ -1,451 +0,0 @@ -name: CI & CD pipeline - -on: - push: - branches: [ main, 'develop', 'dev-*', 'feature/**' ] - workflow_dispatch: - inputs: - action: - description: 'Action to perform' - required: true - type: choice - options: - - rollback - rollback_to_commit: - description: 'Commit hash to rollback to (optional, uses last deploy if empty)' - required: false - type: string - -jobs: - test: - runs-on: ubuntu-latest - name: Test & Code Quality - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python 3.11 - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r tests/infra/requirements-test.txt - pip install flake8 black isort mypy || true - - - name: Code formatting check (Black) - run: | - echo "🔍 Checking code formatting with Black..." - black --check . || (echo "❌ Code formatting issues found. Run 'black .' to fix." && exit 1) - - - name: Import sorting check (isort) - run: | - echo "🔍 Checking import sorting with isort..." - isort --check-only . || (echo "❌ Import sorting issues found. Run 'isort .' to fix." && exit 1) - - - name: Linting (flake8) - Critical errors - run: | - echo "🔍 Running flake8 linter (critical errors only)..." - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - - - name: Linting (flake8) - Warnings - run: | - echo "🔍 Running flake8 linter (warnings)..." - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics || true - continue-on-error: true - - - name: Run infrastructure tests - run: | - python -m pytest tests/infra/ -v --tb=short - - - name: Validate Prometheus config - run: | - python -m pytest tests/infra/test_prometheus_config.py -v - - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: | - .pytest_cache/ - htmlcov/ - retention-days: 7 - - - name: Send test failure notification - if: failure() - uses: appleboy/telegram-action@v1.0.0 - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - message: | - ❌ CI Tests Failed - - 📦 Repository: prod - 🌿 Branch: ${{ github.ref_name }} - 📝 Commit: ${{ github.sha }} - 👤 Author: ${{ github.actor }} - - ❌ Tests failed! Deployment blocked. Please fix the issues and try again. - - 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - continue-on-error: true - - create-pr: - runs-on: ubuntu-latest - name: Create Pull Request - needs: test - if: | - github.event_name == 'push' && - needs.test.result == 'success' && - github.ref_name != 'main' && - github.ref_name != 'develop' && - (startsWith(github.ref_name, 'dev-') || startsWith(github.ref_name, 'feature/')) - # Примечание: Для создания PR из той же ветки нужен PAT (Personal Access Token) - # Создайте PAT с правами repo и добавьте в Secrets как PAT - permissions: - contents: write - pull-requests: write - issues: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - persist-credentials: true - - - name: Check if PR already exists - id: check-pr - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - script: | - const branchName = context.ref.replace('refs/heads/', ''); - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - head: context.repo.owner + ':' + branchName, - base: 'main', - state: 'open' - }); - - if (prs.length > 0) { - core.setOutput('exists', 'true'); - core.setOutput('number', prs[0].number); - core.setOutput('url', prs[0].html_url); - } else { - core.setOutput('exists', 'false'); - } - - - name: Update existing PR - if: steps.check-pr.outputs.exists == 'true' - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - script: | - const prNumber = parseInt('${{ steps.check-pr.outputs.number }}'); - const branchName = context.ref.replace('refs/heads/', ''); - - const commitSha = '${{ github.sha }}'; - const author = '${{ github.actor }}'; - - const updateBody = '## Updated Changes\n\n' + - 'PR updated with new commits after successful CI tests.\n\n' + - '- Latest commit: ' + commitSha + '\n' + - '- Branch: `' + branchName + '`\n' + - '- Author: @' + author + '\n\n' + - '## Test Results\n\n' + - '✅ All tests passed successfully!\n\n' + - 'Please review the changes and merge when ready.'; - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber, - title: 'Merge ' + branchName + ' into main', - body: updateBody - }); - - console.log('✅ PR #' + prNumber + ' updated successfully'); - - - name: Create Pull Request - if: steps.check-pr.outputs.exists == 'false' - id: create-pr - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - script: | - const branchName = context.ref.replace('refs/heads/', ''); - - console.log('📝 Creating PR from ' + branchName + ' to main...'); - - try { - const commitSha = '${{ github.sha }}'; - const author = '${{ github.actor }}'; - - const prBody = '## Changes\n\n' + - 'This PR was automatically created after successful CI tests.\n\n' + - '- Branch: `' + branchName + '`\n' + - '- Commit: `' + commitSha + '`\n' + - '- Author: @' + author + '\n\n' + - '## Test Results\n\n' + - '✅ All tests passed successfully!\n\n' + - 'Please review the changes and merge when ready.'; - - const { data: pr } = await github.rest.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: 'Merge ' + branchName + ' into main', - head: branchName, - base: 'main', - body: prBody, - draft: false - }); - - // Добавляем labels - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: ['automated', 'ready-for-review'] - }); - } catch (labelError) { - console.log('⚠️ Could not add labels: ' + labelError.message); - } - - core.setOutput('number', pr.number.toString()); - core.setOutput('url', pr.html_url); - console.log('✅ PR #' + pr.number + ' created successfully: ' + pr.html_url); - - } catch (error) { - console.error('❌ Failed to create PR: ' + error.message); - if (error.status === 422) { - console.log('⚠️ PR might already exist or branch is up to date with base'); - // Пробуем найти существующий PR - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - head: context.repo.owner + ':' + branchName, - base: 'main', - state: 'open' - }); - - if (prs.length > 0) { - const pr = prs[0]; - core.setOutput('number', pr.number.toString()); - core.setOutput('url', pr.html_url); - console.log('✅ Found existing PR #' + pr.number + ': ' + pr.html_url); - } else { - core.setOutput('number', ''); - const serverUrl = '${{ github.server_url }}'; - const repo = '${{ github.repository }}'; - core.setOutput('url', serverUrl + '/' + repo + '/pulls'); - throw error; - } - } else { - throw error; - } - } - - - name: Send PR notification - PR exists - if: steps.check-pr.outputs.exists == 'true' - uses: appleboy/telegram-action@v1.0.0 - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - message: | - ℹ️ Pull Request Updated - - 📦 Repository: prod - 🌿 Branch: ${{ github.ref_name }} → main - 📝 Commit: ${{ github.sha }} - 👤 Author: ${{ github.actor }} - - ✅ All tests passed! PR #${{ steps.check-pr.outputs.number }} already exists and has been updated. - - 🔗 View PR: ${{ steps.check-pr.outputs.url }} - continue-on-error: true - - - name: Send PR notification - PR created - if: steps.check-pr.outputs.exists == 'false' && steps.create-pr.outcome == 'success' - uses: appleboy/telegram-action@v1.0.0 - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - message: | - 📝 Pull Request Created - - 📦 Repository: prod - 🌿 Branch: ${{ github.ref_name }} → main - 📝 Commit: ${{ github.sha }} - 👤 Author: ${{ github.actor }} - - ${{ steps.create-pr.outputs.number != '' && format('✅ All tests passed! Pull request #{0} has been created and is ready for review.', steps.create-pr.outputs.number) || '✅ All tests passed! Pull request has been created and is ready for review.' }} - - ${{ steps.create-pr.outputs.url != '' && format('🔗 View PR: {0}', steps.create-pr.outputs.url) || format('🔗 View PRs: {0}/{1}/pulls', github.server_url, github.repository) }} - continue-on-error: true - - rollback: - runs-on: ubuntu-latest - name: Manual Rollback - if: | - github.event_name == 'workflow_dispatch' && - github.event.inputs.action == 'rollback' - environment: - name: production - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: main - - - name: Manual Rollback - uses: appleboy/ssh-action@v1.0.0 - with: - host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} - username: ${{ vars.SERVER_USER || secrets.SERVER_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} - script: | - set -e - echo "🔄 Starting manual rollback..." - - cd /home/prod - DEPLOY_HISTORY="/home/prod/.deploy_history.txt" - DEPLOY_HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" - - # Определяем коммит для отката - if [ -n "${{ github.event.inputs.rollback_to_commit }}" ]; then - ROLLBACK_COMMIT="${{ github.event.inputs.rollback_to_commit }}" - echo "📝 Using specified commit: $ROLLBACK_COMMIT" - else - # Используем последний успешный деплой из истории - ROLLBACK_COMMIT=$(grep "|success" "$DEPLOY_HISTORY" 2>/dev/null | tail -1 | cut -d'|' -f2 || echo "") - - # Если нет в истории, используем сохраненный коммит - if [ -z "$ROLLBACK_COMMIT" ]; then - if [ -f "/tmp/last_deploy_commit.txt" ]; then - ROLLBACK_COMMIT=$(cat /tmp/last_deploy_commit.txt) - echo "📝 Using saved commit from /tmp/last_deploy_commit.txt: $ROLLBACK_COMMIT" - else - echo "❌ No commit specified and no previous deploy found!" - exit 1 - fi - else - echo "📝 Using last successful deploy from history: $ROLLBACK_COMMIT" - fi - fi - - # Проверяем что коммит существует - git fetch origin main - if ! git rev-parse --verify "$ROLLBACK_COMMIT" > /dev/null 2>&1; then - echo "❌ Commit $ROLLBACK_COMMIT not found!" - exit 1 - fi - - # Откатываем код - echo "🔄 Rolling back to commit: $ROLLBACK_COMMIT" - - # Исправляем права на файлы - sudo chown -R deploy:deploy /home/prod || true - - git reset --hard "$ROLLBACK_COMMIT" - - # Устанавливаем правильные права после отката - sudo chown -R deploy:deploy /home/prod || true - - echo "✅ Code rolled back to: $ROLLBACK_COMMIT" - - # Пересобираем все контейнеры с обновлением базовых образов и кешированием - echo "🔨 Rebuilding all containers with --pull (updating base images, using cache)..." - docker-compose down || true - docker-compose build --pull - docker-compose up -d - - echo "✅ Containers rebuilt and started" - - # Ждем запуска сервисов - echo "⏳ Waiting for services to start (45 seconds)..." - sleep 45 - - # Health checks с экспоненциальным retry - check_health() { - local service=$1 - local url=$2 - local attempt=1 - local delays=(5 15 45) - local max_attempts=${#delays[@]} - - echo "🔍 Checking $service health..." - - while [ $attempt -le $max_attempts ]; do - if curl -f -s --max-time 10 "$url" > /dev/null 2>&1; then - echo "✅ $service is healthy (attempt $attempt/$max_attempts)" - return 0 - else - if [ $attempt -lt $max_attempts ]; then - delay=${delays[$((attempt - 1))]} - echo "⏳ $service not ready yet (attempt $attempt/$max_attempts), waiting ${delay} seconds..." - sleep $delay - else - echo "❌ $service health check failed after $max_attempts attempts" - return 1 - fi - fi - attempt=$((attempt + 1)) - done - - return 1 - } - - HEALTH_CHECK_FAILED=0 - - check_health "Prometheus" "http://localhost:9090/-/healthy" || HEALTH_CHECK_FAILED=1 - check_health "Grafana" "http://localhost:3000/api/health" || HEALTH_CHECK_FAILED=1 - check_health "Telegram Bot" "http://localhost:8080/health" || HEALTH_CHECK_FAILED=1 - check_health "AnonBot" "http://localhost:8081/health" || HEALTH_CHECK_FAILED=1 - - if [ $HEALTH_CHECK_FAILED -eq 1 ]; then - echo "⚠️ Some health checks failed, but rollback completed" - else - echo "✅ All health checks passed after rollback!" - fi - - # Обновляем историю - TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") - COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" "$ROLLBACK_COMMIT" 2>/dev/null || echo "Manual rollback") - COMMIT_AUTHOR="${{ github.actor }}" - echo "${TIMESTAMP}|${ROLLBACK_COMMIT}|${COMMIT_MESSAGE}|${COMMIT_AUTHOR}|rolled_back" >> "$DEPLOY_HISTORY" - - # Оставляем только последние N записей - tail -n "$DEPLOY_HISTORY_SIZE" "$DEPLOY_HISTORY" > "${DEPLOY_HISTORY}.tmp" && mv "${DEPLOY_HISTORY}.tmp" "$DEPLOY_HISTORY" - - echo "✅ Rollback completed successfully" - - - name: Send rollback notification - if: always() - uses: appleboy/telegram-action@v1.0.0 - with: - to: ${{ secrets.TELEGRAM_CHAT_ID }} - token: ${{ secrets.TELEGRAM_BOT_TOKEN }} - message: | - 🔄 Manual Rollback: ${{ job.status }} - - 📦 Repository: prod - 🌿 Branch: main - 📝 Commit: ${{ github.event.inputs.rollback_to_commit || 'Previous successful deploy' }} - 👤 Author: ${{ github.actor }} - - ${{ job.status == 'success' && '✅ Rollback completed successfully! Services restored to specified version.' || '❌ Rollback failed! Check logs for details.' }} - - 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - continue-on-error: true