diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 0513b6f..f3cd2b4 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -137,7 +137,7 @@ jobs: script: | cd /opt/${{ secrets.APP_NAME || 'sharenet' }} - # Pull latest code from repository (includes docker-compose.yml and nginx config) + # Pull latest code from repository (includes scripts and docker-compose.yml) git pull origin main # Create environment file for this deployment @@ -145,9 +145,14 @@ jobs: echo "REGISTRY=${{ secrets.CI_HOST }}:5000" >> .env echo "IMAGE_NAME=${{ secrets.APP_NAME || 'sharenet' }}" >> .env echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}" >> .env + echo "POSTGRES_USER=${{ secrets.POSTGRES_USER || 'sharenet' }}" >> .env + echo "POSTGRES_DB=${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER || 'sharenet' }}:${{ secrets.POSTGRES_PASSWORD || 'your_secure_password_here' }}@postgres:5432/${{ secrets.POSTGRES_DB || 'sharenet' }}" >> .env echo "NODE_ENV=production" >> .env echo "RUST_LOG=info" >> .env - # Run deployment using the repository's docker-compose.yml - sudo ./deploy.sh \ No newline at end of file + # Make scripts executable + chmod +x scripts/*.sh + + # Run deployment using the new deployment script + ./scripts/deploy.sh deploy \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 2b0ce7a..ea19ebd 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -30,6 +30,7 @@ FROM debian:bookworm-slim RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ + wget \ && rm -rf /var/lib/apt/lists/* # Create non-root user @@ -55,7 +56,7 @@ EXPOSE 3001 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:3001/health || exit 1 + CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1 # Run the application CMD ["./sharenet-api"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 60b081f..d964cbe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: postgres: condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] interval: 30s timeout: 10s retries: 3 diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..e8b5c8b --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Sharenet Production Deployment Script +# This script handles safe production deployments with migrations + +set -e + +# Configuration +APP_NAME="sharenet" +BACKUP_DIR="backups" +MIGRATION_SCRIPT="./scripts/migrate.sh" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Pre-deployment checks +pre_deployment_checks() { + log_info "Running pre-deployment checks..." + + # Check if migration script exists + if [ ! -f "$MIGRATION_SCRIPT" ]; then + log_error "Migration script not found: $MIGRATION_SCRIPT" + log_error "Current directory: $(pwd)" + log_error "Available files in scripts/:" + ls -la scripts/ 2>/dev/null || log_error "scripts/ directory not found" + exit 1 + fi + + # Check if migration script is executable + if [ ! -x "$MIGRATION_SCRIPT" ]; then + log_info "Making migration script executable" + chmod +x "$MIGRATION_SCRIPT" + fi + + # Check database connection + if ! "$MIGRATION_SCRIPT" status >/dev/null 2>&1; then + log_error "Cannot connect to database" + log_error "Running migration script with verbose output:" + "$MIGRATION_SCRIPT" status + exit 1 + fi + + log_success "Pre-deployment checks passed" +} + +# Create backup +create_backup() { + log_info "Creating database backup..." + mkdir -p "$BACKUP_DIR" + + if "$MIGRATION_SCRIPT" backup; then + log_success "Backup created successfully" + else + log_error "Backup failed" + exit 1 + fi +} + +# Run migrations +run_migrations() { + log_info "Running database migrations..." + + if "$MIGRATION_SCRIPT" run; then + log_success "Migrations completed successfully" + else + log_error "Migrations failed" + log_warning "Consider restoring from backup" + exit 1 + fi +} + +# Deploy application +deploy_application() { + log_info "Deploying application..." + + # Pull latest images + docker-compose pull + + # Stop current services + docker-compose down + + # Start services with new images + docker-compose up -d + + # Wait for services to be healthy + log_info "Waiting for services to be healthy..." + local max_attempts=60 # 5 minutes with 5-second intervals + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + if docker-compose ps | grep -q "healthy"; then + log_success "All services are healthy" + return 0 + fi + + log_info "Waiting for services to become healthy... (attempt $((attempt + 1))/$max_attempts)" + sleep 5 + attempt=$((attempt + 1)) + done + + log_error "Services failed to become healthy within timeout" + log_error "Current service status:" + docker-compose ps + log_error "Recent logs:" + docker-compose logs --tail=50 + exit 1 +} + +# Rollback function +rollback() { + log_warning "Rolling back deployment..." + + # Stop services + docker-compose down + + # Restore from backup + local latest_backup=$(ls -t "$BACKUP_DIR"/backup_*.sql 2>/dev/null | head -1) + if [ -n "$latest_backup" ]; then + log_info "Restoring from backup: $latest_backup" + "$MIGRATION_SCRIPT" restore "$latest_backup" + else + log_warning "No backup found for rollback" + fi + + # Start services with previous images + docker-compose up -d + + log_warning "Rollback completed" +} + +# Main deployment process +main() { + local command="${1:-deploy}" + + case "$command" in + deploy) + log_info "Starting production deployment..." + pre_deployment_checks + create_backup + run_migrations + deploy_application + log_success "Production deployment completed successfully" + ;; + rollback) + log_warning "Starting rollback process..." + rollback + log_success "Rollback completed successfully" + ;; + check) + log_info "Running deployment checks..." + pre_deployment_checks + "$MIGRATION_SCRIPT" status + log_success "All checks passed" + ;; + *) + log_error "Unknown command: $command" + echo "Usage: $0 {deploy|rollback|check}" + exit 1 + ;; + esac +} + +# Handle interrupts gracefully +trap 'log_error "Deployment interrupted"; exit 1' INT TERM + +# Run main function +main "$@" \ No newline at end of file diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100644 index 0000000..87c2def --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,317 @@ +#!/bin/bash + +# Sharenet Database Migration Script +# This script handles database migrations in production environments + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +MIGRATIONS_DIR="backend/migrations" +BACKUP_DIR="backups" +DATABASE_URL="${DATABASE_URL:-}" +DRY_RUN="${DRY_RUN:-false}" +VERBOSE="${VERBOSE:-false}" + +# Functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +show_help() { + cat << EOF +Sharenet Database Migration Script + +Usage: $0 [COMMAND] [OPTIONS] + +Commands: + run Run pending migrations + status Show migration status + create Create a new migration file + revert Revert the last migration + backup Create a backup before migration + restore Restore from backup + +Options: + --dry-run Show what would be done without executing + --verbose Show detailed output + --help Show this help message + +Environment Variables: + DATABASE_URL Database connection string + DRY_RUN Set to 'true' for dry run mode + VERBOSE Set to 'true' for verbose output + +Examples: + $0 run # Run pending migrations + $0 status # Show migration status + $0 create add_user_table # Create new migration + DRY_RUN=true $0 run # Dry run migrations +EOF +} + +check_dependencies() { + log_info "Checking dependencies..." + + if ! command -v sqlx &> /dev/null; then + log_error "sqlx CLI not found. Install with: cargo install sqlx-cli" + exit 1 + fi + + if [ -z "$DATABASE_URL" ]; then + log_error "DATABASE_URL environment variable is required" + exit 1 + fi + + log_success "Dependencies check passed" +} + +check_connection() { + log_info "Testing database connection..." + + # Check if we're running in Docker environment + if [ -f /.dockerenv ] || [ -f /proc/1/cgroup ] && grep -q docker /proc/1/cgroup; then + log_info "Detected Docker environment" + # In Docker, use the service name as host + if [[ "$DATABASE_URL" == *"localhost"* ]] || [[ "$DATABASE_URL" == *"127.0.0.1"* ]]; then + log_info "Updating DATABASE_URL for Docker environment" + export DATABASE_URL=$(echo "$DATABASE_URL" | sed 's/localhost/postgres/g' | sed 's/127.0.0.1/postgres/g') + fi + fi + + if ! sqlx database create --database-url "$DATABASE_URL" 2>/dev/null; then + log_warning "Database might already exist, continuing..." + fi + + if ! sqlx migrate info --database-url "$DATABASE_URL" >/dev/null 2>&1; then + log_error "Cannot connect to database. Check DATABASE_URL and network connectivity" + log_error "DATABASE_URL: $DATABASE_URL" + exit 1 + fi + + log_success "Database connection successful" +} + +run_migrations() { + log_info "Running database migrations..." + + if [ "$DRY_RUN" = "true" ]; then + log_warning "DRY RUN MODE - No changes will be made" + sqlx migrate info --database-url "$DATABASE_URL" + return + fi + + # Show current status + sqlx migrate info --database-url "$DATABASE_URL" + + # Run migrations + if sqlx migrate run --database-url "$DATABASE_URL"; then + log_success "Migrations completed successfully" + + # Show final status + log_info "Final migration status:" + sqlx migrate info --database-url "$DATABASE_URL" + else + log_error "Migration failed" + exit 1 + fi +} + +show_status() { + log_info "Migration status:" + sqlx migrate info --database-url "$DATABASE_URL" +} + +create_migration() { + if [ $# -eq 0 ]; then + log_error "Migration name is required" + echo "Usage: $0 create " + exit 1 + fi + + local migration_name="$1" + log_info "Creating migration: $migration_name" + + if [ "$DRY_RUN" = "true" ]; then + log_warning "DRY RUN MODE - Migration file would be created" + echo "sqlx migrate add $migration_name" + return + fi + + if sqlx migrate add --database-url "$DATABASE_URL" "$migration_name"; then + log_success "Migration file created successfully" + log_info "Edit the generated file in $MIGRATIONS_DIR/" + else + log_error "Failed to create migration file" + exit 1 + fi +} + +revert_migration() { + log_warning "Reverting last migration..." + + if [ "$DRY_RUN" = "true" ]; then + log_warning "DRY RUN MODE - No changes will be made" + return + fi + + if sqlx migrate revert --database-url "$DATABASE_URL"; then + log_success "Migration reverted successfully" + else + log_error "Failed to revert migration" + exit 1 + fi +} + +backup_database() { + # Create backup directory if it doesn't exist + mkdir -p "$BACKUP_DIR" + + local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).sql" + log_info "Creating database backup: $backup_file" + + if [ "$DRY_RUN" = "true" ]; then + log_warning "DRY RUN MODE - Backup would be created" + return + fi + + # Extract connection details from DATABASE_URL + local db_host=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\).*/\1/p') + local db_port=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') + local db_name=$(echo "$DATABASE_URL" | sed -n 's/.*\/\([^?]*\).*/\1/p') + local db_user=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') + local db_pass=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') + + # Set PGPASSWORD for pg_dump + export PGPASSWORD="$db_pass" + + if pg_dump -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" > "$backup_file"; then + log_success "Backup created: $backup_file" + else + log_error "Backup failed" + exit 1 + fi +} + +restore_database() { + if [ $# -eq 0 ]; then + log_error "Backup file is required" + echo "Usage: $0 restore " + exit 1 + fi + + local backup_file="$1" + + # If backup file doesn't have a path, assume it's in BACKUP_DIR + if [[ "$backup_file" != */* ]]; then + backup_file="$BACKUP_DIR/$backup_file" + fi + + if [ ! -f "$backup_file" ]; then + log_error "Backup file not found: $backup_file" + exit 1 + fi + + log_warning "Restoring database from: $backup_file" + log_warning "This will overwrite the current database!" + + if [ "$DRY_RUN" = "true" ]; then + log_warning "DRY RUN MODE - No changes will be made" + return + fi + + read -p "Are you sure you want to continue? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Restore cancelled" + exit 0 + fi + + # Extract connection details from DATABASE_URL + local db_host=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\).*/\1/p') + local db_port=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p') + local db_name=$(echo "$DATABASE_URL" | sed -n 's/.*\/\([^?]*\).*/\1/p') + local db_user=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p') + local db_pass=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p') + + # Set PGPASSWORD for psql + export PGPASSWORD="$db_pass" + + if psql -h "$db_host" -p "$db_port" -U "$db_user" -d "$db_name" < "$backup_file"; then + log_success "Database restored successfully" + else + log_error "Restore failed" + exit 1 + fi +} + +# Main script logic +main() { + local command="${1:-}" + + case "$command" in + run) + check_dependencies + check_connection + run_migrations + ;; + status) + check_dependencies + check_connection + show_status + ;; + create) + check_dependencies + create_migration "${@:2}" + ;; + revert) + check_dependencies + check_connection + revert_migration + ;; + backup) + check_dependencies + check_connection + backup_database + ;; + restore) + check_dependencies + check_connection + restore_database "${@:2}" + ;; + help|--help|-h) + show_help + ;; + "") + log_error "No command provided" + show_help + exit 1 + ;; + *) + log_error "Unknown command: $command" + show_help + exit 1 + ;; + esac +} + +# Run main function with all arguments +main "$@" \ No newline at end of file