first implementation

This commit is contained in:
Wuesteon 2025-11-27 17:26:18 +01:00
parent 98efa6f6e8
commit 74dc6892ab
61 changed files with 30899 additions and 4934 deletions

170
scripts/deploy/build-and-push.sh Executable file
View file

@ -0,0 +1,170 @@
#!/bin/bash
# Build and push Docker images for manacore services
# Usage: ./build-and-push.sh [service] [tag]
# Example: ./build-and-push.sh chat-backend v1.0.0
# Example: ./build-and-push.sh all latest
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
DOCKER_REGISTRY=${DOCKER_REGISTRY:-"wuesteon"}
SERVICE=${1:-"all"}
TAG=${2:-"latest"}
PLATFORM=${PLATFORM:-"linux/amd64"}
# Function to print colored output
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to build and push a service
build_and_push() {
local service=$1
local dockerfile=$2
local context=${3:-.}
local image_name="${DOCKER_REGISTRY}/${service}"
log_info "Building ${service}..."
# Build the image
if docker buildx build \
--platform ${PLATFORM} \
--tag "${image_name}:${TAG}" \
--tag "${image_name}:latest" \
--file "${dockerfile}" \
--progress plain \
${context}; then
log_info "Successfully built ${service}"
# Push the image
log_info "Pushing ${service} to registry..."
if docker push "${image_name}:${TAG}" && docker push "${image_name}:latest"; then
log_info "Successfully pushed ${service}"
return 0
else
log_error "Failed to push ${service}"
return 1
fi
else
log_error "Failed to build ${service}"
return 1
fi
}
# Function to build all services
build_all() {
local services=(
"mana-core-auth:services/mana-core-auth/Dockerfile"
"maerchenzauber-backend:apps/maerchenzauber/apps/backend/Dockerfile"
"chat-backend:apps/chat/apps/backend/Dockerfile"
"manadeck-backend:apps/manadeck/apps/backend/Dockerfile"
"nutriphi-backend:apps/nutriphi/apps/backend/Dockerfile"
"news-api:apps/news/apps/api/Dockerfile"
)
local failed_services=()
for service_config in "${services[@]}"; do
IFS=':' read -r service dockerfile <<< "$service_config"
if [ -f "$dockerfile" ]; then
if ! build_and_push "$service" "$dockerfile" "."; then
failed_services+=("$service")
fi
else
log_warn "Dockerfile not found for ${service}: ${dockerfile}"
fi
echo ""
done
# Report results
echo ""
echo "=========================================="
if [ ${#failed_services[@]} -eq 0 ]; then
log_info "All services built and pushed successfully!"
return 0
else
log_error "Failed to build/push the following services:"
for service in "${failed_services[@]}"; do
echo " - ${service}"
done
return 1
fi
}
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed or not in PATH"
exit 1
fi
# Check if buildx is available
if ! docker buildx version &> /dev/null; then
log_error "Docker buildx is not available"
log_info "Install it with: docker buildx install"
exit 1
fi
# Login to Docker registry
if [ -n "${DOCKER_USERNAME}" ] && [ -n "${DOCKER_PASSWORD}" ]; then
log_info "Logging in to Docker registry..."
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
fi
# Main execution
log_info "Starting build and push process..."
log_info "Registry: ${DOCKER_REGISTRY}"
log_info "Tag: ${TAG}"
log_info "Platform: ${PLATFORM}"
echo ""
if [ "$SERVICE" == "all" ]; then
build_all
else
# Build specific service
case "$SERVICE" in
"mana-core-auth")
build_and_push "mana-core-auth" "services/mana-core-auth/Dockerfile" "."
;;
"maerchenzauber-backend")
build_and_push "maerchenzauber-backend" "apps/maerchenzauber/apps/backend/Dockerfile" "."
;;
"chat-backend")
build_and_push "chat-backend" "apps/chat/apps/backend/Dockerfile" "."
;;
"manadeck-backend")
build_and_push "manadeck-backend" "apps/manadeck/apps/backend/Dockerfile" "."
;;
"nutriphi-backend")
build_and_push "nutriphi-backend" "apps/nutriphi/apps/backend/Dockerfile" "."
;;
"news-api")
build_and_push "news-api" "apps/news/apps/api/Dockerfile" "."
;;
*)
log_error "Unknown service: $SERVICE"
echo "Available services: all, mana-core-auth, maerchenzauber-backend, chat-backend, manadeck-backend, nutriphi-backend, news-api"
exit 1
;;
esac
fi
log_info "Build and push process completed!"

201
scripts/deploy/deploy-hetzner.sh Executable file
View file

@ -0,0 +1,201 @@
#!/bin/bash
# Deploy to Hetzner server via SSH
# Usage: ./deploy-hetzner.sh [environment] [service]
# Example: ./deploy-hetzner.sh staging all
# Example: ./deploy-hetzner.sh production chat-backend
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
ENVIRONMENT=${1:-"staging"}
SERVICE=${2:-"all"}
# Environment-specific variables
if [ "$ENVIRONMENT" == "production" ]; then
SSH_HOST=${PRODUCTION_HOST}
SSH_USER=${PRODUCTION_USER}
SSH_KEY=${PRODUCTION_SSH_KEY}
DEPLOY_DIR="~/manacore-production"
COMPOSE_FILE="docker-compose.production.yml"
else
SSH_HOST=${STAGING_HOST}
SSH_USER=${STAGING_USER}
SSH_KEY=${STAGING_SSH_KEY}
DEPLOY_DIR="~/manacore-staging"
COMPOSE_FILE="docker-compose.staging.yml"
fi
# Function to print colored output
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Validate required variables
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ]; then
log_error "SSH configuration missing for ${ENVIRONMENT}"
log_error "Please set: ${ENVIRONMENT^^}_HOST and ${ENVIRONMENT^^}_USER"
exit 1
fi
# SSH command helper
ssh_exec() {
if [ -n "$SSH_KEY" ]; then
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "$@"
else
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "$@"
fi
}
# SCP command helper
scp_copy() {
if [ -n "$SSH_KEY" ]; then
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no "$@"
else
scp -o StrictHostKeyChecking=no "$@"
fi
}
log_info "Starting deployment to ${ENVIRONMENT}..."
log_info "Target: ${SSH_USER}@${SSH_HOST}"
log_info "Service: ${SERVICE}"
echo ""
# Step 1: Prepare deployment directory
log_info "Preparing deployment directory..."
ssh_exec << EOF
mkdir -p ${DEPLOY_DIR}
mkdir -p ${DEPLOY_DIR}/logs
mkdir -p ${DEPLOY_DIR}/backups
cd ${DEPLOY_DIR}
EOF
# Step 2: Copy docker-compose file
log_info "Copying docker-compose configuration..."
scp_copy "${COMPOSE_FILE}" "${SSH_USER}@${SSH_HOST}:${DEPLOY_DIR}/docker-compose.yml"
# Step 3: Copy environment file if exists
if [ -f ".env.${ENVIRONMENT}" ]; then
log_info "Copying environment configuration..."
scp_copy ".env.${ENVIRONMENT}" "${SSH_USER}@${SSH_HOST}:${DEPLOY_DIR}/.env"
else
log_warn "No .env.${ENVIRONMENT} file found, using existing environment"
fi
# Step 4: Pull latest images
log_info "Pulling latest Docker images..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose pull ${SERVICE}
EOF
# Step 5: Run migrations if needed
if [ "$SERVICE" == "all" ] || [ "$SERVICE" == "mana-core-auth" ]; then
log_info "Running database migrations..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose run --rm mana-core-auth pnpm run db:migrate || echo "Migrations completed or skipped"
EOF
fi
# Step 6: Deploy services
log_info "Deploying services..."
if [ "$SERVICE" == "all" ]; then
# Zero-downtime rolling update for all services
ssh_exec << 'EOF'
cd ${DEPLOY_DIR}
SERVICES=$(docker compose config --services)
for service in $SERVICES; do
echo "Deploying $service..."
# Scale up with new version
docker compose up -d --no-deps --scale $service=2 $service
sleep 15
# Scale down to single instance
docker compose up -d --no-deps --scale $service=1 $service
sleep 5
done
# Cleanup old images
docker image prune -f
EOF
else
# Deploy single service
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose up -d --no-deps ${SERVICE}
sleep 10
EOF
fi
# Step 7: Health checks
log_info "Running health checks..."
HEALTH_ENDPOINTS=(
"mana-core-auth:3001:/api/v1/health"
"maerchenzauber-backend:3002:/health"
"chat-backend:3002:/api/health"
)
FAILED_CHECKS=0
for endpoint in "${HEALTH_ENDPOINTS[@]}"; do
IFS=':' read -r service port path <<< "$endpoint"
log_info "Checking health of ${service}..."
if ssh_exec << EOF
HEALTH=\$(docker compose -f ${DEPLOY_DIR}/docker-compose.yml exec -T ${service} wget -q -O - http://localhost:${port}${path} 2>/dev/null || echo "FAILED")
if [[ "\$HEALTH" == *"FAILED"* ]]; then
echo "Health check failed for ${service}"
exit 1
else
echo "Health check passed for ${service}"
exit 0
fi
EOF
then
log_info "${service} is healthy"
else
log_error "${service} health check failed"
((FAILED_CHECKS++))
fi
done
echo ""
# Step 8: Display service status
log_info "Current service status:"
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose ps
EOF
echo ""
# Final result
if [ $FAILED_CHECKS -eq 0 ]; then
log_info "Deployment to ${ENVIRONMENT} completed successfully! ✅"
exit 0
else
log_error "Deployment completed with ${FAILED_CHECKS} failed health checks"
log_warn "Please check service logs with: ssh ${SSH_USER}@${SSH_HOST} 'cd ${DEPLOY_DIR} && docker compose logs'"
exit 1
fi

88
scripts/deploy/health-check.sh Executable file
View file

@ -0,0 +1,88 @@
#!/bin/bash
# Health check script for deployed services
# Usage: ./health-check.sh [environment]
# Example: ./health-check.sh staging
# Example: ./health-check.sh production
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ENVIRONMENT=${1:-"staging"}
# Environment-specific configuration
if [ "$ENVIRONMENT" == "production" ]; then
BASE_URL=${PRODUCTION_API_URL:-"https://api.manacore.app"}
else
BASE_URL=${STAGING_API_URL:-"https://staging.manacore.app"}
fi
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Health check endpoints
declare -A ENDPOINTS=(
["Mana Core Auth"]="/api/v1/health"
["Maerchenzauber Backend"]="/health"
["Chat Backend"]="/api/health"
)
# Counter for failed checks
FAILED=0
TOTAL=0
log_info "Running health checks for ${ENVIRONMENT}..."
log_info "Base URL: ${BASE_URL}"
echo ""
# Check each endpoint
for service in "${!ENDPOINTS[@]}"; do
endpoint="${ENDPOINTS[$service]}"
url="${BASE_URL}${endpoint}"
((TOTAL++))
log_info "Checking ${service}..."
# Make HTTP request
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${url}" -m 10 || echo "000")
if [ "$HTTP_CODE" == "200" ]; then
log_info "${service}: OK (HTTP ${HTTP_CODE})"
else
log_error "${service}: FAILED (HTTP ${HTTP_CODE})"
((FAILED++))
fi
echo ""
done
# Summary
echo "=========================================="
log_info "Health Check Summary:"
echo " Total checks: ${TOTAL}"
echo " Passed: $((TOTAL - FAILED))"
echo " Failed: ${FAILED}"
echo "=========================================="
if [ $FAILED -eq 0 ]; then
log_info "All health checks passed! ✅"
exit 0
else
log_error "${FAILED} health check(s) failed ❌"
exit 1
fi

116
scripts/deploy/migrate-db.sh Executable file
View file

@ -0,0 +1,116 @@
#!/bin/bash
# Database migration script for Supabase projects
# Usage: ./migrate-db.sh [project] [environment]
# Example: ./migrate-db.sh chat staging
# Example: ./migrate-db.sh mana-core-auth production
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PROJECT=${1}
ENVIRONMENT=${2:-"staging"}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Validate input
if [ -z "$PROJECT" ]; then
log_error "Project name is required"
echo "Usage: ./migrate-db.sh [project] [environment]"
echo "Available projects: chat, maerchenzauber, manadeck, memoro, picture, nutriphi, news, mana-core-auth"
exit 1
fi
log_info "Running database migrations for ${PROJECT} (${ENVIRONMENT})..."
# Set Supabase environment variables based on project and environment
case "$PROJECT" in
"chat")
if [ "$ENVIRONMENT" == "production" ]; then
SUPABASE_URL=${CHAT_SUPABASE_URL}
SUPABASE_SERVICE_KEY=${CHAT_SUPABASE_SERVICE_KEY}
else
SUPABASE_URL=${STAGING_CHAT_SUPABASE_URL}
SUPABASE_SERVICE_KEY=${STAGING_CHAT_SUPABASE_SERVICE_KEY}
fi
MIGRATION_DIR="apps/chat/supabase/migrations"
;;
"maerchenzauber")
if [ "$ENVIRONMENT" == "production" ]; then
SUPABASE_URL=${MAERCHENZAUBER_SUPABASE_URL}
SUPABASE_SERVICE_KEY=${MAERCHENZAUBER_SUPABASE_SERVICE_KEY}
else
SUPABASE_URL=${STAGING_MAERCHENZAUBER_SUPABASE_URL}
SUPABASE_SERVICE_KEY=${STAGING_MAERCHENZAUBER_SUPABASE_SERVICE_KEY}
fi
MIGRATION_DIR="apps/maerchenzauber/supabase/migrations"
;;
"mana-core-auth")
if [ "$ENVIRONMENT" == "production" ]; then
DATABASE_URL=${PRODUCTION_AUTH_DATABASE_URL}
else
DATABASE_URL=${STAGING_AUTH_DATABASE_URL}
fi
MIGRATION_DIR="services/mana-core-auth/src/db/migrations"
# Use Drizzle for mana-core-auth
log_info "Running Drizzle migrations for mana-core-auth..."
cd services/mana-core-auth
pnpm run db:migrate
exit 0
;;
*)
log_error "Unknown project: $PROJECT"
exit 1
;;
esac
# Check if migration directory exists
if [ ! -d "$MIGRATION_DIR" ]; then
log_warn "No migrations found for ${PROJECT}"
exit 0
fi
# Check for Supabase CLI
if ! command -v supabase &> /dev/null; then
log_error "Supabase CLI is not installed"
log_info "Install it with: npm install -g supabase"
exit 1
fi
# Link to remote project
log_info "Linking to Supabase project..."
supabase link --project-ref $(echo $SUPABASE_URL | sed 's|https://||' | sed 's|.supabase.co||')
# Run migrations
log_info "Applying migrations from ${MIGRATION_DIR}..."
cd $MIGRATION_DIR
# List pending migrations
log_info "Pending migrations:"
ls -1 *.sql 2>/dev/null || log_info "No SQL migrations found"
# Apply migrations using Supabase CLI
for migration in *.sql; do
if [ -f "$migration" ]; then
log_info "Applying migration: $migration"
supabase db push
fi
done
log_info "Database migrations completed successfully! ✅"

213
scripts/deploy/rollback.sh Executable file
View file

@ -0,0 +1,213 @@
#!/bin/bash
# Rollback script for emergency deployment rollback
# Usage: ./rollback.sh [environment] [service]
# Example: ./rollback.sh production all
# Example: ./rollback.sh staging chat-backend
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
ENVIRONMENT=${1:-"staging"}
SERVICE=${2:-"all"}
# Environment-specific variables
if [ "$ENVIRONMENT" == "production" ]; then
SSH_HOST=${PRODUCTION_HOST}
SSH_USER=${PRODUCTION_USER}
SSH_KEY=${PRODUCTION_SSH_KEY}
DEPLOY_DIR="~/manacore-production"
else
SSH_HOST=${STAGING_HOST}
SSH_USER=${STAGING_USER}
SSH_KEY=${STAGING_SSH_KEY}
DEPLOY_DIR="~/manacore-staging"
fi
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Validate required variables
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ]; then
log_error "SSH configuration missing for ${ENVIRONMENT}"
exit 1
fi
# SSH command helper
ssh_exec() {
if [ -n "$SSH_KEY" ]; then
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "$@"
else
ssh -o StrictHostKeyChecking=no "${SSH_USER}@${SSH_HOST}" "$@"
fi
}
log_warn "⚠️ ROLLBACK INITIATED ⚠️"
log_info "Environment: ${ENVIRONMENT}"
log_info "Service: ${SERVICE}"
echo ""
# Confirm rollback
read -p "Are you sure you want to rollback? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
log_info "Rollback cancelled"
exit 0
fi
echo ""
log_info "Starting rollback process..."
# Step 1: Check for previous deployment backup
log_info "Checking for previous deployment backup..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
if [ ! -d "backups" ] || [ -z "\$(ls -A backups)" ]; then
echo "ERROR: No backups found!"
exit 1
fi
# Get latest backup
LATEST_BACKUP=\$(ls -t backups | head -1)
echo "Latest backup found: \$LATEST_BACKUP"
cd backups/\$LATEST_BACKUP
# Verify backup contents
if [ ! -f "docker-compose.yml" ]; then
echo "ERROR: Backup incomplete - missing docker-compose.yml"
exit 1
fi
echo "Backup validated"
EOF
# Step 2: Stop current services
log_info "Stopping current services..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose stop ${SERVICE}
EOF
# Step 3: Restore from backup
log_info "Restoring from backup..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
LATEST_BACKUP=\$(ls -t backups | head -1)
# Restore docker-compose file
cp backups/\$LATEST_BACKUP/docker-compose.yml ./docker-compose.yml
# Restore environment file if exists
if [ -f "backups/\$LATEST_BACKUP/.env.backup" ]; then
cp backups/\$LATEST_BACKUP/.env.backup ./.env
fi
echo "Files restored from backup: \$LATEST_BACKUP"
EOF
# Step 4: Restore database if service is auth
if [ "$SERVICE" == "all" ] || [ "$SERVICE" == "mana-core-auth" ]; then
log_info "Restoring database..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
LATEST_BACKUP=\$(ls -t backups | head -1)
if [ -f "backups/\$LATEST_BACKUP/postgres_backup.sql" ]; then
# Restore PostgreSQL backup
docker compose exec -T postgres psql -U \$POSTGRES_USER < backups/\$LATEST_BACKUP/postgres_backup.sql
echo "Database restored"
else
echo "WARNING: No database backup found"
fi
EOF
fi
# Step 5: Start services with previous images
log_info "Starting services with previous configuration..."
ssh_exec << EOF
cd ${DEPLOY_DIR}
# Get image tags from backup
LATEST_BACKUP=\$(ls -t backups | head -1)
if [ -f "backups/\$LATEST_BACKUP/deployment_images.txt" ]; then
echo "Previous deployment images:"
cat backups/\$LATEST_BACKUP/deployment_images.txt
fi
# Start services
docker compose up -d ${SERVICE}
# Wait for services to start
sleep 20
EOF
# Step 6: Health checks
log_info "Running health checks after rollback..."
HEALTH_ENDPOINTS=(
"mana-core-auth:3001:/api/v1/health"
"maerchenzauber-backend:3002:/health"
"chat-backend:3002:/api/health"
)
FAILED_CHECKS=0
for endpoint in "${HEALTH_ENDPOINTS[@]}"; do
IFS=':' read -r service port path <<< "$endpoint"
if ssh_exec << EOF
HEALTH=\$(docker compose -f ${DEPLOY_DIR}/docker-compose.yml exec -T ${service} wget -q -O - http://localhost:${port}${path} 2>/dev/null || echo "FAILED")
if [[ "\$HEALTH" == *"FAILED"* ]]; then
exit 1
else
exit 0
fi
EOF
then
log_info "${service} is healthy"
else
log_warn "⚠️ ${service} health check failed"
((FAILED_CHECKS++))
fi
done
echo ""
# Step 7: Display service status
log_info "Current service status:"
ssh_exec << EOF
cd ${DEPLOY_DIR}
docker compose ps
EOF
echo ""
# Final result
if [ $FAILED_CHECKS -eq 0 ]; then
log_info "Rollback completed successfully! ✅"
log_info "Services have been restored to previous version"
exit 0
else
log_error "Rollback completed with ${FAILED_CHECKS} failed health checks"
log_warn "Manual intervention may be required"
exit 1
fi