name: Deploy to Production on: push: branches: [ master ] 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 dry_run: description: 'Dry run (only for deploy — no SSH, only show planned steps)' required: true type: choice default: no options: - no - yes 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') env: DRY_RUN: ${{ github.event.inputs.dry_run == 'yes' }} concurrency: group: production-deploy-telegram-helper-bot cancel-in-progress: false environment: name: production steps: - name: Checkout code uses: actions/checkout@v4 with: ref: master - name: Dry run (simulate deploy steps) if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'yes' run: | echo "🔍 DRY RUN — no SSH, no changes on server" echo "Would run on server:" echo " 1. cd /home/prod/bots/telegram-helper-bot" echo " 2. Backup DB → database/tg-bot-database_YYYYMMDD-HHMMSS.db (удаляется при успехе)" echo " 3. CURRENT_COMMIT + history; git fetch origin master && git reset --hard origin/master" echo " 4. apply_migrations.py (бэкап БД делается в шаге 1, при успехе удаляется в конце)" echo " 5. docker-compose -f /home/prod/docker-compose.yml config (validate)" echo " 6. docker-compose stop telegram-bot; build --pull telegram-bot; up -d telegram-bot" echo " 7. sleep 10; check container bots_telegram_bot" echo "" echo "Secrets/vars required: SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY, SSH_PORT, TELEGRAM_BOT_TOKEN, TELEGRAM_TEST_BOT_TOKEN" if [ -f docker-compose.yml ]; then echo "✅ docker-compose.yml present in repo (validation would run on server from /home/prod)" fi - name: Deploy to server if: github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'yes' 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..." DB_PATH="/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db" DB_DIR="/home/prod/bots/telegram-helper-bot/database" BACKUP_FILE="" sudo chown -R deploy:deploy /home/prod/bots/telegram-helper-bot || true cd /home/prod/bots/telegram-helper-bot # Бэкап БД в самом начале; при успешном деплое удалим в конце if [ -f "$DB_PATH" ]; then echo "💾 Creating database backup (before any changes)..." BACKUP_NAME="tg-bot-database_$(date +%Y%m%d-%H%M%S).db" BACKUP_FILE="${DB_DIR}/${BACKUP_NAME}" cp "$DB_PATH" "$BACKUP_FILE" && echo "✅ Backup: $BACKUP_FILE" || { echo "❌ Backup failed!"; exit 1; } fi # Сохраняем информацию о коммите (до pull) — из репо telegram-helper-bot 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 master..." git fetch origin master git reset --hard origin/master 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..." 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" # Успешный деплой — удаляем бэкап (при ошибке на любом шаге бэкап остаётся для rollback) if [ -n "${BACKUP_FILE:-}" ] && [ -f "$BACKUP_FILE" ]; then rm -f "$BACKUP_FILE" && echo "✅ Backup removed (deploy success)" fi else echo "❌ Container failed to start!" docker logs bots_telegram_bot --tail 50 || true exit 1 fi - name: Update deploy history if: always() && env.DRY_RUN != 'true' 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() && env.DRY_RUN != 'true' 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: master 📝 Commit: ${{ github.sha }} 👤 Author: ${{ github.actor }} ${{ job.status == 'success' && '✅ Deployment successful! Container restarted with migrations applied.' || '❌ Deployment failed! Check logs for details.' }} 🔗 View details: ${{ vars.GITEA_PUBLIC_URL || 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.DRY_RUN != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "🔍 Searching for merged PR associated with commit ${{ github.sha }}..." # Находим последний мерженный PR для master по merge commit SHA COMMIT_SHA="${{ github.sha }}" PR_NUMBER=$(gh pr list --state merged --base master --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 master --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.DRY_RUN != 'true' && 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: ${{ vars.GITEA_PUBLIC_URL || 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: master - 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 master 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: master 📝 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: ${{ vars.GITEA_PUBLIC_URL || github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} continue-on-error: true