Skip to content

#6628: Added a command to remove a deploy hook from the list of deployed hooks in the site State #6280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: 13.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/deploycommand.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,36 @@ documentation][HOOK_update_N()] for more details.
| [HOOK_post_update_NAME()](https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Extension!module.api.php/function/hook_post_update_NAME) | Drupal | Runs *before* config is imported. |
| [HOOK_deploy_NAME()](https://github.com/drush-ops/drush/tree/HEAD/drush.api.php) | Drush | Runs *after* config is imported. |

## Deploy Hook Commands

Drush provides several commands to manage deploy hooks:

| Command | Purpose |
| --- | --- |
| `deploy:hook` | Run pending deploy update hooks. |
| `deploy:hook-status` | Show the status of pending deploy hooks. |
| `deploy:mark-complete` | Skip all pending deploy hooks and mark them as complete. |
| `deploy:hook-list` | List all deployed hooks that have been run. |
| `deploy:hook-unset` | Remove a specific hook from the deployed hooks list. |
| `deploy:redeploy` | Re-run a specific deployed hook. |

### Examples

List all deployed hooks:
```shell
drush deploy:hook-list
```

Remove a specific hook from the deployed hooks list:
```shell
drush deploy:hook-unset hook_deploy_NAME
```

Re-run a specific deployed hook:
```shell
drush deploy:redeploy hook_deploy_NAME
```

## Configuration

If you need to customize this command, you should use Drush configuration for the
Expand Down
46 changes: 46 additions & 0 deletions docs/module-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Module Schema Commands

## Overview

The module schema commands allow you to manage the schema version of Drupal modules.

In Drupal, each module maintains a schema version number that is used to track which database updates have been applied. When a module is updated, it may include update hooks (`hook_update_N()`) that need to be run to update the database schema or data. Drupal's update system uses the stored schema version to determine which updates need to be applied.

## Available Commands

### module:schema-set (mss)

Set the schema version for a module.

```bash
drush module:schema-set <module> <version>
```

#### Arguments

- `module`: The machine name of the module.
- `version`: The schema version to set.

#### Examples

```bash
# Set the schema version for system module to 8000
drush module:schema-set system 8000

# Set the schema version for a custom module to 8001
drush mss custom_module 8001
```

#### Use Cases

This command is useful in several scenarios:

1. **Development**: When developing update hooks, you may need to reset the schema version to test your updates.
2. **Troubleshooting**: If an update fails and you need to re-run it, you can set the schema version to a previous value.
3. **Migration**: When migrating a site, you might need to adjust schema versions to match the expected state.
4. **Testing**: For testing update paths or simulating specific module states.

#### Notes

- The module must be installed and enabled for this command to work.
- Use with caution in production environments, as setting incorrect schema versions can lead to update hooks being skipped or run multiple times.
236 changes: 204 additions & 32 deletions src/Commands/core/DeployHookCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Drush\Log\SuccessInterface;
use Psr\Log\LogLevel;

use function OpenTelemetry\Instrumentation\hook;

final class DeployHookCommands extends DrushCommands
{
use AutowireTrait;
Expand All @@ -28,6 +30,10 @@ final class DeployHookCommands extends DrushCommands
const HOOK = 'deploy:hook';
const BATCH_PROCESS = 'deploy:batch-process';
const MARK_COMPLETE = 'deploy:mark-complete';
const HOOK_LIST = 'deploy:hook-list';
const HOOK_UNSET = 'deploy:hook-unset';
const UPDATE_TYPE = '_deploy_';
const HOOK_REDEPLOY = 'deploy:redeploy';

public function __construct(
private readonly SiteAliasManagerInterface $siteAliasManager
Expand Down Expand Up @@ -119,38 +125,7 @@ public function run(): int
throw new UserAbortException();
}

$success = true;
if (!$this->getConfig()->simulate()) {
$operations = [];
foreach ($pending as $function) {
$operations[] = ['\Drush\Commands\core\DeployHookCommands::updateDoOneDeployHook', [$function]];
}

$batch = [
'operations' => $operations,
'title' => 'Updating',
'init_message' => 'Starting deploy hooks',
'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.',
'finished' => [$this, 'updateFinished'],
];
batch_set($batch);
$result = drush_backend_batch_process(self::BATCH_PROCESS);

$success = false;
if (!is_array($result)) {
$this->logger()->error(dt('Batch process did not return a result array. Returned: !type', ['!type' => gettype($result)]));
} elseif (!empty($result[0]['#abort'])) {
// Whenever an error occurs the batch process does not continue, so
// this array should only contain a single item, but we still output
// all available data for completeness.
$this->logger()->error(dt('Update aborted by: !process', [
'!process' => implode(', ', $result[0]['#abort']),
]));
} else {
$success = true;
}
}

$success = $this->batchOperation($pending);
$level = $success ? SuccessInterface::SUCCESS : LogLevel::ERROR;
$this->logger()->log($level, dt('Finished performing deploy hooks.'));
return $success ? self::EXIT_SUCCESS : self::EXIT_FAILURE;
Expand Down Expand Up @@ -283,4 +258,201 @@ public function markComplete(): int
$this->logger()->success(dt('Marked %count pending deploy hooks as complete.', ['%count' => count($pending)]));
return self::EXIT_SUCCESS;
}

/**
* Prints information about deployed hooks.
*
* @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
*/
#[CLI\Command(name: self::HOOK_LIST)]
#[CLI\Usage(name: 'drush deploy:hook-list', description: 'Prints information about deployed hooks.')]
#[CLI\FieldLabels(labels: [
'module' => 'Module',
'hook' => 'Hook',
])]
#[CLI\DefaultTableFields(fields: ['module', 'hook'])]
#[CLI\FilterDefaultField(field: 'hook')]
#[CLI\Topics(topics: [DocsCommands::DEPLOY])]
#[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
public function list(): RowsOfFields
{
$update_functions = $this->getDeployedHooks();

$updates = [];
foreach ($update_functions as $function) {
// Validate function name format.
if (!str_contains($function, self::UPDATE_TYPE)) {
$this->logger()->warning("Skipping invalid hook function: {function}", ['function' => $function]);
continue;
}

// Split function name into extension and update.
[$extension, $update] = explode(self::UPDATE_TYPE, $function);
if (empty($extension) || empty($update)) {
$this->logger()->warning("Invalid hook function format: {function}", ['function' => $function]);
continue;
}

// Store the update data.
$updates[$extension]['deployed'][$update] = true;
if (!isset($updates[$extension]['start'])) {
$updates[$extension]['start'] = $update;
}
}
$rows = [];
foreach ($updates as $module => $update_data) {
foreach ($update_data['deployed'] as $hook => $value) {
$rows[] = [
'module' => $module,
'hook' => $hook,
];
}
}
return new RowsOfFields($rows);
}

/**
* Unsets a hook from the deployed hooks list.
*
* @param string $hook_name The name of the hook to remove (e.g., hook_deploy_NAME)
*
* @return int Exit code
*/
#[CLI\Command(name: self::HOOK_UNSET)]
#[CLI\Argument(name: 'hook_name', description: 'The name of the hook to remove (e.g., hook_deploy_NAME)')]
#[CLI\Usage(
name: 'drush deploy:hook-unset hook_deploy_NAME',
description: 'Removes the specified hook from the deployed hooks list'
)]
#[CLI\Topics(topics: [DocsCommands::DEPLOY])]
#[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
public function unset(string $hook_name): int
{
$deployed_hooks = $this->getDeployedHooks();
// Check if the hook exists.
if (!in_array($hook_name, $deployed_hooks, true)) {
$this->logger()->warning("Hook {hook} not found in deployed hooks.", [
'hook' => $hook_name
]);
return self::EXIT_SUCCESS;
}
// Remove the hook from the list.
$update_functions = array_filter($deployed_hooks, function ($function) use ($hook_name) {
return $function !== $hook_name;
});

// Update the deployed hook list.
\Drupal::service('keyvalue')->get('deploy_hook')->set('existing_updates', $update_functions);
Copy link

Choose a reason for hiding this comment

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

We should have this service available as $this->keyValue.

Copy link
Author

@harivansh0 harivansh0 May 12, 2025

Choose a reason for hiding this comment

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

Hi @MurzNN
The DeployHookCommands class does not have a keyValue property. Although the getRegistry() function injects the service, the class extends UpdateRegistry, which defines keyValue as a protected property. Since it's protected and not accessible directly in the subclass, we cannot use it, without modifying the drupal core UpdateRegistry or use refelection.
Or I could using autowire to inject
#[Autowire(service: 'keyvalue')] protected KeyValueFactoryInterface $keyValueFactory,

$this->logger()->success(dt('Hook !hook_name removed from deployed hooks list.', [
'!hook_name' => $hook_name
]));
return self::EXIT_SUCCESS;
}

/**
* Redeploys a hook.
*
* @param string $hook_name
* The name of the hook to redeploy (e.g., hook_deploy_NAME)
*
* @return int
* Exit code.
* @throws \Drush\Exceptions\UserAbortException
*/
#[CLI\Command(name: self::HOOK_REDEPLOY)]
#[CLI\Argument(name: 'hook_name', description: 'The name of the hook to redeploy (e.g., hook_deploy_NAME)')]
#[CLI\Usage(
name: 'drush deploy:redeploy hook_deploy_NAME',
description: 'Redeploys the specified hook'
)]
#[CLI\Version(version: '10.3')]
#[CLI\Topics(topics: [DocsCommands::DEPLOY])]
#[CLI\Bootstrap(level: DrupalBootLevels::FULL)]
public function redeploy(string $hook_name): int
{
$deployed_hooks = $this->getDeployedHooks();
if (!in_array($hook_name, $deployed_hooks)) {
$this->logger()->success("Hook {$hook_name} not found in deployed hooks.", [
'hook' => $hook_name
]);
return self::EXIT_SUCCESS;
}

if (!$this->io()->confirm(dt('Do you wish to run the specified deployed hooks?'))) {
throw new UserAbortException();
}
// Build and run the batch process.
$success = $this->batchOperation([$hook_name]);
$level = $success ? SuccessInterface::SUCCESS : LogLevel::ERROR;
$this->logger()->log($level, dt('Finished performing re-deploy hooks.'));
return $success ? self::EXIT_SUCCESS : self::EXIT_FAILURE;
}


/**
* Get all deployed hooks.
*
* @return array
*/
public function getDeployedHooks(): array
{
$store = \Drupal::service('keyvalue')->get('deploy_hook');
return $store->get('existing_updates', []);
}

/**
* Build the batch process to run the deployment hooks.
*
* @param array<int, string> $hooks
* An array of function names to be executed as deploy hooks.
*
* @return bool
* TRUE if the batch process was started successfully, FALSE otherwise.
*/
public function batchOperation(array $hooks): bool
{
$success = true;
if (!$this->getConfig()->simulate()) {
$operations = [];
foreach ($hooks as $function) {
$operations[]
= [
'\Drush\Commands\core\DeployHookCommands::updateDoOneDeployHook',
[$function]
];
}

$batch = [
'operations' => $operations,
'title' => 'Updating',
'init_message' => 'Starting deploy hooks',
'error_message' => 'An unrecoverable error has occurred. You can find the error message below.
It is advised to copy it to the clipboard for reference.',
'finished' => [$this, 'updateFinished'],
];
batch_set($batch);
$result = drush_backend_batch_process(self::BATCH_PROCESS);

$success = false;
if (!is_array($result)) {
$this->logger()->error(
dt(
'Batch process did not return a result array. Returned: !type',
['!type' => gettype($result)]
)
);
} elseif (!empty($result[0]['#abort'])) {
// Whenever an error occurs, the batch process does not continue, so
// this array should only contain a single item, but we still output
// all available data for completeness.
$this->logger()->error(dt('Update aborted by: !process', [
'!process' => implode(', ', $result[0]['#abort']),
]));
} else {
$success = true;
}
}

return $success;
}
}
Loading