This commit is contained in:
16
scripts/backup_db.sh
Executable file
16
scripts/backup_db.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
DB_SERVICE="${DB_SERVICE:-db}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-drivers}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-drivers}"
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
OUT="${BACKUP_DIR}/carpass-${POSTGRES_DB}-${STAMP}.dump"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo "Creating database backup: $OUT"
|
||||
$COMPOSE exec -T "$DB_SERVICE" pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc > "$OUT"
|
||||
echo "Backup complete: $OUT"
|
||||
30
scripts/bootstrap_admin.py
Normal file
30
scripts/bootstrap_admin.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.session import async_session_factory
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
admin_ids = settings.admin_telegram_id_list
|
||||
if not admin_ids:
|
||||
raise SystemExit("ADMIN_TELEGRAM_IDS is empty")
|
||||
async with async_session_factory() as session:
|
||||
for telegram_id in admin_ids:
|
||||
result = await session.execute(select(User).where(User.telegram_id == telegram_id))
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
user = User(telegram_id=telegram_id, username=str(telegram_id), platform_role="admin")
|
||||
session.add(user)
|
||||
else:
|
||||
user.platform_role = "admin"
|
||||
await session.commit()
|
||||
print(f"Bootstrapped admins: {', '.join(str(item) for item in admin_ids)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
9
scripts/check_migrations.sh
Executable file
9
scripts/check_migrations.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "Checking Alembic migration chain..."
|
||||
python -m alembic heads
|
||||
python -m alembic current || true
|
||||
python -m alembic upgrade head
|
||||
python -m alembic current
|
||||
echo "Alembic migrations applied successfully."
|
||||
67
scripts/cleanup_jobs.py
Normal file
67
scripts/cleanup_jobs.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
|
||||
from app.db.session import async_session_factory
|
||||
from app.models.car import ServiceEmployee, ServiceNotification, ServiceVisit
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
now = datetime.now(UTC)
|
||||
async with async_session_factory() as session:
|
||||
expired = (
|
||||
await session.execute(
|
||||
select(ServiceEmployee).where(
|
||||
ServiceEmployee.status == "invited",
|
||||
ServiceEmployee.invite_expires_at.is_not(None),
|
||||
ServiceEmployee.invite_expires_at <= now,
|
||||
)
|
||||
)
|
||||
).scalars()
|
||||
expired_count = 0
|
||||
for employee in expired:
|
||||
employee.status = "expired"
|
||||
employee.invite_token = None
|
||||
expired_count += 1
|
||||
|
||||
abandoned_count = 0
|
||||
abandoned = (
|
||||
await session.execute(
|
||||
select(ServiceNotification).where(
|
||||
ServiceNotification.status.in_(["failed", "retrying"]),
|
||||
ServiceNotification.retry_count >= 5,
|
||||
ServiceNotification.created_at < now - timedelta(days=1),
|
||||
)
|
||||
)
|
||||
).scalars()
|
||||
for notification in abandoned:
|
||||
notification.status = "abandoned"
|
||||
abandoned_count += 1
|
||||
|
||||
old_notifications = await session.execute(
|
||||
delete(ServiceNotification).where(
|
||||
ServiceNotification.status == "abandoned",
|
||||
ServiceNotification.created_at < now - timedelta(days=30),
|
||||
)
|
||||
)
|
||||
orphan_drafts = await session.execute(
|
||||
delete(ServiceVisit).where(
|
||||
ServiceVisit.status == "draft",
|
||||
ServiceVisit.created_at < now - timedelta(days=90),
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
print(
|
||||
"Cleanup done: "
|
||||
f"expired_invites={expired_count}, "
|
||||
f"abandoned_notifications={abandoned_count}, "
|
||||
f"deleted_old_notifications={old_notifications.rowcount or 0}, "
|
||||
f"orphan_drafts={orphan_drafts.rowcount or 0}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
55
scripts/deploy.sh
Executable file
55
scripts/deploy.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/carpass/app}"
|
||||
BRANCH="${DEPLOY_BRANCH:-main}"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8000/ready}"
|
||||
BACKUP_BEFORE_DEPLOY="${BACKUP_BEFORE_DEPLOY:-false}"
|
||||
|
||||
if [[ ! -d "$APP_DIR/.git" ]]; then
|
||||
echo "Deploy directory is not a git repository: $APP_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
|
||||
if [[ ! -f ".env" ]]; then
|
||||
echo ".env is missing in $APP_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching $BRANCH..."
|
||||
git fetch origin "$BRANCH"
|
||||
git checkout "$BRANCH"
|
||||
git pull --ff-only origin "$BRANCH"
|
||||
|
||||
if [[ "$BACKUP_BEFORE_DEPLOY" == "true" ]]; then
|
||||
echo "Creating pre-deploy database backup..."
|
||||
./scripts/backup_db.sh
|
||||
fi
|
||||
|
||||
echo "Building and starting containers..."
|
||||
$COMPOSE build
|
||||
$COMPOSE up -d
|
||||
|
||||
echo "Applying migrations..."
|
||||
$COMPOSE exec -T api alembic upgrade head
|
||||
|
||||
echo "Running smoke checks..."
|
||||
$COMPOSE exec -T api python -m compileall -q app bot
|
||||
|
||||
echo "Running health check: $HEALTH_URL"
|
||||
for attempt in {1..30}; do
|
||||
if curl -fsS "$HEALTH_URL" >/tmp/carpass-ready.json; then
|
||||
cat /tmp/carpass-ready.json
|
||||
echo
|
||||
$COMPOSE ps
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "API readiness check failed" >&2
|
||||
$COMPOSE logs --tail=120 api >&2
|
||||
exit 1
|
||||
22
scripts/restore_db.sh
Executable file
22
scripts/restore_db.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 path/to/backup.dump" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BACKUP_FILE="$1"
|
||||
COMPOSE="${COMPOSE:-docker compose}"
|
||||
DB_SERVICE="${DB_SERVICE:-db}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-drivers}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-drivers}"
|
||||
|
||||
if [[ ! -f "$BACKUP_FILE" ]]; then
|
||||
echo "Backup file not found: $BACKUP_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Restoring $BACKUP_FILE into $POSTGRES_DB. This replaces database contents."
|
||||
cat "$BACKUP_FILE" | $COMPOSE exec -T "$DB_SERVICE" pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists
|
||||
echo "Restore complete"
|
||||
Reference in New Issue
Block a user