From 6ac5feaba1bef7624e068d31ee4032530a920e64 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 09:47:01 +0000 Subject: [PATCH 1/4] fix(migration): revert container migrations when a later upgrade step fails The ee-custom-container-migrations step had no undo, so when a later step (e.g. the image migration after a failed pull) failed, the container migrations of that run stayed applied and recorded while the old images kept running. For v4.13.0 this leaves the auth migration's _wildcard.X files on the old nginx-proxy template, which applies them to sibling sites. The step now reverts the container migrations completed by the same invocation, newest first: it calls their down() and deletes their migrations rows so the next attempt runs them again. Migrations recorded by earlier runs are never touched. A down() that throws is logged and doesn't stop the others, and the undo runs at most once. On a fresh install nothing is reverted, as before. --- .../Migration/CustomContainerMigrations.php | 47 +++++++++++++++++++ php/EE/Runner.php | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/php/EE/Migration/CustomContainerMigrations.php b/php/EE/Migration/CustomContainerMigrations.php index 39f5c86e5..77c7acb9c 100644 --- a/php/EE/Migration/CustomContainerMigrations.php +++ b/php/EE/Migration/CustomContainerMigrations.php @@ -8,6 +8,9 @@ class CustomContainerMigrations { + /** @var array Migrations completed by this invocation, oldest first: name => instance. */ + private static $executed = []; + /** * Executes pending migrations of container. */ @@ -16,6 +19,8 @@ public static function execute_migrations() { Utils\delem_log( 'ee migration start' ); EE::debug( 'Executing custom container migrations' ); + self::$executed = []; + $migrations = self::get_all_migrations(); if ( empty( $migrations ) ) { @@ -35,6 +40,46 @@ public static function execute_migrations() { EE::debug( 'Successfully migrated EasyEngine' ); } + /** + * Reverts the container migrations completed by this invocation, newest first, when a later upgrade step fails. + * + * Their rows are deleted so the next attempt runs them again. Migrations recorded by earlier runs are not touched. + */ + public static function revert_executed_migrations() { + + $executed = array_reverse( self::$executed, true ); + self::$executed = []; + + if ( empty( $executed ) ) { + return; + } + + // On a fresh install they are no-ops by design, and older down() methods assume an upgrade. + if ( ! \EE\Model\Option::get( 'version' ) ) { + EE::debug( 'Fresh install: not reverting container migrations' ); + return; + } + + foreach ( $executed as $name => $migration ) { + EE::debug( "Reverting: $name" ); + try { + $migration->down(); + } catch ( \Throwable $e ) { + EE::warning( "Could not revert container migration $name: " . $e->getMessage() ); + } + + // Deleted even if down() failed: migrations are idempotent, and a retry must run it again. + try { + foreach ( Migration::where( 'migration', $name ) as $row ) { + $row->delete(); + } + EE::debug( "Reverted: $name" ); + } catch ( \Throwable $e ) { + EE::warning( "Could not delete the migrations row of $name: " . $e->getMessage() ); + } + } + } + /** * @return array of available migrations */ @@ -130,6 +175,7 @@ private static function execute_migration_stack( $migrations ) { ] ); $migration->status = 'complete'; + self::$executed[ $migrations[0] ] = $migration; EE::debug( "Migrated: $migrations[0]" ); $remaining_migrations = array_splice( $migrations, 1, count( $migrations ) ); self::execute_migration_stack( $remaining_migrations ); @@ -144,6 +190,7 @@ private static function execute_migration_stack( $migrations ) { $migration->down(); $migrated[0]->delete(); } + unset( self::$executed[ $migrations[0] ] ); EE::debug( "Reverted: $migrations[0]" ); throw $e; diff --git a/php/EE/Runner.php b/php/EE/Runner.php index 8a686e9f3..ab95a5ae7 100644 --- a/php/EE/Runner.php +++ b/php/EE/Runner.php @@ -170,7 +170,7 @@ private function migrate() { $rsp = new \EE\RevertableStepProcessor(); $rsp->add_step( 'ee-db-migrations', 'EE\Migration\Executor::execute_migrations' ); - $rsp->add_step( 'ee-custom-container-migrations', 'EE\Migration\CustomContainerMigrations::execute_migrations' ); + $rsp->add_step( 'ee-custom-container-migrations', 'EE\Migration\CustomContainerMigrations::execute_migrations', 'EE\Migration\CustomContainerMigrations::revert_executed_migrations' ); $rsp->add_step( 'ee-docker-image-migrations', 'EE\Migration\Containers::start_container_migration' ); $rsp->add_step( 'ee-update-docker-compose', 'EE\Migration\Containers::update_docker_compose' ); $rsp->add_step( 'ee-update-cron-config', 'EE\Cron\Utils\update_cron_config' ); From 86bf3fb928c9ff782403dc0707f6e21375d74738 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 11:33:56 +0000 Subject: [PATCH 2/4] fix(migration): log a failed container migration revert as a removed row When a container migration's down() threw during the undo, the log still said "Reverted: X" after the warning. It now says "Reverted" only when down() succeeded, and "Removed the migrations row of X" otherwise. --- php/EE/Migration/CustomContainerMigrations.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/php/EE/Migration/CustomContainerMigrations.php b/php/EE/Migration/CustomContainerMigrations.php index 77c7acb9c..e29c764d6 100644 --- a/php/EE/Migration/CustomContainerMigrations.php +++ b/php/EE/Migration/CustomContainerMigrations.php @@ -62,9 +62,11 @@ public static function revert_executed_migrations() { foreach ( $executed as $name => $migration ) { EE::debug( "Reverting: $name" ); + $reverted = true; try { $migration->down(); } catch ( \Throwable $e ) { + $reverted = false; EE::warning( "Could not revert container migration $name: " . $e->getMessage() ); } @@ -73,7 +75,7 @@ public static function revert_executed_migrations() { foreach ( Migration::where( 'migration', $name ) as $row ) { $row->delete(); } - EE::debug( "Reverted: $name" ); + EE::debug( $reverted ? "Reverted: $name" : "Removed the migrations row of $name" ); } catch ( \Throwable $e ) { EE::warning( "Could not delete the migrations row of $name: " . $e->getMessage() ); } From 0d7015c7bfe3517a13f01b88aa9d7d950047b58f Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 11:33:57 +0000 Subject: [PATCH 3/4] feat(migration): fire after_docker_image_migration once the image migration succeeds Adds an ee-after-docker-image-migrations step right after ee-docker-image-migrations that runs EE::do_hook( 'after_docker_image_migration' ). The updated global containers, e.g. the new nginx-proxy, run from that point, so packages can apply changes that must wait for them. auth-command uses it to apply the _wildcard.* auth files it staged while the old nginx-proxy template ran. The hook also fires when no image changed, e.g. on nightly re-runs. The step has no undo: if a later step fails, the container migrations are reverted as before, which undoes what their hooks applied. --- php/EE/Runner.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/php/EE/Runner.php b/php/EE/Runner.php index ab95a5ae7..3f1a4e673 100644 --- a/php/EE/Runner.php +++ b/php/EE/Runner.php @@ -172,6 +172,8 @@ private function migrate() { $rsp->add_step( 'ee-db-migrations', 'EE\Migration\Executor::execute_migrations' ); $rsp->add_step( 'ee-custom-container-migrations', 'EE\Migration\CustomContainerMigrations::execute_migrations', 'EE\Migration\CustomContainerMigrations::revert_executed_migrations' ); $rsp->add_step( 'ee-docker-image-migrations', 'EE\Migration\Containers::start_container_migration' ); + // The updated global containers (e.g. nginx-proxy) run from here on. No undo: a later failure reverts the container migrations. + $rsp->add_step( 'ee-after-docker-image-migrations', 'EE::do_hook', null, [ 'after_docker_image_migration' ] ); $rsp->add_step( 'ee-update-docker-compose', 'EE\Migration\Containers::update_docker_compose' ); $rsp->add_step( 'ee-update-cron-config', 'EE\Cron\Utils\update_cron_config' ); $rsp->add_step( 'ee-setup-logrotate', 'EE\Logrotate\Utils::setup_logrotate' ); From 07054b2c7d705e3cd74b6b46e7c646658ca83143 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Fri, 25 Sep 2026 12:25:01 +0000 Subject: [PATCH 4/4] fix(migration): keep container migrations once the image migration has run The container migrations' undo also ran when a step after ee-docker-image-migrations failed, e.g. ee-update-docker-compose. That step has no undo, so the new images, the recreated containers and their DB tags stayed, and reverting the container migrations put their old state onto the upgraded containers. For the auth migration, this restored the old-template auth files on the new nginx-proxy, so subsites and aliases were unprotected until a successful re-run. A new ee-keep-container-migrations step right after the image migration clears the list of container migrations to revert. A later failure, including one in an after_docker_image_migration listener, now keeps them and their migrations rows. A failure up to and including the image migration still reverts them as before. --- php/EE/Migration/CustomContainerMigrations.php | 9 +++++++++ php/EE/Runner.php | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/php/EE/Migration/CustomContainerMigrations.php b/php/EE/Migration/CustomContainerMigrations.php index e29c764d6..1fcb1f8bd 100644 --- a/php/EE/Migration/CustomContainerMigrations.php +++ b/php/EE/Migration/CustomContainerMigrations.php @@ -82,6 +82,15 @@ public static function revert_executed_migrations() { } } + /** + * Keeps the container migrations completed by this invocation: a later failure no longer reverts them. + */ + public static function keep_executed_migrations() { + + self::$executed = []; + EE::debug( 'Keeping the container migrations of this run' ); + } + /** * @return array of available migrations */ diff --git a/php/EE/Runner.php b/php/EE/Runner.php index 3f1a4e673..f31ca9a89 100644 --- a/php/EE/Runner.php +++ b/php/EE/Runner.php @@ -172,7 +172,8 @@ private function migrate() { $rsp->add_step( 'ee-db-migrations', 'EE\Migration\Executor::execute_migrations' ); $rsp->add_step( 'ee-custom-container-migrations', 'EE\Migration\CustomContainerMigrations::execute_migrations', 'EE\Migration\CustomContainerMigrations::revert_executed_migrations' ); $rsp->add_step( 'ee-docker-image-migrations', 'EE\Migration\Containers::start_container_migration' ); - // The updated global containers (e.g. nginx-proxy) run from here on. No undo: a later failure reverts the container migrations. + // The new images run from here on and have no undo, so a later failure must not revert the container migrations onto them. + $rsp->add_step( 'ee-keep-container-migrations', 'EE\Migration\CustomContainerMigrations::keep_executed_migrations' ); $rsp->add_step( 'ee-after-docker-image-migrations', 'EE::do_hook', null, [ 'after_docker_image_migration' ] ); $rsp->add_step( 'ee-update-docker-compose', 'EE\Migration\Containers::update_docker_compose' ); $rsp->add_step( 'ee-update-cron-config', 'EE\Cron\Utils\update_cron_config' );