fix(deploy): improve deployment robustness and reliability
All checks were successful
Test Runner / test-basic (push) Successful in 8s
Test Runner / test-php (push) Successful in 7s
Deploy Application / deploy (push) Successful in 1m35s

- Add docker volume prune to deploy.sh to prevent stale code issues
- Add automatic migrations and cache warmup to staging entrypoint
- Fix nginx race condition by waiting for PHP-FPM before starting
- Improve PHP healthcheck to use php-fpm-healthcheck
- Add curl to production nginx Dockerfile for healthchecks
- Add ensureSeedsTable() to SeedRepository for automatic table creation
- Update SeedCommand to ensure seeds table exists before operations

This prevents 502 Bad Gateway errors during deployment and ensures
fresh code is deployed without volume cache issues.
This commit is contained in:
2025-11-25 17:44:44 +01:00
parent 7785e65d08
commit 85e2360a90
11 changed files with 121 additions and 20 deletions

View File

@@ -47,4 +47,3 @@ final readonly class CreateSeedsTable implements Migration, SafelyReversible
return 'Create seeds table for tracking executed seeders';
}
}

View File

@@ -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
}
}
}

View File

@@ -61,4 +61,3 @@ final readonly class SeedLoader
return null;
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}
}

View File

@@ -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
});
}
}

View File

@@ -36,4 +36,3 @@ interface Seeder
*/
public function getDescription(): string;
}