refactor: упростил скрипты deploy.yml и ci.yml

This commit is contained in:
2026-01-25 22:24:12 +03:00
parent 804ecd6107
commit 4d328444bd
3 changed files with 225 additions and 1223 deletions

99
.github/workflows/ci.yml vendored Normal file
View File

@@ -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

View File

@@ -1,21 +1,32 @@
name: Deploy to Production name: Deploy to Production
on: on:
pull_request: push:
types: [closed]
branches: [ main ] branches: [ main ]
workflow_dispatch: 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: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Deploy to Production name: Deploy to Production
if: |
github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'deploy')
concurrency: concurrency:
group: production-deploy group: production-deploy
cancel-in-progress: false cancel-in-progress: false
if: |
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
github.event_name == 'workflow_dispatch'
environment: environment:
name: production name: production
@@ -25,101 +36,6 @@ jobs:
with: with:
ref: main 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 - name: Deploy to server
uses: appleboy/ssh-action@v1.0.0 uses: appleboy/ssh-action@v1.0.0
with: with:
@@ -133,214 +49,60 @@ jobs:
export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}" export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}"
export ANON_BOT_TOKEN="${{ secrets.ANON_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..." 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 cd /home/prod
# Сохраняем текущий коммит для отката # Сохраняем информацию о коммите
CURRENT_COMMIT=$(git rev-parse HEAD) CURRENT_COMMIT=$(git rev-parse HEAD)
COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" || echo "Unknown") COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s" || echo "Unknown")
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an" || echo "Unknown") COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an" || echo "Unknown")
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S") TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
# Сохраняем для быстрого доступа
echo "$CURRENT_COMMIT" > /tmp/last_deploy_commit.txt
echo "📝 Current commit: $CURRENT_COMMIT" echo "📝 Current commit: $CURRENT_COMMIT"
echo "📝 Commit message: $COMMIT_MESSAGE" echo "📝 Commit message: $COMMIT_MESSAGE"
echo "📝 Author: $COMMIT_AUTHOR" echo "📝 Author: $COMMIT_AUTHOR"
# Сохраняем в файл истории деплоев безопасно # Записываем в историю деплоев
DEPLOY_HISTORY="/home/prod/.deploy_history.txt" HISTORY_FILE="/home/prod/.deploy_history.txt"
DEPLOY_HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}" 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..." echo "📥 Pulling latest changes from main..."
sudo chown -R deploy:deploy /home/prod/bots || true
# Исправляем права на файлы в 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
git fetch origin main git fetch origin main
git reset --hard 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) 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"
fi
# Проверяем docker-compose файл
validate_docker_compose() {
local compose_file="docker-compose.yml"
# Валидация docker-compose
echo "🔍 Validating docker-compose configuration..." echo "🔍 Validating docker-compose configuration..."
docker-compose config > /dev/null || exit 1
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 "✅ docker-compose.yml is valid"
}
validate_docker_compose # Проверка дискового пространства
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
check_disk_space() { echo "⚠️ Insufficient disk space! Cleaning up Docker resources..."
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 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 fi
echo "✅ Sufficient disk space available" # Сборка и запуск контейнеров (кроме ботов для ускорения деплоя)
} echo "🔨 Rebuilding infrastructure containers (excluding bots)..."
docker-compose stop prometheus grafana uptime-kuma alertmanager || true
# Проверяем доступность памяти и CPU (опционально) export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN ANON_BOT_TOKEN
check_resources() { docker-compose build --pull prometheus grafana uptime-kuma alertmanager
echo "💻 Checking system resources..." docker-compose up -d prometheus grafana uptime-kuma alertmanager
# Проверяем доступную память (в MB) echo "✅ Infrastructure containers rebuilt and started (bots remain running)"
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"
- name: Update deploy history - name: Update deploy history
if: always() if: always()
@@ -351,52 +113,16 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }} key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }} port: ${{ vars.SSH_PORT || secrets.SSH_PORT || 22 }}
script: | script: |
# Функция для безопасной записи в историю деплоев с использованием flock HISTORY_FILE="/home/prod/.deploy_history.txt"
# С 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 if [ -f "$HISTORY_FILE" ]; then
# Используем flock напрямую с файлом (работает в zsh и bash) DEPLOY_STATUS="failed"
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
}
DEPLOY_HISTORY="/home/prod/.deploy_history.txt"
if [ -f "$DEPLOY_HISTORY" ]; then
# Обновляем последнюю запись со статусом deploying на success или failed
deploy_status="failed"
if [ "${{ job.status }}" = "success" ]; then if [ "${{ job.status }}" = "success" ]; then
deploy_status="success" DEPLOY_STATUS="success"
fi fi
# Обновляем статус безопасно sed -i '$s/|deploying$/|'"$DEPLOY_STATUS"'/' "$HISTORY_FILE"
safe_update_history_status "$deploy_status" echo "✅ Deploy history updated: $DEPLOY_STATUS"
fi fi
- name: Send deployment notification - name: Send deployment notification
@@ -419,118 +145,12 @@ jobs:
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
continue-on-error: true continue-on-error: true
smoke-tests: rollback:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Smoke Tests name: Rollback to Previous Version
needs: deploy
if: | if: |
always() && github.event_name == 'workflow_dispatch' &&
needs.deploy.result == 'success' github.event.inputs.action == 'rollback'
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'
environment: environment:
name: production name: production
@@ -540,7 +160,7 @@ jobs:
with: with:
ref: main ref: main
- name: Auto Rollback - name: Rollback on server
uses: appleboy/ssh-action@v1.0.0 uses: appleboy/ssh-action@v1.0.0
with: with:
host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }} host: ${{ vars.SERVER_HOST || secrets.SERVER_HOST }}
@@ -552,351 +172,87 @@ jobs:
export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}" export TELEGRAM_BOT_TOKEN="${{ secrets.TELEGRAM_BOT_TOKEN }}"
export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}" export TELEGRAM_TEST_BOT_TOKEN="${{ secrets.TELEGRAM_TEST_BOT_TOKEN }}"
export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}" export ANON_BOT_TOKEN="${{ secrets.ANON_BOT_TOKEN }}"
echo "🔄 Starting automatic rollback after smoke tests failure..."
# Функция для безопасного чтения истории деплоев с использованием flock echo "🔄 Starting rollback..."
# С 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"
}
cd /home/prod cd /home/prod
DEPLOY_HISTORY="/home/prod/.deploy_history.txt"
DEPLOY_HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}"
# Находим последний успешный деплой из истории (безопасно) # Определяем коммит для отката
HISTORY_CONTENT=$(safe_read_history) ROLLBACK_COMMIT="${{ github.event.inputs.rollback_commit }}"
LAST_SUCCESSFUL_COMMIT=$(echo "$HISTORY_CONTENT" | grep "|success" | tail -1 | cut -d'|' -f2 || echo "") HISTORY_FILE="/home/prod/.deploy_history.txt"
# Если нет успешного деплоя в истории, используем сохраненный коммит if [ -z "$ROLLBACK_COMMIT" ]; then
if [ -z "$LAST_SUCCESSFUL_COMMIT" ]; then echo "📝 No commit specified, finding last successful deploy..."
if [ -f "/tmp/last_deploy_commit.txt" ]; then if [ -f "$HISTORY_FILE" ]; then
LAST_SUCCESSFUL_COMMIT=$(cat /tmp/last_deploy_commit.txt) ROLLBACK_COMMIT=$(grep "|success$" "$HISTORY_FILE" | tail -1 | cut -d'|' -f2 || echo "")
echo "📝 Using saved commit from /tmp/last_deploy_commit.txt: $LAST_SUCCESSFUL_COMMIT" fi
else
echo "❌ No previous successful deploy found in history and no saved commit!" if [ -z "$ROLLBACK_COMMIT" ]; then
echo "❌ No successful deploy found in history!"
echo "💡 Please specify commit hash manually or check deploy history"
exit 1 exit 1
fi fi
else
echo "📝 Found last successful deploy in history: $LAST_SUCCESSFUL_COMMIT"
fi 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" echo "🔄 Rolling back code..."
# Исправляем права на файлы в bots директории
fix_bots_permissions
# Проверяем наличие локальных изменений перед reset
echo "🔍 Checking for local changes before rollback..."
git fetch origin main 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 sudo chown -R deploy:deploy /home/prod/bots || true
echo "⚠️ Uncommitted changes detected! They will be lost during rollback."
git status --short || true
fi
git fetch origin main echo "✅ Code rolled back: $CURRENT_COMMIT → $ROLLBACK_COMMIT"
git reset --hard "$LAST_SUCCESSFUL_COMMIT"
# Устанавливаем правильные права после отката
fix_bots_permissions
echo "✅ Code rolled back to: $LAST_SUCCESSFUL_COMMIT"
# Проверяем docker-compose файл
validate_docker_compose() {
local compose_file="docker-compose.yml"
# Валидация docker-compose
echo "🔍 Validating docker-compose configuration..." echo "🔍 Validating docker-compose configuration..."
docker-compose config > /dev/null || exit 1
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 "✅ docker-compose.yml is valid"
}
validate_docker_compose # Проверка дискового пространства
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
check_disk_space() { echo "⚠️ Insufficient disk space! Cleaning up Docker resources..."
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 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 fi
echo "✅ Sufficient disk space available" # Пересобираем и запускаем контейнеры (кроме ботов для ускорения отката)
} echo "🔨 Rebuilding infrastructure containers (excluding bots)..."
docker-compose stop prometheus grafana uptime-kuma alertmanager || true
# Проверяем доступность памяти и CPU (опционально) export TELEGRAM_BOT_TOKEN TELEGRAM_TEST_BOT_TOKEN ANON_BOT_TOKEN
check_resources() { docker-compose build --pull prometheus grafana uptime-kuma alertmanager
echo "💻 Checking system resources..." docker-compose up -d prometheus grafana uptime-kuma alertmanager
# Проверяем доступную память (в MB) echo "✅ Infrastructure containers rebuilt and started (bots remain running)"
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+)" echo "${TIMESTAMP}|${ROLLBACK_COMMIT}|Rollback to: ${COMMIT_MESSAGE}|github-actions|rolled_back" >> "$HISTORY_FILE"
else HISTORY_SIZE="${DEPLOY_HISTORY_SIZE:-10}"
echo "✅ Available memory: ${available_mem}MB" tail -n "$HISTORY_SIZE" "$HISTORY_FILE" > "${HISTORY_FILE}.tmp" && mv "${HISTORY_FILE}.tmp" "$HISTORY_FILE"
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 "✅ Rollback completed successfully" echo "✅ Rollback completed successfully"
@@ -907,16 +263,14 @@ jobs:
to: ${{ secrets.TELEGRAM_CHAT_ID }} to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }} token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: | message: |
🔄 Automatic Rollback: ${{ job.status }} ${{ job.status == 'success' && '🔄' || '❌' }} Rollback: ${{ job.status }}
📦 Repository: prod 📦 Repository: prod
🌿 Branch: main 🌿 Branch: main
📝 Rolled back to previous successful commit 📝 Rolled back to: ${{ github.event.inputs.rollback_commit || 'Last successful commit' }}
${{ github.event.pull_request.number && format('🔀 PR: #{0}', github.event.pull_request.number) || '' }} 👤 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! Check logs for details.' }}
${{ job.status == 'success' && '✅ Rollback completed successfully! Services restored to previous version.' || '❌ Rollback failed! Manual intervention required.' }}
🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 View details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
continue-on-error: true continue-on-error: true

View File

@@ -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