diff --git a/deployment/scripts/deploy.sh b/deployment/scripts/deploy.sh index e9b1798f..8db230b5 100755 --- a/deployment/scripts/deploy.sh +++ b/deployment/scripts/deploy.sh @@ -121,6 +121,13 @@ docker compose $COMPOSE_FILES pull || print_warning "Failed to pull some images, print_info "Stopping existing containers..." docker compose $COMPOSE_FILES down --remove-orphans || print_warning "No existing containers to stop" +# Staging: Remove named volume to ensure fresh code from image +# This prevents stale code persisting between deployments +if [ "$ENVIRONMENT" = "staging" ]; then + print_info "Removing staging code volume to ensure fresh deployment..." + docker volume rm staging-code 2>/dev/null || print_info "No stale staging volume to remove" +fi + # Remove any orphaned containers with conflicting names for container in nginx php redis scheduler queue-worker; do if docker ps -a --format '{{.Names}}' | grep -q "^${container}$"; then diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index cc903d5b..f790a1ff 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -147,9 +147,10 @@ services: - app-backend - app-internal healthcheck: - test: ["CMD", "php", "-v"] - interval: 30s - timeout: 10s + # Check if PHP-FPM is accepting connections + test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"] + interval: 15s + timeout: 5s retries: 3 start_period: 60s deploy: diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index e466bc03..c09b30b7 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -170,11 +170,25 @@ services: echo "" echo "â„šī¸ GIT_REPOSITORY_URL not set, using code from image" fi - + echo "" echo "📊 Environment variables:" env | grep -E "DB_|APP_" | grep -v "PASSWORD|KEY|SECRET" || true + # Run database migrations + if [ -f /var/www/html/console.php ]; then + echo "" + echo "đŸ—„ī¸ Running database migrations..." + cd /var/www/html + php console.php db:migrate --force || echo "âš ī¸ Migration warning (may be OK if already migrated)" + fi + + # Warm up caches + echo "" + echo "đŸ”Ĩ Warming up caches..." + cd /var/www/html + php console.php cache:warm 2>/dev/null || echo "â„šī¸ Cache warmup skipped (command may not exist)" + echo "" echo "đŸ› ī¸ Adjusting filesystem permissions..." chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache 2>/dev/null || true @@ -191,11 +205,13 @@ services: echo "REDIS_PASSWORD_FILE: ${REDIS_PASSWORD_FILE:-NOT SET}" exec php-fpm healthcheck: - test: ["CMD-SHELL", "php-fpm-healthcheck || true"] - interval: 30s - timeout: 10s + # Use HTTP liveness check via php-fpm (not via nginx) + # This checks if the PHP application is actually responding + test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"] + interval: 15s + timeout: 5s retries: 3 - start_period: 40s + start_period: 60s depends_on: redis: condition: service_started @@ -265,6 +281,26 @@ services: fi done + # Wait for PHP-FPM to be ready before starting nginx + # This prevents 502 Bad Gateway errors during startup + echo "âŗ [staging-nginx] Waiting for PHP-FPM to be ready..." + MAX_WAIT=30 + WAITED=0 + while [ $$WAITED -lt $$MAX_WAIT ]; do + # Check if PHP-FPM is accepting connections on port 9000 + if nc -z php 9000 2>/dev/null; then + echo "✅ [staging-nginx] PHP-FPM is ready on php:9000" + break + fi + echo " [staging-nginx] PHP-FPM not ready yet... ($$WAITED/$$MAX_WAIT)" + sleep 1 + WAITED=$$((WAITED + 1)) + done + + if [ $$WAITED -ge $$MAX_WAIT ]; then + echo "âš ī¸ [staging-nginx] PHP-FPM did not become ready within $$MAX_WAIT seconds, starting anyway..." + fi + # Start nginx only (no PHP-FPM, no Git clone - php container handles that) echo "🚀 [staging-nginx] Starting nginx..." exec nginx -g "daemon off;" @@ -280,11 +316,12 @@ services: # Network - "traefik.docker.network=traefik-public" healthcheck: - test: ["CMD-SHELL", "curl -f http://127.0.0.1/health || exit 1"] - interval: 30s - timeout: 10s + # Use /health/live endpoint for lightweight liveness check + test: ["CMD-SHELL", "curl -sf http://127.0.0.1/health/live || exit 1"] + interval: 15s + timeout: 5s retries: 3 - start_period: 10s + start_period: 30s depends_on: php: condition: service_started diff --git a/docker/nginx/Dockerfile.production b/docker/nginx/Dockerfile.production index 4ae6aa73..1e8c827a 100644 --- a/docker/nginx/Dockerfile.production +++ b/docker/nginx/Dockerfile.production @@ -9,6 +9,7 @@ RUN apk add --no-cache \ certbot-nginx \ su-exec \ netcat-openbsd \ + curl \ openssl \ bash diff --git a/src/Framework/Database/Seed/Migrations/CreateSeedsTable.php b/src/Framework/Database/Seed/Migrations/CreateSeedsTable.php index 01debb0f..d62d0e9f 100644 --- a/src/Framework/Database/Seed/Migrations/CreateSeedsTable.php +++ b/src/Framework/Database/Seed/Migrations/CreateSeedsTable.php @@ -47,4 +47,3 @@ final readonly class CreateSeedsTable implements Migration, SafelyReversible return 'Create seeds table for tracking executed seeders'; } } - diff --git a/src/Framework/Database/Seed/SeedCommand.php b/src/Framework/Database/Seed/SeedCommand.php index 151d9378..bb4c268f 100644 --- a/src/Framework/Database/Seed/SeedCommand.php +++ b/src/Framework/Database/Seed/SeedCommand.php @@ -24,6 +24,9 @@ final readonly class SeedCommand $fresh = $input->hasOption('fresh'); try { + // Ensure seeds table exists (auto-create if missing) + $this->seedRepository->ensureSeedsTable(); + // Clear seeds table if --fresh option is provided if ($fresh) { echo "âš ī¸ Clearing seeds table (--fresh option)...\n"; @@ -35,9 +38,10 @@ final readonly class SeedCommand // Run specific seeder echo "Running seeder: {$className}\n"; $seeder = $this->seedLoader->load($className); - + if ($seeder === null) { echo "❌ Seeder '{$className}' not found or cannot be instantiated.\n"; + return ExitCode::SOFTWARE_ERROR; } @@ -50,6 +54,7 @@ final readonly class SeedCommand if (empty($seeders)) { echo "No seeders found.\n"; + return ExitCode::SUCCESS; } @@ -74,4 +79,3 @@ final readonly class SeedCommand } } } - diff --git a/src/Framework/Database/Seed/SeedLoader.php b/src/Framework/Database/Seed/SeedLoader.php index a91097d7..a8cce96d 100644 --- a/src/Framework/Database/Seed/SeedLoader.php +++ b/src/Framework/Database/Seed/SeedLoader.php @@ -61,4 +61,3 @@ final readonly class SeedLoader return null; } } - diff --git a/src/Framework/Database/Seed/SeedRepository.php b/src/Framework/Database/Seed/SeedRepository.php index c4b7bd62..a390807d 100644 --- a/src/Framework/Database/Seed/SeedRepository.php +++ b/src/Framework/Database/Seed/SeedRepository.php @@ -5,17 +5,68 @@ declare(strict_types=1); namespace App\Framework\Database\Seed; use App\Framework\Database\ConnectionInterface; +use App\Framework\Database\Platform\DatabasePlatform; use App\Framework\Database\ValueObjects\SqlQuery; use App\Framework\DateTime\Clock; final readonly class SeedRepository { + private const string TABLE_NAME = 'seeds'; + public function __construct( private ConnectionInterface $connection, + private DatabasePlatform $platform, private Clock $clock ) { } + /** + * Ensure the seeds table exists, create it if not + * + * This method should be called before any seed operations to prevent + * "table does not exist" errors on fresh databases. + */ + public function ensureSeedsTable(): void + { + if ($this->tableExists()) { + return; + } + + $this->createSeedsTable(); + } + + /** + * Check if the seeds table exists + */ + private function tableExists(): bool + { + try { + $sql = $this->platform->getTableExistsSQL(self::TABLE_NAME); + $result = $this->connection->queryScalar(SqlQuery::create($sql)); + + return (bool) $result; + } catch (\Throwable) { + return false; + } + } + + /** + * Create the seeds tracking table + */ + private function createSeedsTable(): void + { + $sql = <<<'SQL' + CREATE TABLE seeds ( + id VARCHAR(64) PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ) + SQL; + + $this->connection->execute(SqlQuery::create($sql)); + } + /** * Check if a seeder has already been executed */ @@ -60,4 +111,3 @@ final readonly class SeedRepository return hash('sha256', $name); } } - diff --git a/src/Framework/Database/Seed/SeedRunner.php b/src/Framework/Database/Seed/SeedRunner.php index 2b67762f..f169aa2c 100644 --- a/src/Framework/Database/Seed/SeedRunner.php +++ b/src/Framework/Database/Seed/SeedRunner.php @@ -27,6 +27,7 @@ final readonly class SeedRunner // Check if already executed if ($this->seedRepository->hasRun($name)) { $this->log("Skipping seeder '{$name}' - already executed", 'info'); + return; } @@ -38,6 +39,7 @@ final readonly class SeedRunner $this->log("Seeder '{$name}' completed successfully", 'info'); } catch (\Throwable $e) { $this->log("Seeder '{$name}' failed: {$e->getMessage()}", 'error'); + throw $e; } } @@ -71,4 +73,3 @@ final readonly class SeedRunner } } } - diff --git a/src/Framework/Database/Seed/SeedServicesInitializer.php b/src/Framework/Database/Seed/SeedServicesInitializer.php index 29c003f5..5bc13063 100644 --- a/src/Framework/Database/Seed/SeedServicesInitializer.php +++ b/src/Framework/Database/Seed/SeedServicesInitializer.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Framework\Database\Seed; use App\Framework\Database\ConnectionInterface; +use App\Framework\Database\Platform\DatabasePlatform; use App\Framework\DateTime\Clock; use App\Framework\DI\Container; use App\Framework\DI\Initializer; @@ -20,6 +21,7 @@ final readonly class SeedServicesInitializer $container->singleton(SeedRepository::class, function (Container $c) { return new SeedRepository( $c->get(ConnectionInterface::class), + $c->get(DatabasePlatform::class), $c->get(Clock::class) ); }); @@ -27,12 +29,14 @@ final readonly class SeedServicesInitializer // SeedLoader $container->singleton(SeedLoader::class, function (Container $c) { $discoveryRegistry = $c->get(DiscoveryRegistry::class); + return new SeedLoader($discoveryRegistry, $c); }); // SeedRunner $container->singleton(SeedRunner::class, function (Container $c) { $logger = $c->has(Logger::class) ? $c->get(Logger::class) : null; + return new SeedRunner( $c->get(SeedRepository::class), $logger @@ -40,4 +44,3 @@ final readonly class SeedServicesInitializer }); } } - diff --git a/src/Framework/Database/Seed/Seeder.php b/src/Framework/Database/Seed/Seeder.php index 1feb953f..b05193ee 100644 --- a/src/Framework/Database/Seed/Seeder.php +++ b/src/Framework/Database/Seed/Seeder.php @@ -36,4 +36,3 @@ interface Seeder */ public function getDescription(): string; } -