harden deploy reports and admin alerts

This commit is contained in:
VPN SaaS Dev
2026-05-18 18:17:53 +09:00
parent 2d5695fdce
commit 22b9b40d78
12 changed files with 549 additions and 31 deletions

96
scripts/rsync_deploy.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -Eeuo pipefail
REMOTE="${REMOTE:-root@drivers.smartsoltech.kr}"
REMOTE_DIR="${REMOTE_DIR:-/opt/drivers_bot}"
BASE_URL="${BASE_URL:-http://127.0.0.1:8000}"
COMPOSE="${COMPOSE:-docker compose}"
RUN_LOCAL_CHECKS="${RUN_LOCAL_CHECKS:-true}"
BACKUP_BEFORE_DEPLOY="${BACKUP_BEFORE_DEPLOY:-true}"
BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
REVISION="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
EXCLUDES=(
"--exclude=.git/"
"--exclude=.env"
"--exclude=.env.*"
"--exclude=.venv/"
"--exclude=venv/"
"--exclude=__pycache__/"
"--exclude=.pytest_cache/"
"--exclude=.ruff_cache/"
"--exclude=.history/"
"--exclude=backups/"
"--exclude=*.sqlite"
"--exclude=*.sqlite3"
"--exclude=*.db"
)
send_remote_report() {
local text="$1"
ssh "$REMOTE" "cd '$REMOTE_DIR' && CARPASS_REPORT_TEXT=\$(cat) $COMPOSE exec -T -e CARPASS_REPORT_TEXT api python scripts/send_telegram_report.py" <<<"$text" || true
}
fail_report() {
local line="${1:-unknown}"
send_remote_report "❌ CarPass rsync deploy failed
Branch: $BRANCH
Revision: $REVISION
Step line: $line
Target: $REMOTE"
}
trap 'fail_report "$LINENO"' ERR
if [[ "$RUN_LOCAL_CHECKS" == "true" ]]; then
echo "Running local checks..."
.venv/bin/ruff check app bot tests
.venv/bin/pytest -q
fi
send_remote_report "🚀 CarPass rsync deploy started
Branch: $BRANCH
Revision: $REVISION
Target: $REMOTE
Checks: local=${RUN_LOCAL_CHECKS}"
echo "Checking remote..."
ssh "$REMOTE" "test -d '$REMOTE_DIR' && test -f '$REMOTE_DIR/docker-compose.yml'"
if [[ "$BACKUP_BEFORE_DEPLOY" == "true" ]]; then
echo "Creating remote code backup..."
ssh "$REMOTE" "cd '$(dirname "$REMOTE_DIR")' && mkdir -p '$REMOTE_DIR/backups' && tar --exclude='$(basename "$REMOTE_DIR")/backups' --exclude='$(basename "$REMOTE_DIR")/.git' --exclude='$(basename "$REMOTE_DIR")/.env' --exclude='$(basename "$REMOTE_DIR")/.venv' -czf '$REMOTE_DIR/backups/code_pre_rsync_$(date +%Y%m%d%H%M%S).tgz' '$(basename "$REMOTE_DIR")'"
fi
echo "Syncing code with rsync..."
rsync -az --delete "${EXCLUDES[@]}" ./ "$REMOTE:$REMOTE_DIR/"
echo "Building remote images..."
ssh "$REMOTE" "cd '$REMOTE_DIR' && $COMPOSE build"
send_remote_report "🧱 CarPass rsync deploy progress
Branch: $BRANCH
Step: docker build completed"
echo "Applying migrations..."
ssh "$REMOTE" "cd '$REMOTE_DIR' && $COMPOSE run --rm api alembic upgrade head"
send_remote_report "🗄️ CarPass rsync deploy progress
Branch: $BRANCH
Step: migrations applied"
echo "Starting services..."
ssh "$REMOTE" "cd '$REMOTE_DIR' && $COMPOSE up -d"
echo "Waiting for API readiness..."
ssh "$REMOTE" "cd '$REMOTE_DIR' && for i in \$(seq 1 30); do status=\$(docker inspect -f '{{.State.Health.Status}}' drivers_bot-api-1 2>/dev/null || echo missing); echo \"api_health=\$status\"; [ \"\$status\" = healthy ] && exit 0; sleep 2; done; $COMPOSE logs --tail=120 api; exit 1"
echo "Running remote smoke tests..."
ssh "$REMOTE" "cd '$REMOTE_DIR' && BASE_URL='$BASE_URL' ./scripts/smoke_test.sh && curl -fsSI '$BASE_URL/admin.html' | head -5 && $COMPOSE ps"
send_remote_report "✅ CarPass rsync deploy completed
Branch: $BRANCH
Revision: $REVISION
Migration: $(ssh "$REMOTE" "cd '$REMOTE_DIR' && curl -fsS '$BASE_URL/ready'" | tr '\n' ' ')
Checks: /health ok, /ready ok, /metrics ok, /admin.html 200
Services: api healthy, bot restarted"
echo "Deploy completed."

81
scripts/send_telegram_report.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import asyncio
import os
from collections.abc import Iterable
import httpx
from sqlalchemy import select
from app.core.config import settings
from app.db.session import async_session_factory
from app.models import car, expense, gamification, push # noqa: F401
from app.models.user import User
REPORT_ROLES = {"admin", "super_admin", "moderator", "support"}
def env_recipients() -> list[str]:
recipients: list[str] = []
if settings.admin_notification_chat_id:
recipients.append(settings.admin_notification_chat_id)
recipients.extend(str(item) for item in settings.admin_telegram_id_list)
return recipients
async def db_recipients() -> list[str]:
async with async_session_factory() as session:
result = await session.execute(
select(User.telegram_id).where(User.platform_role.in_(REPORT_ROLES))
)
return [str(row[0]) for row in result.all() if row[0]]
def unique(values: Iterable[str]) -> list[str]:
return list(dict.fromkeys(item.strip() for item in values if item and item.strip()))
async def send_report(text: str, *, dry_run: bool = False) -> int:
recipients = unique([*env_recipients(), *(await db_recipients())])
if dry_run:
print(f"telegram_report_dry_run recipients={len(recipients)}")
return len(recipients)
if not settings.bot_token or not recipients:
print("telegram_report_skipped")
return 0
sent = 0
async with httpx.AsyncClient(timeout=10) as client:
for chat_id in recipients:
try:
response = await client.post(
f"https://api.telegram.org/bot{settings.bot_token}/sendMessage",
json={"chat_id": chat_id, "text": text, "disable_web_page_preview": True},
)
response.raise_for_status()
sent += 1
except Exception as exc: # noqa: BLE001 - deploy report must never fail deploy
print(f"telegram_report_failed chat_id={chat_id} error={type(exc).__name__}")
print(f"telegram_report_sent_count {sent}")
return sent
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Send a CarPass operational Telegram report.")
parser.add_argument("--text", help="Report text. Defaults to CARPASS_REPORT_TEXT.")
parser.add_argument("--dry-run", action="store_true")
return parser.parse_args()
async def main() -> None:
args = parse_args()
text = args.text or os.getenv("CARPASS_REPORT_TEXT") or ""
if not text.strip():
raise SystemExit("Report text is required")
await send_report(text, dry_run=args.dry_run)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -13,4 +13,10 @@ echo
echo "Checking metrics..."
curl -fsS "$BASE_URL/metrics" | grep -q "carpass_requests_total"
for path in / /sto.html /admin.html /work_order.html; do
echo "Checking static page $path..."
curl -fsSI "$BASE_URL$path" | grep -q "200 OK"
done
echo "Smoke test passed."