Ir al contenido principal
Laravel, shipping fast.

La restricción que hace las migraciones distintas de cualquier otro cambio de código: durante al menos unos segundos, y a veces durante toda la ventana de despliegue, el esquema que estás cambiando lo sigue leyendo la versión que aún sirve peticiones.

Eso descarta toda una categoría de migraciones «obviamente inofensivas». Renombrar una columna no lo es: la versión antigua no sabe que el nombre nuevo existe. Eliminar una columna tampoco: la versión antigua podría seguir escribiendo en ella. Las migraciones de la flota se apoyan en una guarda simple: comprueba antes de cambiar, para que la migración sea segura tanto si ya se aplicó parcialmente como si no.

// database/migrations/2026_06_04_120000_create_smoke_runs_table.php
return new class extends Migration
{
    public function up(): void
    {
        if (Schema::hasTable('smoke_runs')) {
            return;
        }

        Schema::create('smoke_runs', function (Blueprint $table): void {
            $table->id();
            $table->timestamp('started_at');
            $table->boolean('ok');
            $table->string('stage', 32);
            // ...remaining columns
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('smoke_runs');
    }
};

La comprobación inicial significa que esta migración puede correr dos veces, en dos servidores distintos, en el orden en que la tubería llegue a ellos, y la segunda ejecución es una operación nula en lugar de un error fatal. La misma disciplina aparece unas migraciones después, añadiendo columnas en lugar de una tabla:

// database/migrations/2026_06_04_123134_add_activation_metrics_to_smoke_runs_table.php
public function up(): void
{
    Schema::table('smoke_runs', function (Blueprint $table): void {
        if (! Schema::hasColumn('smoke_runs', 'license_at')) {
            $table->timestamp('license_at')->nullable()->after('teardown_ms');
            $table->unsignedInteger('license_ms')->nullable()->after('license_at');
            // ...remaining columns
        }
    });
}

Cada columna nueva es opcional y aditiva. La versión antigua, aún corriendo mientras esta migración se ejecuta, no tiene ni idea de que license_at existe y no lo necesita. Sigue leyendo y escribiendo las columnas que ya conoce. La versión nueva, cuando el tráfico cambie, encuentra las columnas ya ahí. Nadie tiene que coordinar el milisegundo exacto en que esquema y código cambian juntos, porque el cambio de esquema por sí solo no rompe nada que ya funcionara.

En producción, las migraciones corren con php artisan migrate --force, la bandera que se salta la pregunta interactiva, porque una tubería de despliegue no puede responder a una pregunta. No lo confundas con --graceful, que devuelve un código de salida exitoso aunque ocurra un error. Ese es el comportamiento correcto para arrancar un proyecto que nunca ha visto un esquema. Es el comportamiento equivocado para un despliegue de producción. Una migración que falla en producción debería detener el despliegue en voz alta, no salir con 0 y dejar que código roto empiece a servir tráfico sobre un esquema que nunca llegó.