And Now We’re Ready: Final Tests and Flipping the Switch

A rocket on a launchpad with the boosters ignited.

Part 5 of a series. Start with Part 1 if you’re just joining.

There’s a particular kind of satisfaction that comes with getting to this point. Weeks of groundwork — running upgrade reports, auditing modules, rewriting templates, migrating themes, upgrading the database engine — and now we’re finally here. The infrastructure is ready. The modules are compatible. The themes are migrated. Every known blocker has been resolved.

Now we actually upgrade Drupal.

The remaining phases move faster than what came before. Phases 4 through 8 are: take backups, upgrade core, run database updates, test each subsite, deploy to production. If you’ve done the Phase 3 work carefully, each of these steps should proceed without surprises. That’s the payoff for doing the groundwork first.

Phase 4: Pre-Upgrade Backups

Even with all the preparation, you want a snapshot of the current working state immediately before making the irreversible changes. Not the backup you took before the MariaDB upgrade — a fresh one, from the current state of the codebase and database.

For this project, that means: - Exporting the single shared local database (all four subsites, one dump) - Committing or stashing any local changes that aren’t already committed - Creating a dedicated git branch for the upgrade work

The branch is important. It gives you a clean before/after in git history, makes it easy to open a pull request or review the diff before merging to production, and gives you an easy rollback path if something goes wrong after deployment.

Phase 5: Upgrading Core and Contrib

The actual upgrade is a composer.json constraint update followed by composer update. The constraint change tells Composer that you want Drupal 11; the update resolves the dependency graph and downloads everything.

The core packages to update: - drupal/core - drupal/core-recommended - drupal/core-composer-scaffold - drupal/core-project-message

All four should move to ^11 together. Trying to update them independently tends to cause dependency conflicts.

Before running composer update, also remove any patches in composer.json that are no longer needed. Patches that were applied to fix D10 bugs may not apply cleanly to D11 versions of the same package, and a failed patch will stop the Composer update cold. For this project, the business_responsive_theme patches could be removed entirely since we’d already replaced that theme. The address module patches needed verification — whether the bug had been fixed in the current D11-compatible release.

When you run composer update, watch the output carefully. Composer will tell you if it can’t resolve a constraint, and the error messages, while sometimes cryptic, do tell you which package is blocking the resolution. Most conflicts trace back to a contrib module whose version constraint hasn’t been updated to allow D11 — the fix is to update that constraint and try again.

After composer update completes successfully, don’t open the browser yet. Run drush status first to confirm Drupal recognizes the new version and the database is reachable. Then proceed to Phase 6.

Phase 6: Database Updates

Drupal stores a lot of state in the database — schema versions, configuration, cached data. When you upgrade core (and contrib modules), many of these need to be migrated to the new structure via database update hooks. drush updb runs those update hooks.

For a multi-site installation, you run updb for each subsite independently. Even though they share a single database, they have separate table prefixes and separate Drupal bootstrap paths. Running updb against one site updates its tables; you have to run it explicitly for each.

The typical pattern is: run updb, check for errors, then clear cache. Run updb first — don’t clear cache before update hooks have run, because some update hooks depend on cached data.

If updb completes without errors on all four subsites, you’re in good shape. If it reports errors, read them carefully. Update hook failures usually have descriptive messages that point directly at the problem.

Phase 7: Testing

With the database updates complete, the sites should be running on Drupal 11. Now you verify that “running” actually means “working.”

The testing checklist for each subsite:

  • Does the site load without PHP errors or Drupal exceptions?
  • Is the admin UI accessible and functional?
  • Does the content display correctly — articles, pages, whatever content types this site uses?
  • Do file uploads and media work?
  • If the site has e-commerce (aircooled does), does checkout flow work end-to-end?
  • Do webforms submit correctly?
  • Is Redis caching active? (Check /admin/reports/status)
  • Does cron run without errors? (Check the recent log messages)

Most issues at this stage are one of three things: a module that wasn’t fully compatible after all, a cache that needed clearing, or a configuration that got garbled during the update. The Drupal watchdog log at /admin/reports/dblog is your best friend for tracking down what’s actually failing.

Test locally until you’re confident everything works. The time you spend here is vastly cheaper than debugging a broken production site.

Phase 8: Deploy to Production

The final phase is deploying to Platform.sh. By the time you get here, the MariaDB upgrade (Phase 2) should already be complete — Platform.sh should already be running MariaDB 10.6+. If you haven’t done that yet, do it now before pushing the upgraded code.

The deployment sequence: 1. Push the upgrade branch to Platform.sh and let it build 2. Once the build succeeds, SSH in and run updb for each subsite on Platform.sh 3. Verify all subsites are working on the production environment 4. Merge the upgrade branch to master

Running updb on Platform.sh after the code push is the same step you did locally — the production database needs those update hooks to run just like the local one did. Don’t skip it.

Once you’ve verified everything is working and merged to master, you’re done. Drupal 11.

A Note on What Comes After

A few housekeeping items that are worth handling after the upgrade is stable:

mglaman/composer-drupal-lenient — This package allows Composer to install modules that haven’t explicitly declared D11 compatibility even when they might work fine. It’s a useful bridge during an upgrade but a crutch you don’t want to leave in place indefinitely. Once you’re on D11 and all your modules have proper D11-compatible releases, remove it.

composer.json description — If you’ve been running this project since the Drupal 8 era, there’s probably a line in composer.json that says something like “A Drupal 8 project.” Update it.

Platform.sh PHP version reference — If your CLAUDE.md or any documentation refers to the Platform.sh PHP version, update it to reflect 8.3.

These don’t affect functionality, but they’re the kind of thing that confuses people (including future you) who look at the project six months from now.

Reflecting on the Process

Looking back at the full arc of this upgrade, the thing that made the biggest difference was the planning phase. Not the tooling, not the scripts — the act of laying out every dependency and blocker in order before touching anything.

Every previous Drupal upgrade I did the same way: read the change record, update things in roughly the right order, fix problems as they came up. It worked, but there were always a few ugly hours somewhere in the middle where I was debugging two problems at once and couldn’t tell which was causing which.

Having a written plan with explicit phases changed that. When I hit a problem, I knew exactly where I was in the sequence and what should already be resolved. That context made debugging much faster.

The other thing worth noting: the theme migration — the part that felt most daunting when I looked at the Upgrade Status report — ended up being well-scoped once I actually read the code. Yes, one template needed a full rewrite. But the rewrite was maybe two hours of work once I understood what Bootstrap 5 expected. The fear was larger than the task.

That’s usually how it goes.

Expert Mode: Phases 4 Through 8

Phase 4 — Backups and Branching

# Export the local database (all subsites share one database) ddev export-db --database=db --gzip --file=backups/hinchodb-local-pre-d11.sql.gz # Commit any outstanding local changes git add -A git commit -m "pre-d11 upgrade state" # Create the upgrade branch git checkout -b drupal11-upgrade

Phase 5 — Upgrade Core and Contrib

Update core version constraints:

ddev composer require \  "drupal/core:^11" \  "drupal/core-recommended:^11" \  "drupal/core-composer-scaffold:^11" \  "drupal/core-project-message:^11"

Remove patches that no longer apply:

In composer.json, find the patches section and remove any patches for modules you’ve removed or that have been fixed in the D11-compatible release. Then run:

# Run the update ddev composer update # If you get dependency conflicts, identify the blocker: ddev composer why-not drupal/core:^11 # Verify the update succeeded ddev drush --uri=aircooled status | grep "Drupal version"

Phase 6 — Database Updates

# Run updb and clear cache on each subsite for site in aircooled careersinspanish figueroa svloba; do  echo "=== Running updb for $site ==="  ddev drush --uri=$site updb -y  ddev drush --uri=$site cr  echo "=== Done with $site ===" done

If updb reports errors:

# Check the full error output ddev drush --uri=aircooled updb -v # Check the database log for error details ddev drush --uri=aircooled watchdog:show --type=php --severity=error

Phase 7 — Testing

# Quick smoke test — verify each site loads and Drupal reports healthy for site in aircooled careersinspanish figueroa svloba; do  echo "=== $site ==="  ddev drush --uri=$site status  ddev drush --uri=$site core:requirements --severity=2  # show warnings and errors only done # Check Redis is active ddev drush --uri=aircooled php:eval "echo \Drupal::cache()->get('some_key') !== FALSE ? 'cache active' : 'checking...';" # Check cron runs clean ddev drush --uri=aircooled cron ddev drush --uri=aircooled watchdog:show --count=20

Phase 8 — Deploy to Production

Verify Platform.sh MariaDB version before pushing:

platform ssh -- mysql --version

Push the upgrade branch:

git push platform drupal11-upgrade

Monitor the build on Platform.sh:

platform activity:log

Run database updates on production after the build:

for site in aircooled careersinspanish figueroa svloba; do  echo "=== Running updb for $site on Platform.sh ==="  platform drush --uri=$site updb -y  platform drush --uri=$site cr done

Verify production:

for site in aircooled careersinspanish figueroa svloba; do  echo "=== $site ==="  platform drush --uri=$site status | grep "Drupal version" done

Merge to master when everything checks out:

git checkout master git merge drupal11-upgrade git push platform master # Or open a PR and merge through your normal process

Post-upgrade cleanup:

# Remove mglaman/composer-drupal-lenient if no longer needed ddev composer remove mglaman/composer-drupal-lenient # Update composer.json description from "Drupal 8" to "Drupal 11" # (edit manually in composer.json) # Run a final composer update to make sure everything is clean ddev composer update

That’s the full upgrade. Drupal 6 to 7, 7 to 8, 8 to 9, 9 to 10, and now 10 to 11 — every major version since the beginning. The methods change, the tools get better, but the fundamentals stay the same: understand before you act, work in order, and test before you deploy.