drush migrate:import --group aborts the whole remaining group on any migration's partial failure
Area: migration / DX / tooling
Raised during: Session 2026-07-17 (first live migrate:import on dev-0, against the newly-loaded mandala_d7_images/mandala_d7_shared staging-RDS source — see d11-dev-database-bootstrap-and-migration-source.md)
Jira: (add when available)
Priority: Medium — not a correctness bug, but blocks a full --group run every time the known d7_images_collection_memberships partial failure occurs, which is every run until the user migration is unblocked
Observation
Running drush migrate:import --group=mandala_images on dev-0 stopped after only
three migrations (d7_images_collections, d7_images_subcollections,
d7_images_collection_memberships) even though the group has nine. The other
six — including the three big ones (d7_images_image_agent,
d7_images_image_descriptions, d7_images_shanti_image) — never started.
d7_images_collection_memberships (not d7_images_image_collection_membership
— easy to conflate; see the "Two similarly-named migrations" note below) processed
246 rows, 36 created, 210 failed. This is the documented, expected limitation:
this migration maps D7 Organic Groups user memberships (etid = a D7 user id)
to D11 Group membership relationships, and the real user migration
(feat/user-migration, PR #45, still a held draft) hasn't landed — so 210 of 246
rows reference D7 users that don't exist in D11 yet, and fail their entity lookup.
This matches the same ratio previously seen in DDEV (38/249, per [[project-d7-shared-user-database]]) — not a new defect.
The problem is what happened next: migrate_tools' --group runner treats a
migration ending with any failed rows as throwing an Exception
(MigrateToolsCommands.php line 1215: <migration> Migration - N failed.),
and that exception aborts the entire remaining --group sequence — the six
migrations queued after d7_images_collection_memberships in dependency order
never ran at all, silently (well — loudly, but easy to miss: the shell just
exits non-zero and stops).
Two similarly-named migrations — don't confuse them
| Migration ID | Label | Maps | Expected result |
|---|---|---|---|
d7_images_collection_memberships (plural "memberships") |
OG user memberships → Group membership relationships | D7 user etid → D11 user |
Partial failure expected until the user migration lands |
d7_images_image_collection_membership (singular "membership") |
OG image→group memberships → group_node:shanti_image relationships | D7 image node etid (via d7_images_shanti_image lookup) → group |
Should be clean — not user-dependent |
Verified directly against migrate_plus.migration.*.yml in config/sync, not
assumed — the naming is genuinely easy to mix up mid-conversation.
Workaround used (2026-07-17)
Ran the remaining six migrations individually, in dependency order, as one
single shell script handed to a single docker exec call — not six separate
invocations. Each line runs plain (no &&) so one migration's exit code can't
block the next, and everything appends to one log file inside the container:
docker exec -e MIGRATE_SOURCE_DATABASE=mandala_d7_images mandala-drupal-0 sh -c '
cd /opt/drupal/app/drupal
vendor/bin/drush migrate:import d7_images_external_classification_scheme --verbose >> /tmp/migrate_import2.log 2>&1
vendor/bin/drush migrate:import d7_images_external_classification --verbose >> /tmp/migrate_import2.log 2>&1
vendor/bin/drush migrate:import d7_images_image_agent --verbose >> /tmp/migrate_import2.log 2>&1
vendor/bin/drush migrate:import d7_images_image_descriptions --verbose >> /tmp/migrate_import2.log 2>&1
vendor/bin/drush migrate:import d7_images_shanti_image --verbose >> /tmp/migrate_import2.log 2>&1
vendor/bin/drush migrate:import d7_images_image_collection_membership --verbose >> /tmp/migrate_import2.log 2>&1
echo ALL_DONE >> /tmp/migrate_import2.log
'
Migrate API resolves each migration's own dependencies via its migration map
tables regardless of how the ID was invoked (not only via --group), so
running them individually in the right order works the same as the group run
would have — it just doesn't abort partway.
This matters operationally, not just as a workaround: because it's one
script, not six separate commands, nothing external needs to trigger the
next migration once the current one finishes — no cron, no re-invocation,
no orchestrator watching for completion. It's a single sh process on dev-0
(see the containerd-shim-runc-v2 note below) that simply advances to its own
next line when the current drush call returns, exactly like running any
shell script and walking away. That single process is what needs to still be
alive for the sequence to continue — and per the note below, it doesn't depend
on any SSH/VPN session, so it survives this Claude Code session ending too.
To check on / resume a run like this:
ssh -i ~/.ssh/id_rsa ys2n@mandala-drupal-dev-0.internal.lib.virginia.edu \
'sudo docker exec mandala-drupal-0 sh -c "tail -40 /tmp/migrate_import2.log"'
ALL_DONEat the end of the log = the whole sequence finished (check each migration'sProcessed N items (...)line for failures).- An
Exception/error partway with noALL_DONE= that one script instance stopped (or the box/container restarted). Nothing is lost — Migrate API tracks completed rows per migration in its map tables — just re-run the specific migration ID that didn't finish (with the same-e MIGRATE_SOURCE_DATABASE=mandala_d7_images), then continue down the remaining list by hand. - Still advancing with no
ALL_DONEyet = still running normally, nothing to do.
Recommendation
Pick one (not yet decided):
- Exclude
d7_images_collection_membershipsfrom--groupruns until the user migration lands, running it as a separate, explicitly-acknowledged step — cleanest, but means--groupalone is never sufficient for a full run until then. - Raise
migrate_tools' failure-tolerance for this one migration (if such a per-migration setting exists — not yet investigated) so a known, expected partial failure doesn't abort the group. - Just document the individual-invocation sequence as the standard runbook for now (what this note does) and revisit once user migration removes the partial-failure case entirely.
Whichever is picked, update scripts/migration-cycle.sh (currently DDEV-first,
--group-based) to account for it — it will hit the identical abort the first
time it's run against a source that includes real collection membership data
with no matching user migration.
Related operational notes from the same run
- Confirms
kmassets-sync-hook-fires-during-migration.md's guard fires correctly in a live (non-DDEV) run —[notice] kmassets per-node Solr sync suppressed.../...re-enabled after migration.appeared around every migration in the sequence, exactly asMigrateSyncSubscriber(PR #51) intends. See that file's update. - Throughput on dev-0 (talking directly to
rds-mysql8-staging, no VPN in the data path — the process runs inside the container viadocker exec, parented bycontainerd-shim-runc-v2under init, not by any SSH session): ~1,000–1,100 rows/min forimage_agent(paragraph entity creates). Same order of magnitude as the historical DDEV baseline ("~an hour" for the full ~111k-row run, i.e. ~1,850 rows/min) — the cost is inherent per-row Drupal Entity API overhead (field storage writes, validation, hooks), not network latency, so don't expect a materially faster run from a "closer to the DB" environment. Plan for multiple hours, not "~an hour", for a full fresh Images migration run outside DDEV. - A non-interactive
docker execsurvives the launching SSH session dying, if its output is redirected to a file inside the container (>> file) rather than streamed back through thedocker execAPI pipe to the client. Verified via process ancestry: the actual work is parented bycontainerd-shim-runc-v2(PPID 1), not by thesudo docker execclient process tied to the SSH session. Safe to disconnect/reconnect and justtail/grepthe log file to check on a long-running migration.
Related
- kmassets sync hook fires during migration
- D7 shared user database
- Dev database: bootstrap + migration source
scripts/migration-cycle.sh,drupal/config/sync/migrate_plus.migration.d7_images_collection_memberships.yml,drupal/config/sync/migrate_plus.migration.d7_images_image_collection_membership.yml