diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..783144c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: CI pipeline + +on: + push: + branches: [ 'dev-*', 'feature-*' ] + pull_request: + branches: [ 'dev-*', 'feature-*', 'main' ] + workflow_dispatch: + +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 requirements.txt + pip install -r requirements-dev.txt + + - 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 || true + + - 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 tests + run: | + echo "🧪 Running tests..." + python -m pytest tests/ -v --tb=short + + - 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: telegram-helper-bot + 🌿 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: telegram-helper-bot + 🌿 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 new file mode 100644 index 0000000..d26d3cd --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,356 @@ +name: Deploy to Production + +on: + 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-telegram-helper-bot + cancel-in-progress: false + environment: + name: production + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: main + + - name: Deploy to server + 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 }}" + + echo "🚀 Starting deployment to production..." + + 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: $CURRENT_COMMIT" + echo "📝 Commit message: $COMMIT_MESSAGE" + echo "📝 Author: $COMMIT_AUTHOR" + + # Записываем в историю деплоев + HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.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" + + # Обновляем код + echo "📥 Pulling latest changes from main..." + sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true + cd /home/prod/bots/telegram-helper-bot + git fetch origin main + git reset --hard origin/main + sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true + + NEW_COMMIT=$(git rev-parse HEAD) + echo "✅ Code updated: $CURRENT_COMMIT → $NEW_COMMIT" + + # Применяем миграции БД перед перезапуском контейнера + echo "🔄 Applying database migrations..." + DB_PATH="/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db" + + if [ -f "$DB_PATH" ]; then + cd /home/prod/bots/telegram-helper-bot + python3 scripts/apply_migrations.py --db "$DB_PATH" || { + echo "❌ Ошибка при применении миграций!" + exit 1 + } + echo "✅ Миграции применены успешно" + else + echo "⚠️ База данных не найдена, пропускаем миграции (будет создана при первом запуске)" + fi + + # Валидация docker-compose + echo "🔍 Validating docker-compose configuration..." + cd /home/prod + 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 + + # Пересобираем и перезапускаем контейнер бота + echo "🔨 Rebuilding and restarting telegram-bot container..." + cd /home/prod + + export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN + docker-compose stop telegram-bot || true + docker-compose build --pull telegram-bot + docker-compose up -d telegram-bot + + echo "✅ Telegram bot container rebuilt and started" + + # Ждем немного и проверяем healthcheck + echo "⏳ Waiting for container to start..." + sleep 10 + + if docker ps | grep -q bots_telegram_bot; then + echo "✅ Container is running" + else + echo "❌ Container failed to start!" + docker logs bots_telegram_bot --tail 50 || true + exit 1 + fi + + - name: Update deploy history + if: always() + 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: | + HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.txt" + + if [ -f "$HISTORY_FILE" ]; then + DEPLOY_STATUS="failed" + if [ "${{ job.status }}" = "success" ]; then + DEPLOY_STATUS="success" + fi + + sed -i '$s/|deploying$/|'"$DEPLOY_STATUS"'/' "$HISTORY_FILE" + echo "✅ Deploy history updated: $DEPLOY_STATUS" + fi + + - name: Send deployment 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' && '✅' || '❌' }} Deployment: ${{ job.status }} + + 📦 Repository: telegram-helper-bot + 🌿 Branch: main + 📝 Commit: ${{ github.sha }} + 👤 Author: ${{ github.actor }} + + ${{ job.status == 'success' && '✅ Deployment successful! Container restarted with migrations applied.' || '❌ Deployment failed! Check logs for details.' }} + + 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + continue-on-error: true + + - name: Get PR body from merged PR + if: job.status == 'success' && github.event_name == 'push' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🔍 Searching for merged PR associated with commit ${{ github.sha }}..." + + # Находим последний мерженный PR для main ветки по merge commit SHA + COMMIT_SHA="${{ github.sha }}" + PR_NUMBER=$(gh pr list --state merged --base main --limit 10 --json number,mergeCommit --jq ".[] | select(.mergeCommit.oid == \"$COMMIT_SHA\") | .number" | head -1) + + # Если не нашли по merge commit, ищем последний мерженный PR + if [ -z "$PR_NUMBER" ]; then + echo "⚠️ PR not found by merge commit, trying to get latest merged PR..." + PR_NUMBER=$(gh pr list --state merged --base main --limit 1 --json number --jq '.[0].number') + fi + + if [ -n "$PR_NUMBER" ] && [ "$PR_NUMBER" != "null" ]; then + echo "✅ Found PR #$PR_NUMBER" + PR_BODY=$(gh pr view $PR_NUMBER --json body --jq '.body // ""') + + if [ -n "$PR_BODY" ] && [ "$PR_BODY" != "null" ]; then + echo "PR_BODY<> $GITHUB_ENV + echo "$PR_BODY" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV + echo "✅ PR body extracted successfully" + else + echo "⚠️ PR body is empty" + fi + else + echo "⚠️ No merged PR found for this commit" + fi + continue-on-error: true + + - name: Send PR body to important logs + if: job.status == 'success' && github.event_name == 'push' && env.PR_BODY != '' + uses: appleboy/telegram-action@v1.0.0 + with: + to: ${{ secrets.IMPORTANT_LOGS_CHAT }} + token: ${{ secrets.TELEGRAM_BOT_TOKEN }} + message: | + 📋 Pull Request Description (PR #${{ env.PR_NUMBER }}): + + ${{ env.PR_BODY }} + + 🔗 PR: ${{ github.server_url }}/${{ github.repository }}/pull/${{ env.PR_NUMBER }} + 📝 Commit: ${{ github.sha }} + continue-on-error: true + + rollback: + runs-on: ubuntu-latest + name: Rollback to Previous Version + 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: Rollback on server + 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 }}" + + echo "🔄 Starting rollback..." + + cd /home/prod + + # Определяем коммит для отката + ROLLBACK_COMMIT="${{ github.event.inputs.rollback_commit }}" + HISTORY_FILE="/home/prod/.deploy_history_telegram_helper_bot.txt" + + 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 + fi + + echo "📝 Rolling back to commit: $ROLLBACK_COMMIT" + + # Проверяем, что коммит существует + cd /home/prod/bots/telegram-helper-bot + 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/telegram-helper-bot || true + + # Откатываем код + echo "🔄 Rolling back code..." + git fetch origin main + git reset --hard "$ROLLBACK_COMMIT" + + # Исправляем права после отката + sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true + + echo "✅ Code rolled back: $CURRENT_COMMIT → $ROLLBACK_COMMIT" + + # Валидация docker-compose + echo "🔍 Validating docker-compose configuration..." + cd /home/prod + 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 + + # Пересобираем и перезапускаем контейнер + echo "🔨 Rebuilding and restarting telegram-bot container..." + cd /home/prod + + export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN + docker-compose stop telegram-bot || true + docker-compose build --pull telegram-bot + docker-compose up -d telegram-bot + + echo "✅ Telegram bot container rebuilt and started" + + # Записываем в историю + 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" + + - 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: | + ${{ job.status == 'success' && '🔄' || '❌' }} Rollback: ${{ job.status }} + + 📦 Repository: telegram-helper-bot + 🌿 Branch: main + 📝 Rolled back to: ${{ github.event.inputs.rollback_commit || 'Last successful commit' }} + 👤 Triggered by: ${{ github.actor }} + + ${{ 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