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,6 +11,7 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasTable('cloud_provider_tokens')) {
Schema::create('cloud_provider_tokens', function (Blueprint $table) { Schema::create('cloud_provider_tokens', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('team_id')->constrained()->onDelete('cascade'); $table->foreignId('team_id')->constrained()->onDelete('cascade');
@ -22,6 +23,7 @@ public function up(): void
$table->index(['team_id', 'provider']); $table->index(['team_id', 'provider']);
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'hetzner_server_id')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->bigInteger('hetzner_server_id')->nullable()->after('id'); $table->bigInteger('hetzner_server_id')->nullable()->after('id');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'hetzner_server_id')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_id'); $table->dropColumn('hetzner_server_id');
}); });
} }
}
}; };

View file

@ -11,19 +11,23 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'cloud_provider_token_id')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null'); $table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'cloud_provider_token_id')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropForeign(['cloud_provider_token_id']); $table->dropForeign(['cloud_provider_token_id']);
$table->dropColumn('cloud_provider_token_id'); $table->dropColumn('cloud_provider_token_id');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id'); $table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_status'); $table->dropColumn('hetzner_server_status');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'is_validating')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->boolean('is_validating')->default(false)->after('hetzner_server_status'); $table->boolean('is_validating')->default(false)->after('hetzner_server_status');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'is_validating')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('is_validating'); $table->dropColumn('is_validating');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('instance_settings', 'dev_helper_version')) {
Schema::table('instance_settings', function (Blueprint $table) { Schema::table('instance_settings', function (Blueprint $table) {
$table->string('dev_helper_version')->nullable(); $table->string('dev_helper_version')->nullable();
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('instance_settings', 'dev_helper_version')) {
Schema::table('instance_settings', function (Blueprint $table) { Schema::table('instance_settings', function (Blueprint $table) {
$table->dropColumn('dev_helper_version'); $table->dropColumn('dev_helper_version');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('scheduled_tasks', 'timeout')) {
Schema::table('scheduled_tasks', function (Blueprint $table) { Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->integer('timeout')->default(300)->after('frequency'); $table->integer('timeout')->default(300)->after('frequency');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('scheduled_tasks', 'timeout')) {
Schema::table('scheduled_tasks', function (Blueprint $table) { Schema::table('scheduled_tasks', function (Blueprint $table) {
$table->dropColumn('timeout'); $table->dropColumn('timeout');
}); });
} }
}
}; };

View file

@ -11,21 +11,43 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('scheduled_task_executions', 'started_at')) {
Schema::table('scheduled_task_executions', function (Blueprint $table) { Schema::table('scheduled_task_executions', function (Blueprint $table) {
$table->timestamp('started_at')->nullable()->after('scheduled_task_id'); $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'); $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'); $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'); $table->text('error_details')->nullable()->after('message');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
Schema::table('scheduled_task_executions', function (Blueprint $table) { $columns = ['started_at', 'retry_count', 'duration', 'error_details'];
$table->dropColumn(['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,20 +11,37 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('applications', 'restart_count')) {
Schema::table('applications', function (Blueprint $table) { Schema::table('applications', function (Blueprint $table) {
$table->integer('restart_count')->default(0)->after('status'); $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'); $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'); $table->string('last_restart_type', 10)->nullable()->after('last_restart_at');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
Schema::table('applications', function (Blueprint $table) { $columns = ['restart_count', 'last_restart_at', 'last_restart_type'];
$table->dropColumn(['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,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->string('detected_traefik_version')->nullable(); $table->string('detected_traefik_version')->nullable();
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('detected_traefik_version'); $table->dropColumn('detected_traefik_version');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
Schema::table('email_notification_settings', function (Blueprint $table) { Schema::table('email_notification_settings', function (Blueprint $table) {
$table->boolean('traefik_outdated_email_notifications')->default(true); $table->boolean('traefik_outdated_email_notifications')->default(true);
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('email_notification_settings', 'traefik_outdated_email_notifications')) {
Schema::table('email_notification_settings', function (Blueprint $table) { Schema::table('email_notification_settings', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_email_notifications'); $table->dropColumn('traefik_outdated_email_notifications');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
Schema::table('telegram_notification_settings', function (Blueprint $table) { Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->text('telegram_notifications_traefik_outdated_thread_id')->nullable(); $table->text('telegram_notifications_traefik_outdated_thread_id')->nullable();
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('telegram_notification_settings', 'telegram_notifications_traefik_outdated_thread_id')) {
Schema::table('telegram_notification_settings', function (Blueprint $table) { Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->dropColumn('telegram_notifications_traefik_outdated_thread_id'); $table->dropColumn('telegram_notifications_traefik_outdated_thread_id');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->json('traefik_outdated_info')->nullable(); $table->json('traefik_outdated_info')->nullable();
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) { Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_info'); $table->dropColumn('traefik_outdated_info');
}); });
} }
}
}; };

View file

@ -11,20 +11,34 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
Schema::table('application_settings', function (Blueprint $table) { Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('inject_build_args_to_dockerfile')->default(true)->after('use_build_secrets'); $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'); $table->boolean('include_source_commit_in_build')->default(false)->after('inject_build_args_to_dockerfile');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('application_settings', 'inject_build_args_to_dockerfile')) {
Schema::table('application_settings', function (Blueprint $table) { Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('inject_build_args_to_dockerfile'); $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'); $table->dropColumn('include_source_commit_in_build');
}); });
} }
}
}; };

View file

@ -11,18 +11,22 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds'); $table->integer('deployment_queue_limit')->default(25)->after('concurrent_builds');
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('server_settings', 'deployment_queue_limit')) {
Schema::table('server_settings', function (Blueprint $table) { Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('deployment_queue_limit'); $table->dropColumn('deployment_queue_limit');
}); });
} }
}
}; };

View file

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

View file

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

View file

@ -13,6 +13,7 @@
*/ */
public function up(): void public function up(): void
{ {
if (! Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) { Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable()->unique()->after('id'); $table->string('uuid')->nullable()->unique()->after('id');
}); });
@ -33,14 +34,17 @@ public function up(): void
$table->string('uuid')->nullable(false)->change(); $table->string('uuid')->nullable(false)->change();
}); });
} }
}
/** /**
* Reverse the migrations. * Reverse the migrations.
*/ */
public function down(): void public function down(): void
{ {
if (Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) { Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->dropColumn('uuid'); $table->dropColumn('uuid');
}); });
} }
}
}; };