fix: add idempotency guards to 18 migrations to prevent upgrade failures

When any migration fails due to table/column already existing, PostgreSQL rolls back
the entire batch and blocks all subsequent migrations. Add Schema::hasTable() and
Schema::hasColumn() guards to all problem migrations for safe re-execution.

Fixes #7606 #7625

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai 2025-12-15 11:23:04 +01:00
parent 6fe4ebeb7e
commit 07cd389eb9
18 changed files with 244 additions and 133 deletions

View File

@ -11,16 +11,18 @@ return new class extends Migration
*/
public function up(): void
{
Schema::create('cloud_provider_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->onDelete('cascade');
$table->string('provider');
$table->text('token');
$table->string('name')->nullable();
$table->timestamps();
if (! Schema::hasTable('cloud_provider_tokens')) {
Schema::create('cloud_provider_tokens', function (Blueprint $table) {
$table->id();
$table->foreignId('team_id')->constrained()->onDelete('cascade');
$table->string('provider');
$table->text('token');
$table->string('name')->nullable();
$table->timestamps();
$table->index(['team_id', 'provider']);
});
$table->index(['team_id', 'provider']);
});
}
}
/**

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->bigInteger('hetzner_server_id')->nullable()->after('id');
});
if (! Schema::hasColumn('servers', 'hetzner_server_id')) {
Schema::table('servers', function (Blueprint $table) {
$table->bigInteger('hetzner_server_id')->nullable()->after('id');
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_id');
});
if (Schema::hasColumn('servers', 'hetzner_server_id')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_id');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');
});
if (! Schema::hasColumn('servers', 'cloud_provider_token_id')) {
Schema::table('servers', function (Blueprint $table) {
$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');
});
}
}
/**
@ -21,9 +23,11 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign(['cloud_provider_token_id']);
$table->dropColumn('cloud_provider_token_id');
});
if (Schema::hasColumn('servers', 'cloud_provider_token_id')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropForeign(['cloud_provider_token_id']);
$table->dropColumn('cloud_provider_token_id');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
});
if (! Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) {
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_status');
});
if (Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_status');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->boolean('is_validating')->default(false)->after('hetzner_server_status');
});
if (! Schema::hasColumn('servers', 'is_validating')) {
Schema::table('servers', function (Blueprint $table) {
$table->boolean('is_validating')->default(false)->after('hetzner_server_status');
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('is_validating');
});
if (Schema::hasColumn('servers', 'is_validating')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('is_validating');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->string('dev_helper_version')->nullable();
});
if (! Schema::hasColumn('instance_settings', 'dev_helper_version')) {
Schema::table('instance_settings', function (Blueprint $table) {
$table->string('dev_helper_version')->nullable();
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('dev_helper_version');
});
if (Schema::hasColumn('instance_settings', 'dev_helper_version')) {
Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('dev_helper_version');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->integer('timeout')->default(300)->after('frequency');
});
if (! Schema::hasColumn('scheduled_tasks', 'timeout')) {
Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->integer('timeout')->default(300)->after('frequency');
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->dropColumn('timeout');
});
if (Schema::hasColumn('scheduled_tasks', 'timeout')) {
Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->dropColumn('timeout');
});
}
}
};

View File

@ -11,12 +11,29 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->timestamp('started_at')->nullable()->after('scheduled_task_id');
$table->integer('retry_count')->default(0)->after('status');
$table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds');
$table->text('error_details')->nullable()->after('message');
});
if (! Schema::hasColumn('scheduled_task_executions', 'started_at')) {
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->timestamp('started_at')->nullable()->after('scheduled_task_id');
});
}
if (! Schema::hasColumn('scheduled_task_executions', 'retry_count')) {
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->integer('retry_count')->default(0)->after('status');
});
}
if (! Schema::hasColumn('scheduled_task_executions', 'duration')) {
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds');
});
}
if (! Schema::hasColumn('scheduled_task_executions', 'error_details')) {
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->text('error_details')->nullable()->after('message');
});
}
}
/**
@ -24,8 +41,13 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->dropColumn(['started_at', 'retry_count', 'duration', 'error_details']);
});
$columns = ['started_at', 'retry_count', 'duration', 'error_details'];
foreach ($columns as $column) {
if (Schema::hasColumn('scheduled_task_executions', $column)) {
Schema::table('scheduled_task_executions', function (Blueprint $table) use ($column) {
$table->dropColumn($column);
});
}
}
}
};

View File

@ -11,11 +11,23 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->integer('restart_count')->default(0)->after('status');
$table->timestamp('last_restart_at')->nullable()->after('restart_count');
$table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
});
if (! Schema::hasColumn('applications', 'restart_count')) {
Schema::table('applications', function (Blueprint $table) {
$table->integer('restart_count')->default(0)->after('status');
});
}
if (! Schema::hasColumn('applications', 'last_restart_at')) {
Schema::table('applications', function (Blueprint $table) {
$table->timestamp('last_restart_at')->nullable()->after('restart_count');
});
}
if (! Schema::hasColumn('applications', 'last_restart_type')) {
Schema::table('applications', function (Blueprint $table) {
$table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
});
}
}
/**
@ -23,8 +35,13 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn(['restart_count', 'last_restart_at', 'last_restart_type']);
});
$columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
foreach ($columns as $column) {
if (Schema::hasColumn('applications', $column)) {
Schema::table('applications', function (Blueprint $table) use ($column) {
$table->dropColumn($column);
});
}
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->string('detected_traefik_version')->nullable();
});
if (! Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) {
$table->string('detected_traefik_version')->nullable();
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('detected_traefik_version');
});
if (Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('detected_traefik_version');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->boolean('traefik_outdated_email_notifications')->default(true);
});
if (! Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->boolean('traefik_outdated_email_notifications')->default(true);
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_email_notifications');
});
if (Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_email_notifications');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->text('telegram_notifications_traefik_outdated_thread_id')->nullable();
});
if (! Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->text('telegram_notifications_traefik_outdated_thread_id')->nullable();
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
});
if (Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->json('traefik_outdated_info')->nullable();
});
if (! Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) {
$table->json('traefik_outdated_info')->nullable();
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_info');
});
if (Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_info');
});
}
}
};

View File

@ -11,10 +11,17 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets');
$table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile');
});
if (! Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets');
});
}
if (! Schema::hasColumn('application_settings', 'include_source_commit_in_build')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile');
});
}
}
/**
@ -22,9 +29,16 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('inject_build_args_to_dockerfile');
$table->dropColumn('include_source_commit_in_build');
});
if (Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('inject_build_args_to_dockerfile');
});
}
if (Schema::hasColumn('application_settings', 'include_source_commit_in_build')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('include_source_commit_in_build');
});
}
}
};

View File

@ -11,9 +11,11 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds');
});
if (! Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
Schema::table('server_settings', function (Blueprint $table) {
$table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds');
});
}
}
/**
@ -21,8 +23,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('deployment_queue_limit');
});
if (Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('deployment_queue_limit');
});
}
}
};

View File

@ -8,15 +8,19 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->integer('docker_images_to_keep')->default(2);
});
if (! Schema::hasColumn('application_settings', 'docker_images_to_keep')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->integer('docker_images_to_keep')->default(2);
});
}
}
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('docker_images_to_keep');
});
if (Schema::hasColumn('application_settings', 'docker_images_to_keep')) {
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('docker_images_to_keep');
});
}
}
};

View File

@ -8,15 +8,19 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('disable_application_image_retention')->default(false);
});
if (! Schema::hasColumn('server_settings', 'disable_application_image_retention')) {
Schema::table('server_settings', function (Blueprint $table) {
$table->boolean('disable_application_image_retention')->default(false);
});
}
}
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('disable_application_image_retention');
});
if (Schema::hasColumn('server_settings', 'disable_application_image_retention')) {
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('disable_application_image_retention');
});
}
}
};

View File

@ -13,25 +13,27 @@ return new class extends Migration
*/
public function up(): void
{
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable()->unique()->after('id');
});
// Generate UUIDs for existing records using chunked processing
DB::table('cloud_provider_tokens')
->whereNull('uuid')
->chunkById(500, function ($tokens) {
foreach ($tokens as $token) {
DB::table('cloud_provider_tokens')
->where('id', $token->id)
->update(['uuid' => (string) new Cuid2]);
}
if (! Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable()->unique()->after('id');
});
// Make uuid non-nullable after filling in values
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable(false)->change();
});
// Generate UUIDs for existing records using chunked processing
DB::table('cloud_provider_tokens')
->whereNull('uuid')
->chunkById(500, function ($tokens) {
foreach ($tokens as $token) {
DB::table('cloud_provider_tokens')
->where('id', $token->id)
->update(['uuid' => (string) new Cuid2]);
}
});
// Make uuid non-nullable after filling in values
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable(false)->change();
});
}
}
/**
@ -39,8 +41,10 @@ return new class extends Migration
*/
public function down(): void
{
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->dropColumn('uuid');
});
if (Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
}
};