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

View File

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

View File

@@ -11,19 +11,23 @@ return new class extends Migration
*/
public function up(): void
{
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');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
if (! Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) {
$table->string('hetzner_server_status')->nullable()->after('hetzner_server_id');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('servers', 'hetzner_server_status')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('hetzner_server_status');
});
}
}
};

View File

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

View File

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

View File

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

View File

@@ -11,21 +11,43 @@ return new class extends Migration
*/
public function up(): void
{
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');
});
}
}
/**
* Reverse the migrations.
*/
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,20 +11,37 @@ return new class extends Migration
*/
public function up(): void
{
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');
});
}
}
/**
* Reverse the migrations.
*/
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,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
if (! Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) {
$table->string('detected_traefik_version')->nullable();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('servers', 'detected_traefik_version')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('detected_traefik_version');
});
}
}
};

View File

@@ -11,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
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);
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
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();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
if (! Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) {
$table->json('traefik_outdated_info')->nullable();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('servers', 'traefik_outdated_info')) {
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('traefik_outdated_info');
});
}
}
};

View File

@@ -11,20 +11,34 @@ return new class extends Migration
*/
public function up(): void
{
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');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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,18 +11,22 @@ return new class extends Migration
*/
public function up(): void
{
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');
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
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
{
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
{
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
{
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
{
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,6 +13,7 @@ return new class extends Migration
*/
public function up(): void
{
if (! Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->string('uuid')->nullable()->unique()->after('id');
});
@@ -33,14 +34,17 @@ return new class extends Migration
$table->string('uuid')->nullable(false)->change();
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (Schema::hasColumn('cloud_provider_tokens', 'uuid')) {
Schema::table('cloud_provider_tokens', function (Blueprint $table) {
$table->dropColumn('uuid');
});
}
}
};