Session Log: dev-0 rebuilt from scratch; the migration is running
Date: 2026-08-26
Participants: Yuji Shinozaki, Claude Code
Outcome: The group chose a from-scratch rebuild over migrate:rollback → import.
dev-0 was reset and reinstalled, users migrated, and the ~15 h images migration is running
detached. Six PRs merged (#157–#160 plus #151, #158). Runbook:
dev0-from-scratch-rebuild-runbook.md.
Hand-written. The value here is a sequence of things that were wrong until they were run.
1. The decision: from scratch, not rollback
migrate:rollback does not reset AUTO_INCREMENT, so every cycle assigns higher nids.
Three consequences settled it:
- Deterministic nids. ADR 016's redirects and the kmassets
images-11-{nid}uids are both keyed on D11 nids, so under rollback→import every rebuild silently invalidates them. From an empty database, nid N maps to the same D7 row every time. - It clears the dev-0 duplicate (D7 nid
981206→ nids 76584/76585) by construction. - It rehearses cutover, which will be a fresh build, never a rollback.
A resume is not the cheap recovery it looks like. prepareRow() runs on every source row
regardless of the migrate map, so a restart at hour 14 costs another ~15 h, not one. And
whether a resume cleanly skips was never verified — the deferred note flagged exactly that,
and the duplicate came from precisely such a resumed run. That is an argument for letting this
run finish uninterrupted.
2. Pre-flight: almost nothing needed saving
| Item | Finding |
|---|---|
| Config drift | No differences |
Nodes / groups without field_legacy_nid |
0 / 0 |
Users without a migrate_map_d7_users row |
0 of 1,543 |
files/, keys/ |
bind-mounted, survive |
An earlier query of mine labelled 156 users "hand-created" — wrong; they are migrated
users who had no D7 authmap row.
Exactly two things a fresh install could not rebuild: the solrproxy OAuth2 consumer
(hashed secret) and the uid-600 test IdP authmap link.
hash_salt was a non-issue, settled by reading the code rather than guessing:
settings.php:359 reads it from getenv('DRUPAL_HASH_SALT') and never writes it to the file,
so site:install cannot change it. Better still, the secret is injected into the container by
the deploy — so the consumer was recreated by reading $SOLRPROXY_CLIENT_SECRET from the
container's own environment. No secret was decrypted, copied or typed.
3. The runbook was wrong in three places
Written carefully, reviewed, and still wrong — each found only by running it:
site:installdoes not drop tables. It refused with Drupal's "already installed" page. An explicit drop step was needed. Nothing was damaged.--existing-configis impossible here. Thestandardprofile implementshook_install(), which Drupal forbids installing from config. Fell back to: normal install → pin the committed site UUID →cim.- The drop needs
-i.docker runwithout it never received the heredoc, so the drop ran against no input and reported success while all 286 tables survived. Caught only because the step asserts "tables after drop must be 0".
Three further obstacles in the same stretch:
- D11 core bug:
Call to undefined function node_access_needs_rebuild()duringcimwhen modules install. Worked around by re-running. - Shortcut entities blocked
cim.drush entity:delete shortcutsilently did nothing; the entity API worked. - The 128 MB limit killed
cim— its third victim aftermigrate:importandkmassets:index-all. Strengthens the case for raising the CLI limit in the image.
4. Two real bugs found in our own code
field_legacy_site was missing from both display configs (#159).
Adding a field makes Drupal auto-add it to the hidden list of the node view and form
displays in active config, but [#152] never updated config/sync to match — so every
environment drifts the moment the field installs. deploy_backend.yml fails the build on
drift, so the next deploy would have failed regardless of this rebuild.
db-checkpoint.sh restore would have dropped only some tables
(#160). The drop builds
DROP TABLE IF EXISTS <list> with GROUP_CONCAT, whose group_concat_max_len defaults to
1024 bytes; this schema's ~286-table list is ~8 KB and was silently truncated. The
"restore is proven" test could not have caught it — the scratch database was empty, so the
drop ran with nothing to concatenate. It now raises the limit and asserts the schema is
actually empty before loading.
5. restore was proven first — and DDEV would not have done it
Before any of this, restore was exercised against a scratch schema on the real RDS
(mandala_restore_test, permitted by GRANT ALL ON mandala%.*): 286/286 tables,
111,341/111,341 nodes, 166,395/166,395 paragraphs, 171/171 groups — exact.
DDEV was considered and rejected: it runs as root on a local MySQL, so it would have
proven the drop/load logic while skipping the thing most likely to fail — whether the app
user can drop and reload on RDS. A TARGET_DB override was added so the safety net can be
tested without risking the live database.
6. The long-clone problem, quantified
pipeline/deployspec.yml:42 clones terraform-infrastructure with no --depth and no
--single-branch — a repo of 11,785 commits / 831 MB of .git — on every deploy,
when the deploy only reads the current tree. --depth 1 --single-branch should cut it
dramatically.
Deliberately not fixed yet: pipeline/** is a trigger path, so merging it starts a deploy,
and the container restart would kill the running migration.
7. Current state — migration RUNNING
dev-0 is a clean D11 install: correct site UUID (dfc3f060-…), unchanged hash_salt, ADR 015
roles, 54 shanti_image fields, config:status clean after the #159 deploy.
- Users migrated: 1,542 created / 0 failed; authmap 1,384 / 0 failed. NB 1,384 against 1,385 before the rebuild — the difference is exactly the hand-made uid-600 link (step 7).
solrproxyconsumer recreated, matching the pre-reset shape (auto_auth=on, redirect = the ALBidxCNAME's/auth).- Images migration launched detached with the 1024 MB limit against
drush.php. ~15 h;d7_images_shanti_imagealone is ~9.3 h at ~200 rows/min.
⚠ Merge freeze while it runs
Nothing touching drupal/**, package/** or pipeline/** may be merged to main until
the run completes — each is a pipeline trigger path, and the deploy restarts the container,
which kills the migration. scripts/** and docs/** are safe.
Remaining steps
- Re-link the uid-600 test identity (not a migration; identity is in
mandala-navina-docs). EXPECT_FILE=scripts/baselines/dev-0.txt ./scripts/migration-cycle.sh validate—integrity:legacy_nid_dupesmust be 0. If the duplicate survives a from-scratch rebuild it is a migration defect, not a July artefact. That is the single most valuable thing this run can tell us.- kmassets:
delete "uid:images-11-*"→index-all→audit --check-stale. - URL smoke tests: public 200 / private 403 / bogus 404.
Then: fill the remaining dev-0 baseline keys (reconciled against the D7 source, not copied
from the result), and land the --depth 1 clone fix.
8. The lesson, again
Every single defect in this session — three runbook errors, two code bugs, three environment obstacles — was invisible to review and static checking, and appeared the moment something was actually executed. The pattern held even for a runbook written specifically to be careful, and for a restore path I had reported as "proven" the same morning.