Decision Use 'drush deploy' when building Drupal sites

accepted

When updating Drupal code and configuration in a given environment, it's useful to make sure all members of the team and all teams follow a consistent set of steps, in the same order. Having consistent steps across multiple projects will reduce onboarding for new team members.

Decision

We standardize the build steps as the ones implemented by the drush deploy command.

Sites are strongly encouraged to run drush deploy directly as part of their automation pipelines.

Developers should write update hooks using these guidelines:

HOOK_update_N()

Code that does not rely on any Drupal services, entity CRUD operations, entity APIs, router system, etc, usually to perform direct database queries. Check the documentation for more details about what is safe to use in these hooks.

These hooks are the first ones to be executed, before configuration is imported.

HOOK_post_update_NAME()

These are called when Drupal is fully bootstrapped and all Drupal APIs are safe to use. This is usually the place to write code that needs to alter content but doesn't require the new configuration in place, since these hooks will be executed before configuration is imported.

HOOK_deploy_NAME()

These are equivalent to HOOK_post_update_NAME() in terms of APIs available, the only difference being that this is executed after configuration is imported.

Customizing the build steps

Under certain circumstances, sites may need to adjust the steps performed during Drupal builds. For example, some sites might need to import configuration twice due to platform or site architecture constraints. In these cases, sites are encouraged to replace the implementation of drush deploy with their own set of steps.

Here is an example snippet that demonstrates an implementation that swaps drush deploy with a custom set of steps, performing config import twice:

// FILE: [project root]/drush/Commands/DeployReplaceCommands.php

<?php

declare(strict_types=1);

namespace Drush\Commands;

use Consolidation\AnnotatedCommand\Hooks\HookManager;
use Consolidation\SiteAlias\SiteAliasManagerAwareTrait;
use Drush\Attributes as CLI;
use Drush\Boot\DrupalBootLevels;
use Drush\Commands\config\ConfigImportCommands;
use Drush\Commands\core\CacheRebuildCommands;
use Drush\Commands\core\DeployCommands;
use Drush\Commands\core\DeployHookCommands;
use Drush\Commands\core\UpdateDBCommands;
use Drush\Drush;
use Drush\SiteAlias\SiteAliasManagerAwareInterface;

/**
 * Replaces the core deploy command with a custom implementation.
 *
 * This lives as a site-level command file (drush/Commands/) rather than in a
 * Drupal module because the original deploy command uses bootstrap level NONE.
 * Module-provided hooks are only registered during Drupal bootstrap, which
 * never happens for NONE commands — so a module-level replace-command hook
 * would never fire.
 */
#[CLI\Bootstrap(DrupalBootLevels::NONE)]
class DeployReplaceCommands extends DrushCommands implements SiteAliasManagerAwareInterface {

  use SiteAliasManagerAwareTrait;

  #[CLI\Hook(type: HookManager::REPLACE_COMMAND_HOOK, target: DeployCommands::DEPLOY)]
  public function replaceDeploy(): void {
    // This is a copy of \Drush\Commands\core\DeployCommands::deploy() but
    // performing two config imports instead of one.
    $self = $this->siteAliasManager()->getSelf();
    $redispatch_options = Drush::redispatchOptions();
    $manager = $this->processManager();

    $this->logger()->notice("Database updates start.");
    $process = $manager->drush($self, UpdateDBCommands::UPDATEDB, [], $redispatch_options);
    $process->mustRun($process->showRealtime());

    $this->logger()->success("First config import start.");
    $process = $manager->drush($self, ConfigImportCommands::IMPORT, [], $redispatch_options);
    $process->mustRun($process->showRealtime());

    $this->logger()->success("Second config import start.");
    $process = $manager->drush($self, ConfigImportCommands::IMPORT, [], $redispatch_options);
    $process->mustRun($process->showRealtime());

    $this->logger()->success("Cache rebuild start.");
    $process = $manager->drush($self, CacheRebuildCommands::REBUILD, [], $redispatch_options);
    $process->mustRun($process->showRealtime());

    $this->logger()->success("Deploy hook start.");
    $process = $manager->drush($self, DeployHookCommands::HOOK, [], $redispatch_options);
    $process->mustRun($process->showRealtime());
  }

}

Consequences

  • Developers have a clear and consistent guide to use update hooks among projects.
  • Automation can be built to ensure these steps are executed consistently.