Skip to content
Snippets Groups Projects
Commit d2a2705c authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3238647 by phenaproxima, tedbow: Create an API for easily accessing...

Issue #3238647 by phenaproxima, tedbow: Create an API for easily accessing relevant Composer information
parent 27c1daa5
No related branches found
No related tags found
No related merge requests found
Showing
with 319 additions and 171 deletions
services:
automatic_updates.readiness_validation_manager:
class: Drupal\automatic_updates\Validation\ReadinessValidationManager
arguments: ['@keyvalue.expirable', '@datetime.time', '@automatic_updates.updater', '@event_dispatcher', 24]
arguments:
- '@keyvalue.expirable'
- '@datetime.time'
- '@automatic_updates.path_locator'
- '@event_dispatcher'
- 24
automatic_updates.updater:
class: Drupal\automatic_updates\Updater
arguments: ['@state', '@string_translation','@package_manager.beginner', '@package_manager.stager', '@package_manager.cleaner', '@package_manager.committer', '@event_dispatcher', '@automatic_updates.path_locator']
arguments:
- '@state'
- '@string_translation'
- '@package_manager.beginner'
- '@package_manager.stager'
- '@package_manager.cleaner'
- '@package_manager.committer'
- '@event_dispatcher'
- '@automatic_updates.path_locator'
automatic_updates.cleaner:
class: Drupal\automatic_updates\ComposerStager\Cleaner
decorates: package_manager.cleaner
......@@ -27,12 +40,12 @@ services:
- { name: event_subscriber }
automatic_updates.staged_projects_validator:
class: Drupal\automatic_updates\Validator\StagedProjectsValidator
arguments: ['@string_translation', '@automatic_updates.path_locator']
arguments:
- '@string_translation'
tags:
- { name: event_subscriber }
automatic_updates.update_version_validator:
class: Drupal\automatic_updates\Validator\UpdateVersionValidator
arguments: ['@automatic_updates.updater']
tags:
- { name: event_subscriber }
automatic_updates.composer_executable_validator:
......
......@@ -12,7 +12,8 @@
},
"require": {
"ext-json": "*",
"php-tuf/composer-stager": "0.2.1"
"php-tuf/composer-stager": "0.2.1",
"composer/composer": "^2"
},
"config": {
"platform": {
......
File moved
<?php
namespace Drupal\package_manager;
use Composer\Composer;
use Composer\Factory;
use Composer\IO\NullIO;
use Drupal\Component\Serialization\Json;
/**
* Defines a utility object to get information from Composer's API.
*/
class ComposerUtility {
/**
* The Composer instance.
*
* @var \Composer\Composer
*/
protected $composer;
/**
* The statically cached names of the Drupal core packages.
*
* @var string[]
*/
private static $corePackages;
/**
* Constructs a new ComposerUtility object.
*
* @param \Composer\Composer $composer
* The Composer instance.
*/
public function __construct(Composer $composer) {
$this->composer = $composer;
}
/**
* Creates a utility object using the files in a given directory.
*
* @param string $dir
* The directory that contains composer.json and composer.lock.
*
* @return \Drupal\package_manager\ComposerUtility
* The utility object.
*/
public static function createForDirectory(string $dir): self {
$io = new NullIO();
$configuration = $dir . DIRECTORY_SEPARATOR . 'composer.json';
// The Composer factory requires that either the HOME or COMPOSER_HOME
// environment variables be set, so momentarily set the COMPOSER_HOME
// variable to the directory we're trying to create a Composer instance for.
// We have to do this because the Composer factory doesn't give us a way to
// pass the home directory in.
// @see \Composer\Factory::getHomeDir()
$home = getenv('COMPOSER_HOME');
putenv("COMPOSER_HOME=$dir");
$composer = Factory::create($io, $configuration);
putenv("COMPOSER_HOME=$home");
return new static($composer);
}
/**
* Returns the canonical names of the supported core packages.
*
* @return string[]
* The canonical list of supported core package names, as listed in
* ../core_packages.json.
*/
protected static function getCorePackageList(): array {
if (self::$corePackages === NULL) {
$file = __DIR__ . '/../core_packages.json';
assert(file_exists($file), "$file does not exist.");
$core_packages = file_get_contents($file);
$core_packages = Json::decode($core_packages);
assert(is_array($core_packages), "$file did not contain a list of core packages.");
self::$corePackages = $core_packages;
}
return self::$corePackages;
}
/**
* Returns the names of the core packages required in composer.json.
*
* All packages listed in ../core_packages.json are considered core packages.
*
* @return string[]
* The names of the required core packages.
*
* @throws \LogicException
* If neither drupal/core or drupal/core-recommended are required.
*
* @todo Make this return a keyed array of packages, not just names.
*/
public function getCorePackageNames(): array {
$requirements = array_keys($this->composer->getPackage()->getRequires());
// Ensure that either drupal/core or drupal/core-recommended are required.
// If neither is, then core cannot be updated, which we consider an error
// condition.
// @todo Move this check to an update validator as part of
// https://www.drupal.org/project/automatic_updates/issues/3241105
$core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements);
if (empty($core_requirements)) {
$file = $this->composer->getConfig()->getConfigSource()->getName();
throw new \LogicException("Drupal core does not appear to be required in $file.");
}
return array_intersect(static::getCorePackageList(), $requirements);
}
/**
* Returns all Drupal extension packages in the lock file.
*
* The following package types are considered Drupal extension packages:
* drupal-module, drupal-theme, drupal-custom-module, and drupal-custom-theme.
*
* @return \Composer\Package\PackageInterface[]
* All Drupal extension packages in the lock file, keyed by name.
*/
public function getDrupalExtensionPackages(): array {
$locked_packages = $this->composer->getLocker()
->getLockedRepository(TRUE)
->getPackages();
$drupal_package_types = [
'drupal-module',
'drupal-theme',
'drupal-custom-module',
'drupal-custom-theme',
];
$drupal_packages = [];
foreach ($locked_packages as $package) {
if (in_array($package->getType(), $drupal_package_types, TRUE)) {
$key = $package->getName();
$drupal_packages[$key] = $package;
}
}
return $drupal_packages;
}
}
<?php
namespace Drupal\Tests\automatic_updates\Kernel;
namespace Drupal\Tests\package_manager\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\KernelTests\KernelTestBase;
......@@ -18,7 +18,7 @@ use Symfony\Component\Finder\Finder;
* For example, the list could live in core/assets, and this test could live
* in the Drupal\Tests\Composer namespace.
*
* @group automatic_updates
* @group package_manager
*/
class CorePackageManifestTest extends KernelTestBase {
......
......@@ -14,17 +14,6 @@ trait PackagesAwareTrait {
*/
protected $packageVersions;
/**
* Constructs a PreStartEvent.
*
* @param string[] $package_versions
* (optional) The desired package versions to update to, keyed by package
* name.
*/
public function __construct(array $package_versions = []) {
$this->packageVersions = $package_versions;
}
/**
* Returns the desired package versions to update to.
*
......
......@@ -2,6 +2,8 @@
namespace Drupal\automatic_updates\Event;
use Drupal\package_manager\ComposerUtility;
/**
* Event fired before staged changes are copied into the active site.
*/
......@@ -9,4 +11,34 @@ class PreCommitEvent extends UpdateEvent {
use ExcludedPathsTrait;
/**
* The Composer utility object for the stage directory.
*
* @var \Drupal\package_manager\ComposerUtility
*/
protected $stageComposer;
/**
* Constructs a new PreCommitEvent object.
*
* @param \Drupal\package_manager\ComposerUtility $active_composer
* A Composer utility object for the active directory.
* @param \Drupal\package_manager\ComposerUtility $stage_composer
* A Composer utility object for the stage directory.
*/
public function __construct(ComposerUtility $active_composer, ComposerUtility $stage_composer) {
parent::__construct($active_composer);
$this->stageComposer = $stage_composer;
}
/**
* Returns a Composer utility object for the stage directory.
*
* @return \Drupal\package_manager\ComposerUtility
* The Composer utility object for the stage directory.
*/
public function getStageComposer(): ComposerUtility {
return $this->stageComposer;
}
}
......@@ -2,6 +2,8 @@
namespace Drupal\automatic_updates\Event;
use Drupal\package_manager\ComposerUtility;
/**
* Event fired before an update begins.
*/
......@@ -10,4 +12,18 @@ class PreStartEvent extends UpdateEvent {
use ExcludedPathsTrait;
use PackagesAwareTrait;
/**
* Constructs a PreStartEvent object.
*
* @param \Drupal\package_manager\ComposerUtility $active_composer
* A Composer utility object for the active directory.
* @param string[] $package_versions
* (optional) The desired package versions to update to, keyed by package
* name.
*/
public function __construct(ComposerUtility $active_composer, array $package_versions = []) {
parent::__construct($active_composer);
$this->packageVersions = $package_versions;
}
}
......@@ -2,6 +2,8 @@
namespace Drupal\automatic_updates\Event;
use Drupal\package_manager\ComposerUtility;
/**
* Event fired when checking if the site could perform an update.
*/
......@@ -9,4 +11,18 @@ class ReadinessCheckEvent extends UpdateEvent {
use PackagesAwareTrait;
/**
* Constructs a ReadinessCheckEvent object.
*
* @param \Drupal\package_manager\ComposerUtility $active_composer
* A Composer utility object for the active directory.
* @param string[] $package_versions
* (optional) The desired package versions to update to, keyed by package
* name.
*/
public function __construct(ComposerUtility $active_composer, array $package_versions = []) {
parent::__construct($active_composer);
$this->packageVersions = $package_versions;
}
}
......@@ -4,6 +4,7 @@ namespace Drupal\automatic_updates\Event;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Component\EventDispatcher\Event;
use Drupal\package_manager\ComposerUtility;
/**
* Event fired when a site is updating.
......@@ -21,6 +22,33 @@ class UpdateEvent extends Event {
*/
protected $results = [];
/**
* The Composer utility object for the active directory.
*
* @var \Drupal\package_manager\ComposerUtility
*/
protected $activeComposer;
/**
* Constructs a new UpdateEvent object.
*
* @param \Drupal\package_manager\ComposerUtility $active_composer
* A Composer utility object for the active directory.
*/
public function __construct(ComposerUtility $active_composer) {
$this->activeComposer = $active_composer;
}
/**
* Returns a Composer utility object for the active directory.
*
* @return \Drupal\package_manager\ComposerUtility
* The Composer utility object for the active directory.
*/
public function getActiveComposer(): ComposerUtility {
return $this->activeComposer;
}
/**
* Adds a validation result.
*
......
......@@ -6,10 +6,10 @@ use Drupal\automatic_updates\Event\PreCommitEvent;
use Drupal\automatic_updates\Event\PreStartEvent;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\Component\Serialization\Json;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\package_manager\ComposerUtility;
use Drupal\system\SystemManager;
use PhpTuf\ComposerStager\Domain\BeginnerInterface;
use PhpTuf\ComposerStager\Domain\CleanerInterface;
......@@ -141,62 +141,19 @@ class Updater {
if (count($project_versions) !== 1 || !array_key_exists('drupal', $project_versions)) {
throw new \InvalidArgumentException("Currently only updates to Drupal core are supported.");
}
$composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory());
$packages = [];
foreach ($this->getCorePackageNames() as $package) {
foreach ($composer->getCorePackageNames() as $package) {
$packages[$package] = $project_versions['drupal'];
}
$stage_key = $this->createActiveStage($packages);
/** @var \Drupal\automatic_updates\Event\PreStartEvent $event */
$event = $this->dispatchUpdateEvent(new PreStartEvent($packages), AutomaticUpdatesEvents::PRE_START);
$event = $this->dispatchUpdateEvent(new PreStartEvent($composer, $packages), AutomaticUpdatesEvents::PRE_START);
$this->beginner->begin($this->pathLocator->getActiveDirectory(), $this->pathLocator->getStageDirectory(), $this->getExclusions($event));
return $stage_key;
}
/**
* Returns the names of the core packages in the project composer.json.
*
* The following packages are considered core packages:
* - drupal/core;
* - drupal/core-recommended;
* - drupal/core-vendor-hardening;
* - drupal/core-composer-scaffold; and
* - drupal/core-project-message.
*
* @return string[]
* The names of the core packages.
*
* @throws \RuntimeException
* If the project composer.json is not found.
* @throws \LogicException
* If the project composer.json does not contain drupal/core or
* drupal/core-recommended.
*
* @todo Move this to an update validator, or use a more robust method of
* detecting the core packages.
*/
public function getCorePackageNames(): array {
$composer = realpath($this->pathLocator->getProjectRoot() . '/composer.json');
if (empty($composer) || !file_exists($composer)) {
throw new \RuntimeException("Could not find project-level composer.json");
}
$data = file_get_contents($composer);
$data = Json::decode($data);
// Ensure that either drupal/core or drupal/core-recommended are required
// by the project. If neither is, then core will not be updated, and we
// consider that an error condition.
$requirements = array_keys($data['require']);
$core_requirements = array_intersect(['drupal/core', 'drupal/core-recommended'], $requirements);
if (empty($core_requirements)) {
throw new \LogicException("Drupal core does not appear to be required in $composer.");
}
$list = file_get_contents(__DIR__ . '/../core_packages.json');
return array_intersect(Json::decode($list), $requirements);
}
/**
* Gets the excluded paths collected by an event object.
*
......@@ -237,10 +194,16 @@ class Updater {
* Commits the current update.
*/
public function commit(): void {
$active_dir = $this->pathLocator->getActiveDirectory();
$active_composer = ComposerUtility::createForDirectory($active_dir);
$stage_dir = $this->pathLocator->getStageDirectory();
$stage_composer = ComposerUtility::createForDirectory($stage_dir);
/** @var \Drupal\automatic_updates\Event\PreCommitEvent $event */
$event = $this->dispatchUpdateEvent(new PreCommitEvent(), AutomaticUpdatesEvents::PRE_COMMIT);
$this->committer->commit($this->pathLocator->getStageDirectory(), $this->pathLocator->getActiveDirectory(), $this->getExclusions($event));
$this->dispatchUpdateEvent(new UpdateEvent(), AutomaticUpdatesEvents::POST_COMMIT);
$event = $this->dispatchUpdateEvent(new PreCommitEvent($active_composer, $stage_composer), AutomaticUpdatesEvents::PRE_COMMIT);
$this->committer->commit($stage_dir, $active_dir, $this->getExclusions($event));
$this->dispatchUpdateEvent(new UpdateEvent($active_composer), AutomaticUpdatesEvents::POST_COMMIT);
}
/**
......
......@@ -4,10 +4,11 @@ namespace Drupal\automatic_updates\Validation;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\PathLocator;
use Drupal\automatic_updates\UpdateRecommender;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\package_manager\ComposerUtility;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
......@@ -44,11 +45,11 @@ class ReadinessValidationManager {
protected $resultsTimeToLive;
/**
* The updater service.
* The path locator service.
*
* @var \Drupal\automatic_updates\Updater
* @var \Drupal\automatic_updates\PathLocator
*/
protected $updater;
protected $pathLocator;
/**
* Constructs a ReadinessValidationManager.
......@@ -57,17 +58,17 @@ class ReadinessValidationManager {
* The key/value expirable factory.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\automatic_updates\Updater $updater
* The updater service.
* @param \Drupal\automatic_updates\PathLocator $path_locator
* The path locator service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
* The event dispatcher service.
* @param int $results_time_to_live
* The number of hours to store results.
*/
public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, Updater $updater, EventDispatcherInterface $dispatcher, int $results_time_to_live) {
public function __construct(KeyValueExpirableFactoryInterface $key_value_expirable_factory, TimeInterface $time, PathLocator $path_locator, EventDispatcherInterface $dispatcher, int $results_time_to_live) {
$this->keyValueExpirable = $key_value_expirable_factory->get('automatic_updates');
$this->time = $time;
$this->updater = $updater;
$this->pathLocator = $path_locator;
$this->eventDispatcher = $dispatcher;
$this->resultsTimeToLive = $results_time_to_live;
}
......@@ -78,10 +79,12 @@ class ReadinessValidationManager {
* @return $this
*/
public function run(): self {
$composer = ComposerUtility::createForDirectory($this->pathLocator->getActiveDirectory());
$recommender = new UpdateRecommender();
$release = $recommender->getRecommendedRelease(TRUE);
if ($release) {
$core_packages = $this->updater->getCorePackageNames();
$core_packages = $composer->getCorePackageNames();
// Update all core packages to the same version.
$package_versions = array_fill(0, count($core_packages), $release->getVersion());
$package_versions = array_combine($core_packages, $package_versions);
......@@ -89,7 +92,7 @@ class ReadinessValidationManager {
else {
$package_versions = [];
}
$event = new ReadinessCheckEvent($package_versions);
$event = new ReadinessCheckEvent($composer, $package_versions);
$this->eventDispatcher->dispatch($event, AutomaticUpdatesEvents::READINESS_CHECK);
$results = $event->getResults();
$this->keyValueExpirable->setWithExpire(
......
......@@ -3,11 +3,8 @@
namespace Drupal\automatic_updates\Validator;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Exception\UpdateException;
use Drupal\automatic_updates\PathLocator;
use Drupal\automatic_updates\Event\PreCommitEvent;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Component\Serialization\Json;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -19,81 +16,32 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The path locator service.
*
* @var \Drupal\automatic_updates\PathLocator
*/
protected $pathLocator;
/**
* Constructs a StagedProjectsValidation object.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The translation service.
* @param \Drupal\automatic_updates\PathLocator $path_locator
* The path locator service.
*/
public function __construct(TranslationInterface $translation, PathLocator $path_locator) {
public function __construct(TranslationInterface $translation) {
$this->setStringTranslation($translation);
$this->pathLocator = $path_locator;
}
/**
* Gets the Drupal packages in a composer.lock file.
*
* @param string $composer_lock_file
* The composer.lock file location.
*
* @return array[]
* The Drupal packages' information, as stored in the lock file, keyed by
* package name.
*/
private function getDrupalPackagesFromLockFile(string $composer_lock_file): array {
if (!file_exists($composer_lock_file)) {
$result = ValidationResult::createError([
$this->t("composer.lock file '@lock_file' not found.", ['@lock_file' => $composer_lock_file]),
]);
throw new UpdateException(
[$result],
'The staged packages could not be evaluated because composer.lock file not found.'
);
}
$composer_lock = file_get_contents($composer_lock_file);
$drupal_packages = [];
$data = Json::decode($composer_lock);
$drupal_package_types = [
'drupal-module',
'drupal-theme',
'drupal-custom-module',
'drupal-custom-theme',
];
$packages = $data['packages'] ?? [];
$packages = array_merge($packages, $data['packages-dev'] ?? []);
foreach ($packages as $package) {
if (in_array($package['type'], $drupal_package_types, TRUE)) {
$drupal_packages[$package['name']] = $package;
}
}
return $drupal_packages;
}
/**
* Validates the staged packages.
*
* @param \Drupal\automatic_updates\Event\UpdateEvent $event
* The update event.
* @param \Drupal\automatic_updates\Event\PreCommitEvent $event
* The event object.
*/
public function validateStagedProjects(UpdateEvent $event): void {
public function validateStagedProjects(PreCommitEvent $event): void {
try {
$active_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getActiveDirectory() . "/composer.lock");
$staged_packages = $this->getDrupalPackagesFromLockFile($this->pathLocator->getStageDirectory() . "/composer.lock");
$active_packages = $event->getActiveComposer()->getDrupalExtensionPackages();
$staged_packages = $event->getStageComposer()->getDrupalExtensionPackages();
}
catch (UpdateException $e) {
foreach ($e->getValidationResults() as $result) {
$event->addValidationResult($result);
}
catch (\Throwable $e) {
$result = ValidationResult::createError([
$e->getMessage(),
]);
$event->addValidationResult($result);
return;
}
......@@ -111,8 +59,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
$new_packages_messages[] = $this->t(
"@type '@name' installed.",
[
'@type' => $type_map[$new_package['type']],
'@name' => $new_package['name'],
'@type' => $type_map[$new_package->getType()],
'@name' => $new_package->getName(),
]
);
}
......@@ -131,8 +79,8 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
$removed_packages_messages[] = $this->t(
"@type '@name' removed.",
[
'@type' => $type_map[$removed_package['type']],
'@name' => $removed_package['name'],
'@type' => $type_map[$removed_package->getType()],
'@name' => $removed_package->getName(),
]
);
}
......@@ -149,14 +97,14 @@ final class StagedProjectsValidator implements EventSubscriberInterface {
if ($pre_existing_packages = array_diff_key($staged_packages, $removed_packages, $new_packages)) {
foreach ($pre_existing_packages as $package_name => $staged_existing_package) {
$active_package = $active_packages[$package_name];
if ($staged_existing_package['version'] !== $active_package['version']) {
if ($staged_existing_package->getVersion() !== $active_package->getVersion()) {
$version_change_messages[] = $this->t(
"@type '@name' from @active_version to @staged_version.",
[
'@type' => $type_map[$active_package['type']],
'@name' => $active_package['name'],
'@staged_version' => $staged_existing_package['version'],
'@active_version' => $active_package['version'],
'@type' => $type_map[$active_package->getType()],
'@name' => $active_package->getName(),
'@staged_version' => $staged_existing_package->getPrettyVersion(),
'@active_version' => $active_package->getPrettyVersion(),
]
);
}
......
......@@ -5,7 +5,6 @@ namespace Drupal\automatic_updates\Validator;
use Drupal\automatic_updates\AutomaticUpdatesEvents;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;
use Drupal\automatic_updates\Event\UpdateEvent;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\Validation\ValidationResult;
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\StringTranslation\StringTranslationTrait;
......@@ -18,23 +17,6 @@ class UpdateVersionValidator implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* Constructs an UpdateVersionSubscriber.
*
* @param \Drupal\automatic_updates\Updater $updater
* The updater service.
*/
public function __construct(Updater $updater) {
$this->updater = $updater;
}
/**
* Returns the running core version, according to the Update module.
*
......@@ -58,7 +40,7 @@ class UpdateVersionValidator implements EventSubscriberInterface {
*/
public function checkUpdateVersion(UpdateEvent $event): void {
$from_version = ExtensionVersion::createFromVersionString($this->getCoreVersion());
$core_package_names = $this->updater->getCorePackageNames();
$core_package_names = $event->getActiveComposer()->getCorePackageNames();
// All the core packages will be updated to the same version, so it doesn't
// matter which specific package we're looking at.
$core_package_name = reset($core_package_names);
......
{
"require": {
"drupal/core": "*"
}
}
{}
{}
{}
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