Skip to content

fix(migration): revert container migrations when a later upgrade step fails - #1936

Open
mrrobot47 wants to merge 4 commits into
EasyEngine:developfrom
mrrobot47:fix/revert-container-migrations-on-failure
Open

mrrobot47 wants to merge 4 commits into
EasyEngine:developfrom
mrrobot47:fix/revert-container-migrations-on-failure

Conversation

@mrrobot47

@mrrobot47 mrrobot47 commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Problem

Runner::migrate() runs the container migrations (ee-custom-container-migrations) before the image migration, but that step has no undo. When a later step fails, for example Unable to pull easyengine/nginx-proxy, the rollback reverts nothing. The container migrations of that run stay applied and recorded in migrations, options.version stays old, and the old images keep running with the new files. The next attempt never runs those migrations again.

In v4.13.0 this is what made the auth-command migration unsafe. Its _wildcard.X htpasswd/ACL files stayed on the old nginx-proxy template, which applies _wildcard.X to every subdomain of X that has no file of its own. A sibling site such as shop.<multisite> then asked for the multisite's password until an upgrade finally succeeded.

Packages also had no point in the upgrade at which they could apply changes that need the new global containers, e.g. the new nginx-proxy template.

Fix

  • Undo of the container migrations, until the image migration has completed. CustomContainerMigrations records the container migrations completed by the current invocation. revert_executed_migrations() is registered as the undo of ee-custom-container-migrations. When the image migration (or an earlier step) fails, it calls down() on those migrations, newest first, 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 as a warning and doesn't stop the others. Its row is still deleted, since migrations are idempotent and must re-run on the retry. The log then says Removed the migrations row of X; Reverted: X only appears when down() succeeded.
    • The undo runs at most once. A failure inside the migration stack keeps the existing in-stack revert (unchanged).
    • Fresh install (no options.version): nothing is reverted, as before.
  • No revert once the image migration has run. A new step ee-keep-container-migrations, right after ee-docker-image-migrations, clears that list. The image migration has no undo: the new images, the recreated containers and their DB tags stay. Reverting the container migrations after it would put their old state onto the upgraded containers (for auth-command: old-template auth files on the new proxy, so subsites and aliases lose their protection). A failure in a later step (ee-update-docker-compose, cron config, logrotate, or an after_docker_image_migration listener) now keeps them and their rows. The retry doesn't re-run them.
  • New hook after_docker_image_migration, fired by the step ee-after-docker-image-migrations right after ee-keep-container-migrations. The updated global containers run from that point. It also fires when no image changed (e.g. nightly re-runs). fix(migration): don't apply wildcard auth files on the old nginx-proxy template auth-command#58 uses it to apply the _wildcard.* auth files it staged while the old proxy template ran.

Pipeline: ee-db-migrations → ee-custom-container-migrations (undo: revert) → ee-docker-image-migrations → ee-keep-container-migrations → ee-after-docker-image-migrations → ee-update-docker-compose → ee-update-cron-config → ee-setup-logrotate.

Every container migration new in v4.13.0 was checked (git diff v4.12.0..develop -- migrations in core and each bumped package's tag range). Only auth-command's 20260924120000_auth-command_regenerate_site_auth_files.php is new, and it gets a real down() in EasyEngine/auth-command#58.

Not covered: steps that call EE::error() (process exit) instead of throwing still skip every undo (pre-existing). A failed pull throws, so it is covered.

Tests

  • Isolated harness (throwaway sqlite, fake packages, the real RevertableStepProcessor/CustomContainerMigrations/models; PHP 7.4 and 8.5, 34/34). It covers:

    • the image migration fails: down() runs newest first for this run's migrations only, their rows are deleted, and a throwing down() is logged as "row removed" while the next one is still reverted; a second undo is a no-op; the retry re-runs them;
    • success, nightly re-run and fresh install revert nothing;
    • the hook fires right after the image migration (also when no image changed) and not when the image migration fails;
    • a failure after the image migration, or a throwing hook listener, reverts nothing and keeps the row, and the retry doesn't re-run it;
    • the registration order in Runner::migrate().
  • Live upgrade from 4.12.0, with a phar of develop + this PR + fix(migration): don't apply wildcard auth files on the old nginx-proxy template auth-command#58 + fix(migration): reload site nginx after its containers are recreated #1937 built the way CI builds the nightly. The host had a WP subdomain multisite with site auth and a whitelist, a sibling shop.<multisite> without auth, a site with alias and *.X aliases plus auth and a whitelist, WP --cache, PHP with a DB, php 8.1, php 7.4 and html sites. A probe curled the public IP every 0.5 s without credentials, and container events were forced every ~5 s during every run (a docker run --rm and a docker restart of an unrelated container).

    Run Result
    Image pull fails (Docker Hub blocked) rc=1, shop. never 401 (0/88), auth migration reverted: htpasswd/ and ACL files byte-identical to before (content, mode, owner, mtime), row removed, version stays 4.12.0
    Retry, successful upgrade rc=0, shop. never 401 (0/113), subsites and aliases protected after promotion, content identical, 0 No route to host
    ee-update-docker-compose fails after the image migration no revert: auth files stay promoted, subsites and aliases stay 401, row kept; the retry succeeds without re-running the migration
    ee killed mid-pull, then retry shop. never 401 (0/72 while held with events); the retry completes the upgrade
    Nightly re-runs DB, proxy files, compose files, containers and .backup unchanged

    The TESTING-NOTES §1 auth matrix passed after the upgrade, and so did create/delete of every site type.

  • Behat (@migration, on the earlier head): a probe container migration and a blocked registry. The probe's down() runs, its row is removed and the version stays old; after unblocking, the probe runs again and the version is bumped. CI: https://github.com/mrrobot47/easyengine/actions/runs/36123058489.

  • php -l on PHP 7.4 and 8.5; no new PHPCS violations.

Release

This should land in v4.13.0, together with auth-command v1.3.1 (EasyEngine/auth-command#58, a patch tag). Bump core's auth-command pin to v1.3.1 before core v4.13.0 is tagged. It combines cleanly with #1937.

… 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The rollback behavior is sound; only a non-blocking misleading debug message remains.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Adds rollback support for container migrations when later upgrade steps fail.

Changes:

  • Tracks migrations completed during the current run.
  • Reverts them in reverse order and removes their database records.
File Description
php/​EE/​Runner.php Registers the container migration rollback callback.
php/​EE/​Migration/​CustomContainerMigrations.php Tracks and reverts newly executed migrations.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

foreach ( Migration::where( 'migration', $name ) as $row ) {
$row->delete();
}
EE::debug( "Reverted: $name" );
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.
…ration 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.
…s 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants