Skip to content
Snippets Groups Projects
Commit 4b6cef8d authored by Ted Bowman's avatar Ted Bowman Committed by Adam G-H
Browse files

Issue #3351895 by tedbow, phenaproxima, pwolanin, xjm: Add command to allow...

Issue #3351895 by tedbow, phenaproxima, pwolanin, xjm: Add command to allow running cron updates via console and by a separate user, for defense-in-depth
parent d8654de0
No related branches found
No related tags found
No related merge requests found
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
"composer-runtime-api": "^2.1" "composer-runtime-api": "^2.1"
}, },
"require-dev": { "require-dev": {
"colinodell/psr-testlogger": "^1.2" "colinodell/psr-testlogger": "^1.2",
"drush/drush": "^11"
}, },
"scripts": { "scripts": {
"phpcbf": "scripts/phpcbf.sh", "phpcbf": "scripts/phpcbf.sh",
...@@ -42,5 +43,12 @@ ...@@ -42,5 +43,12 @@
"psr-4": { "psr-4": {
"Drupal\\automatic_updates\\Development\\": "scripts/src" "Drupal\\automatic_updates\\Development\\": "scripts/src"
} }
},
"extra": {
"drush": {
"services": {
"drush.services.yml": "^11"
}
}
} }
} }
\ No newline at end of file
services:
_defaults:
autowire: true
Drupal\automatic_updates\Commands\AutomaticUpdatesCommands:
tags:
- { name: drush.command }
Drupal\automatic_updates\DrushUpdateStage:
calls:
- ['setLogger', ['@logger.channel.automatic_updates']]
...@@ -61,6 +61,7 @@ composer require \ ...@@ -61,6 +61,7 @@ composer require \
# automatic_updates' development dependencies to the root. # automatic_updates' development dependencies to the root.
# @see https://getcomposer.org/doc/04-schema.md#require-dev # @see https://getcomposer.org/doc/04-schema.md#require-dev
composer require --dev colinodell/psr-testlogger:^1 composer require --dev colinodell/psr-testlogger:^1
composer require --dev drush/drush:^11
# Revert needless changes to Core Composer metapackages. # Revert needless changes to Core Composer metapackages.
git checkout -- "$SITE_DIRECTORY/composer/Metapackage" git checkout -- "$SITE_DIRECTORY/composer/Metapackage"
......
<?php
declare(strict_types = 1);
namespace Drupal\automatic_updates\Commands;
use Drupal\automatic_updates\DrushUpdateStage;
use Drush\Commands\DrushCommands;
/**
* Contains Drush commands for Automatic Updates.
*
* @internal
* This is an internal part of Automatic Updates and may be changed or removed
* at any time without warning. It should not be called directly, and external
* code should not interact with it.
*/
final class AutomaticUpdatesCommands extends DrushCommands {
/**
* Constructs a AutomaticUpdatesCommands object.
*
* @param \Drupal\automatic_updates\DrushUpdateStage $stage
* The console cron updater service.
*/
public function __construct(private readonly DrushUpdateStage $stage) {
parent::__construct();
}
/**
* Automatically updates Drupal core.
*
* @usage auto-update
* Automatically updates Drupal core, if any updates are available.
*
* @option $post-apply Internal use only.
* @option $stage-id Internal use only.
* @option $from-version Internal use only.
* @option $to-version Internal use only.
*
* @command auto-update
*
* @throws \LogicException
* If the --post-apply option is provided without the --stage-id,
* --from-version, and --to-version options.
*/
public function autoUpdate(array $options = ['post-apply' => FALSE, 'stage-id' => NULL, 'from-version' => NULL, 'to-version' => NULL]) {
$io = $this->io();
// The second half of the update process (post-apply etc.) is done by this
// exact same command, with some additional flags, in a separate process to
// ensure that the system is in a consistent state.
// @see \Drupal\automatic_updates\DrushUpdateStage::triggerPostApply()
if ($options['post-apply']) {
if (empty($options['stage-id']) || empty($options['from-version']) || empty($options['to-version'])) {
throw new \LogicException("The post-apply option is for internal use only. It should never be passed directly.");
}
$message = sprintf('Drupal core was successfully updated to %s!', $options['to-version']);
$io->success($message);
$io->info('Running post-apply tasks and final clean-up...');
$this->stage->handlePostApply($options['stage-id'], $options['from-version'], $options['to-version']);
}
else {
if ($this->stage->getMode() === DrushUpdateStage::DISABLED) {
$io->error('Automatic updates are disabled.');
return;
}
$release = $this->stage->getTargetRelease();
if ($release) {
$message = sprintf('Updating Drupal core to %s. This may take a while.', $release->getVersion());
$io->info($message);
$this->stage->performUpdate($release->getVersion(), 300);
}
else {
$io->info("There is no Drupal core update available.");
}
}
}
}
...@@ -168,7 +168,7 @@ class CronUpdateStage extends UpdateStage { ...@@ -168,7 +168,7 @@ class CronUpdateStage extends UpdateStage {
* How long to allow the operation to run before timing out, in seconds, or * How long to allow the operation to run before timing out, in seconds, or
* NULL to never time out. * NULL to never time out.
*/ */
private function performUpdate(string $target_version, ?int $timeout): void { protected function performUpdate(string $target_version, ?int $timeout): void {
$project_info = new ProjectInfo('drupal'); $project_info = new ProjectInfo('drupal');
if (!$this->isAvailable()) { if (!$this->isAvailable()) {
......
<?php
declare(strict_types = 1);
namespace Drupal\automatic_updates;
use Drush\Drush;
/**
* An updater that runs via a Drush command.
*/
final class DrushUpdateStage extends CronUpdateStage {
/**
* {@inheritdoc}
*/
protected function triggerPostApply(string $stage_id, string $start_version, string $target_version): void {
$alias = Drush::aliasManager()->getSelf();
$output = Drush::processManager()
->drush($alias, 'auto-update', [], [
'post-apply' => TRUE,
'stage-id' => $stage_id,
'from-version' => $start_version,
'to-version' => $target_version,
])
->mustRun()
->getOutput();
// Ensure the output of the sub-process is visible.
Drush::output()->write($output);
}
/**
* {@inheritdoc}
*/
public function performUpdate(string $target_version, ?int $timeout): void {
// Overridden to expose this method to calling code.
parent::performUpdate($target_version, $timeout);
}
}
...@@ -5,6 +5,7 @@ declare(strict_types = 1); ...@@ -5,6 +5,7 @@ declare(strict_types = 1);
namespace Drupal\Tests\automatic_updates\Build; namespace Drupal\Tests\automatic_updates\Build;
use Behat\Mink\Element\DocumentElement; use Behat\Mink\Element\DocumentElement;
use Drupal\automatic_updates\DrushUpdateStage;
use Drupal\automatic_updates\CronUpdateStage; use Drupal\automatic_updates\CronUpdateStage;
use Drupal\automatic_updates\UpdateStage; use Drupal\automatic_updates\UpdateStage;
use Drupal\Composer\Composer; use Drupal\Composer\Composer;
...@@ -17,6 +18,7 @@ use Drupal\package_manager\Event\PreCreateEvent; ...@@ -17,6 +18,7 @@ use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreDestroyEvent; use Drupal\package_manager\Event\PreDestroyEvent;
use Drupal\package_manager\Event\PreRequireEvent; use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\Tests\WebAssert; use Drupal\Tests\WebAssert;
use Symfony\Component\Process\Process;
/** /**
* Tests an end-to-end update of Drupal core. * Tests an end-to-end update of Drupal core.
...@@ -408,4 +410,40 @@ class CoreUpdateTest extends UpdateTestBase { ...@@ -408,4 +410,40 @@ class CoreUpdateTest extends UpdateTestBase {
$assert_session->pageTextContains('Ready to update'); $assert_session->pageTextContains('Ready to update');
} }
// BEGIN: DELETE FROM CORE MERGE REQUEST
/**
* Tests updating via Drush.
*/
public function testDrushUpdate(): void {
$this->createTestProject('RecommendedProject');
$this->runComposer('composer require drush/drush', 'project');
$dir = $this->getWorkspaceDirectory() . '/project';
$command = [
$dir . '/vendor/drush/drush/drush',
'auto-update',
'--verbose',
];
$process = new Process($command, $dir . '/web/sites/default');
// Give the update process as much time as it needs to run.
$process->setTimeout(NULL)->mustRun();
$output = $process->getOutput();
$this->assertStringContainsString('Updating Drupal core to 9.8.1. This may take a while.', $output);
$this->assertStringContainsString('Drupal core was successfully updated to 9.8.1!', $output);
$this->assertStringContainsString('Running post-apply tasks and final clean-up...', $output);
$this->assertUpdateSuccessful('9.8.1');
$this->assertExpectedStageEventsFired(DrushUpdateStage::class);
// Rerunning the command should exit with a message that no newer version
// is available.
$process = new Process($command, $process->getWorkingDirectory());
$process->mustRun();
$this->assertStringContainsString("There is no Drupal core update available.", $process->getOutput());
}
// END: DELETE FROM CORE MERGE REQUEST
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment