├── .drupal-updater.yml.dist ├── .gitignore ├── README.md ├── composer.json ├── drupal-updater ├── scripts └── unsupported-modules.php └── src ├── Config ├── Config.php └── ConfigInterface.php ├── PostUpdate ├── PostUpdateDDQG.php └── PostUpdateInterface.php └── UpdaterCommand.php /.drupal-updater.yml.dist: -------------------------------------------------------------------------------- 1 | # Commits by drupal updater will be authored by this author. 2 | author: "Drupal " 3 | # Set to true to only update packages deployed in production. 4 | noDev: false 5 | # Set to true to only update securities. 6 | onlySecurities: false 7 | # Allows specify which packages will be updated. 8 | packages: [] 9 | # List of environments to update. 10 | environments: ['@self'] 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drupal updater 2 | 3 | Drupal updater helps you to update the drupal modules of your site. 4 | 5 | It does an update of all your drupal modules and dependencies defined in the composer.json. 6 | 7 | It also allows update only securities. 8 | 9 | ## Requirements 10 | 11 | This package works with: 12 | 13 | - Drush >=10. 14 | - Composer 2.4 (global). 15 | 16 | Or alternatively, you can run it inside tools like [ddev](https://ddev.com). 17 | 18 | ## Installation 19 | 20 | Before doing the installation, make sure your environment has composer 2.4 or higher installed locally. 21 | 22 | ```bash 23 | composer require metadrop/drupal-updater 24 | ``` 25 | 26 | Or, if you are using `ddev`: 27 | ```bash 28 | ddev composer require metadrop/drupal-updater 29 | ``` 30 | 31 | ## Configuration 32 | 33 | Configuration helps automating update workflows. All the parameters that are repeated through updates 34 | can be added to a configuration file to just launch `drupal-updater`, saving time adding the parameters manually, 35 | or doing custom helpers in local / ci environments. 36 | 37 | There is a template with configuration ready to use at **vendor/metadrop/drupal-updater/drupal-updater.yml.dist**, to use it just copy it to the root: 38 | 39 | ``` 40 | cp vendor/metadrop/drupal-updater/.drupal-updater.yml.dist .drupal-updater.yml 41 | ``` 42 | 43 | The file .drupal-updater.yml at root is the default path, but it is possible to override configuration path by using the **--config** parameter: 44 | 45 | ``` 46 | drupal-updater --config .drupal-updater.securities.yml 47 | ``` 48 | 49 | Edit .drupal-updater.yml to setup custom parameters when needed. 50 | 51 | ### Configuration variables 52 | 53 | The following variables can be setup through .drupal-updater.yml 54 | 55 | - **author**: Commits author 56 | - **noDev**: Set to true to only update packages deployed in production. 57 | - **onlySecurities**: Set to true to only update securities. 58 | - **packages**: Allows specify which packages will be updated. 59 | - **environments**: Array list of environments to update. 60 | 61 | 62 | ## How it works 63 | 64 | 65 | This module will try to update your dependencies based on how they are required in the composer.json. 66 | 67 | - Before starting to update, all the Drupal configuration is consolidated and commited into GIT. 68 | - For each module / package updated the changes are commited: 69 | - For PHP packages, it commits the composer.lock 70 | - For Drupal extensions, it applies the updates, commits the configuration changed and the modified files. On multisites environments it will export/commit the configuration for all environments keeping them all synchronized (see parameters). 71 | 72 | If a package has an available update and that update can't be done by running `composer update`, it won't do the upgrade. This means that not all packages will be upgraded, but most of them yes. 73 | 74 | ## Usage 75 | 76 | Basic update: 77 | 78 | ```bash 79 | ./vendor/bin/drupal-updater update 80 | ``` 81 | 82 | Parameters allowed: 83 | 84 | - **--config**: Specify where the configuration file is located. 85 | - **--security**: It will update only securities. 86 | - **--no-dev**: It won't update dev dependencies, only the primary ones. 87 | - **--author**: It sets the git commits author. Example: `Test` 88 | - **--environment**: List of sites (drush alias) to be run on Drupal Multisites. The drush alias must be local. 89 | 90 | Examples: 91 | 92 | - Update securities: 93 | 94 | ```bash 95 | ./vendor/bin/drupal-updater --security 96 | ``` 97 | 98 | - Update only primary packages: 99 | 100 | ```bash 101 | ./vendor/bin/drupal-updater --no-dev 102 | ``` 103 | 104 | - Update specific packages: 105 | 106 | ```bash 107 | ./vendor/bin/drupal-updater --packages=drupal/core-recommended,drupal/core-dev 108 | ``` 109 | 110 | - Update with a specific author: 111 | 112 | ```bash 113 | ./vendor/bin/drupal-updater --author=Test 114 | ``` 115 | 116 | - Update on multiple sites (Drupal Multisite): 117 | 118 | ```bash 119 | ./vendor/bin/drupal-updater --environments=@site1.local,@site2.local,@site3.local,@site4.local 120 | ``` 121 | 122 | ### DDEV 123 | 124 | If you are using `ddev`, you can just run the commands above prepending `ddev exec`. 125 | 126 | Example: 127 | 128 | ```bash 129 | ddev exec ./vendor/bin/drupal-updater --security 130 | ``` 131 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metadrop/drupal-updater", 3 | "description": "Update drupal project dependencies", 4 | "type": "library", 5 | "bin": [ 6 | "drupal-updater" 7 | ], 8 | "require": { 9 | "symfony/console": ">=4", 10 | "symfony/process": ">=4", 11 | "drush/drush": ">=10", 12 | "davidrjonas/composer-lock-diff": "^1.7" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Omar", 17 | "email": "omar.lopesino@metadrop.net" 18 | } 19 | ], 20 | "minimum-stability": "dev", 21 | "autoload": { 22 | "psr-4": { 23 | "DrupalUpdater\\": "src/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /drupal-updater: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new UpdaterCommand()); 27 | $app->setDefaultCommand('update'); 28 | $app->run(); 29 | -------------------------------------------------------------------------------- /scripts/unsupported-modules.php: -------------------------------------------------------------------------------- 1 | moduleExists('update')) { 12 | throw new Exception("Unable to report unsupported modules as Update module is not enabled in the site."); 13 | } 14 | 15 | $updateManager = \Drupal::service('update.manager'); 16 | $updateManager->refreshUpdateData(); 17 | $projectData = $updateManager->getProjects(); 18 | 19 | $available_updates = update_get_available(TRUE); 20 | $project_data = update_calculate_project_data($available_updates); 21 | 22 | $projects = array_filter($project_data, function (array $project) { 23 | return $project['status'] === UpdateManagerInterface::NOT_SUPPORTED; 24 | }); 25 | 26 | if (!empty($projects)) { 27 | $projects_unsupported_data = array_map(function ($project) { 28 | 29 | // If the recommended version is the current one, then there aren't recommended 30 | // versions as the module is obsolete. 31 | $recommended = $project['recommended'] != $project['existing_version'] ? $project['recommended'] : 'None'; 32 | 33 | return [ 34 | 'project_name' => $project['name'], 35 | 'current_version' => $project['existing_version'], 36 | 'recommended_version' => $recommended, 37 | ]; 38 | }, $projects); 39 | 40 | } 41 | else { 42 | 43 | $projects_unsupported_data = []; 44 | } 45 | 46 | print json_encode($projects_unsupported_data); 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Config/Config.php: -------------------------------------------------------------------------------- 1 | author = $author; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getAuthor(): string { 67 | return $this->author; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function setEnvironments(array $environments) { 74 | $this->environments = $environments; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function getEnvironments(): array { 81 | return $this->environments; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function setOnlySecurities(bool $onlySecurities) { 88 | $this->onlySecurities = $onlySecurities; 89 | } 90 | 91 | /** 92 | * {@inheritdoc} 93 | */ 94 | public function onlyUpdateSecurities(): bool { 95 | return $this->onlySecurities; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function setNoDev(bool $noDev) { 102 | $this->noDev = $noDev; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function noDev(): bool { 109 | return $this->noDev; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function setPackages(array $packages) { 116 | $this->packages = $packages; 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | public function getPackages(): array { 123 | return $this->packages; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function setConsolidateConfiguration(bool $consolidate) { 130 | $this->consolidateConfiguration = $consolidate; 131 | } 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function getConsolidateConfiguration(): bool { 137 | return $this->consolidateConfiguration; 138 | } 139 | 140 | /** 141 | * Creates a configuration isntance given a YAML configuration file. 142 | * 143 | * @param string $config_file 144 | * Configuration file. 145 | * 146 | * @return self 147 | * Configuration ready to use. 148 | */ 149 | public static function createFromConfigurationFile(string $config_file) { 150 | 151 | $instance = new static(); 152 | $configuration = Yaml::parseFile($config_file); 153 | 154 | $string_fields = [ 155 | 'author', 156 | ]; 157 | 158 | foreach ($string_fields as $string_field) { 159 | if (isset($configuration[$string_field]) && !is_string($configuration[$string_field])) { 160 | throw new \InvalidArgumentException(sprintf('"%s" configuration key must be a string, %s given', $string_field, gettype($configuration['repository']))); 161 | } 162 | elseif (!empty($configuration[$string_field])) { 163 | $instance->{$string_field} = $configuration[$string_field]; 164 | } 165 | } 166 | 167 | $boolean_fields = [ 168 | 'onlySecurities', 169 | 'noDev', 170 | 'consolidateConfiguration', 171 | ]; 172 | 173 | foreach ($boolean_fields as $boolean_field) { 174 | if (isset($configuration[$boolean_field]) && !is_bool($configuration[$boolean_field])) { 175 | throw new \InvalidArgumentException(sprintf('"%s" config key must be a boolean, %s given!', $boolean_field, gettype($configuration[$boolean_field]))); 176 | } 177 | elseif (isset($configuration[$boolean_field])) { 178 | $instance->{$boolean_field} = $configuration[$boolean_field]; 179 | } 180 | } 181 | 182 | $array_fields = [ 183 | 'environments', 184 | 'packages', 185 | ]; 186 | 187 | foreach ($array_fields as $array_field) { 188 | if (isset($configuration[$array_field]) && !is_array($configuration[$array_field])) { 189 | throw new \InvalidArgumentException(sprintf('"%s" config key must be an array, %s given!', $array_field, gettype($configuration[$array_field]))); 190 | } 191 | elseif (!empty($configuration[$array_field])) { 192 | $instance->{$array_field} = $configuration[$array_field]; 193 | } 194 | } 195 | 196 | return $instance; 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/Config/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | '; 8 | 9 | /** 10 | * Get the commits author. 11 | * 12 | * @return string 13 | * Commits author. 14 | */ 15 | public function getAuthor() : string; 16 | 17 | /** 18 | * Sets the commits author. 19 | * 20 | * @param string $author 21 | * Commits author. 22 | */ 23 | public function setAuthor(string $author); 24 | 25 | /** 26 | * Sets the environment list. 27 | * 28 | * @param array $environments 29 | * Environemnts. 30 | */ 31 | public function getEnvironments() : array; 32 | 33 | /** 34 | * Gets the environment list. 35 | * 36 | * @return array|string[] 37 | * List of environments. 38 | */ 39 | public function setEnvironments(array $environments); 40 | 41 | /** 42 | * Indicates if only securities should be updated. 43 | * 44 | * @return bool 45 | * True when only securities will e updated. 46 | */ 47 | public function onlyUpdateSecurities() : bool; 48 | 49 | /** 50 | * Setup securities update. 51 | * 52 | * @param bool $onlySecurities 53 | * If true, only securities will be updated. 54 | */ 55 | public function setOnlySecurities(bool $only_update_securities); 56 | 57 | /** 58 | * Indicates whether not update dev packages. 59 | * 60 | * @return bool 61 | * True when dev packages must not be updated. 62 | */ 63 | public function noDev() : bool; 64 | 65 | /** 66 | * Setup to not update development packages. 67 | * 68 | * @param bool $noDev 69 | * If true, development packages won't be updated. 70 | */ 71 | public function setNoDev(bool $no_dev); 72 | 73 | /** 74 | * Gets the list of packages to update. 75 | * 76 | * @return array 77 | * List of packages to update. 78 | */ 79 | public function getPackages() : array; 80 | 81 | /** 82 | * Set the list of packages to update. 83 | * 84 | * @param array $packages 85 | * Packages list. 86 | */ 87 | public function setPackages(array $packages); 88 | 89 | /** 90 | * Set if configuration should be or not procesed. 91 | * 92 | * @param bool $consolidate 93 | * Set to true to import / export configuraton. 94 | */ 95 | public function setConsolidateConfiguration(bool $consolidate); 96 | 97 | /** 98 | * Get if configuration must be exported or imported. 99 | * 100 | * @return bool 101 | * TRUE when configuration must be managed. 102 | */ 103 | public function getConsolidateConfiguration(); 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/PostUpdate/PostUpdateDDQG.php: -------------------------------------------------------------------------------- 1 | isPackageUpdated($package, $composerLockDiff)) { 36 | return false; 37 | } 38 | 39 | [$oldVersion, $newVersion] = $this->getPackageUpdate($package, $composerLockDiff); 40 | 41 | // Find matching DDQG ignore entry and update if necessary 42 | $ignoreEntries = &$composerJson['config']['audit']['ignore']; 43 | 44 | if ($this->processDDQGEntry($ignoreEntries, $moduleName, $oldVersion, $newVersion, $output)) { 45 | $json = json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; 46 | file_put_contents('composer.json', $json); 47 | $this->runCommand('git add composer.json'); 48 | } 49 | 50 | } 51 | 52 | /** 53 | * Process a DDQG ignore entry for a module. 54 | * 55 | * @param array &$ignoreEntries 56 | * Reference to the ignore entries array. 57 | * @param string $moduleName 58 | * The module name without vendor prefix. 59 | * @param string $oldVersion 60 | * The old version of the module. 61 | * @param string $newVersion 62 | * The new version of the module. 63 | * @param \Symfony\Component\Console\Output\OutputInterface $output 64 | * The output interface for logging. 65 | * 66 | * @return bool 67 | * TRUE if the entry was modified, FALSE otherwise. 68 | */ 69 | protected function processDDQGEntry(array &$ignoreEntries, string $moduleName, string $oldVersion, string $newVersion, OutputInterface $output): bool { 70 | foreach ($ignoreEntries as $key => $message) { 71 | // Match pattern: DDQG-unsupported-drupal-{module_name}-{version}. 72 | if (preg_match('/^DDQG-unsupported-drupal-' . preg_quote($moduleName) . '-(.+)$/', $key, $matches)) { 73 | $oldReleaseType = $this->getReleaseType($oldVersion); 74 | $newReleaseType = $this->getReleaseType($newVersion); 75 | 76 | // Let composer audit the module if it has changed to abandoned. 77 | if ($this->isPackageAbandoned($newVersion) && !$this->isPackageAbandoned($oldVersion)) { 78 | unset($ignoreEntries[$key]); 79 | $output->writeln("Removed DDQG ignore entry for $moduleName (now abandoned)"); 80 | return true; 81 | } 82 | 83 | // Do not ignore it anymore as its stable. 84 | if ($newReleaseType === 'stable' && $oldReleaseType !== 'stable') { 85 | unset($ignoreEntries[$key]); 86 | $output->writeln("Removed DDQG ignore entry for $moduleName (now stable)"); 87 | return true; 88 | } 89 | 90 | // It is still not stable / it hasn't changed its version. 91 | $newKey = 'DDQG-unsupported-drupal-' . $moduleName . '-' . $this->formatVersionForDDQG($newVersion); 92 | unset($ignoreEntries[$key]); 93 | $ignoreEntries[$newKey] = $message; 94 | $output->writeln("Updated DDQG ignore entry for $moduleName: $oldVersion -> $newVersion"); 95 | return true; 96 | } 97 | } 98 | 99 | return false; 100 | } 101 | 102 | /** 103 | * Determines the release type from a version string. 104 | * 105 | * @param string $version 106 | * The version string (e.g., "1.0.0", "2.0.0-alpha3", "3.0.0-RC2"). 107 | * 108 | * @return string 109 | * The release type: 'stable', 'alpha', 'beta', or 'rc'. 110 | */ 111 | protected function getReleaseType(string $version): string { 112 | if (str_contains($version, '-alpha')) { 113 | return 'alpha'; 114 | } 115 | elseif (str_contains($version, '-beta')) { 116 | return 'beta'; 117 | } 118 | elseif (str_contains(strtolower($version), '-rc')) { 119 | return 'rc'; 120 | } 121 | return 'stable'; 122 | } 123 | 124 | /** 125 | * Checks if a package version indicates it's abandoned. 126 | * 127 | * @param string $version 128 | * The version string. 129 | * 130 | * @return bool 131 | * TRUE if the package is abandoned, FALSE otherwise. 132 | */ 133 | protected function isPackageAbandoned(string $version): bool { 134 | return str_contains(strtolower($version), 'abandoned'); 135 | } 136 | 137 | /** 138 | * Formats a version string for DDQG ignore entry. 139 | * 140 | * @param string $version 141 | * The version string (e.g., "2.0.0-alpha3"). 142 | * 143 | * @return string 144 | * The formatted version (e.g., "2.0.0.0-alpha3"). 145 | */ 146 | protected function formatVersionForDDQG(string $version): string { 147 | // DDQG format appears to use x.x.x.x format 148 | // If version has 3 parts (x.x.x), add a .0. 149 | $parts = explode('-', $version); 150 | $versionNumber = $parts[0]; 151 | $suffix = isset($parts[1]) ? '-' . $parts[1] : ''; 152 | 153 | $versionParts = explode('.', $versionNumber); 154 | if (count($versionParts) === 3) { 155 | $versionNumber .= '.0'; 156 | } 157 | 158 | return $versionNumber . $suffix; 159 | } 160 | 161 | /** 162 | * Check if package has been updated. 163 | * 164 | * @param string $packageName 165 | * Package name. 166 | * @param array $composerLockDiff 167 | * Composer lock diff. 168 | * 169 | * @return bool 170 | * TRUE when the package is updated. 171 | */ 172 | protected function isPackageUpdated(string $packageName, array $composerLockDiff): bool { 173 | return !empty($this->getPackageUpdate($packageName, $composerLockDiff)); 174 | } 175 | 176 | /** 177 | * Gets package update information. 178 | * 179 | * @param string $packageName 180 | * Package name. 181 | * @param array $composerLockDiff 182 | * Composer lock diff. 183 | * 184 | * @return array 185 | * Data indicating what has been updated. 186 | */ 187 | protected function getPackageUpdate(string $packageName, array $composerLockDiff): array { 188 | return $composerLockDiff['changes'][$packageName] ?? $composerLockDiff['changes-dev'][$packageName] ?? []; 189 | } 190 | 191 | /** 192 | * Runs a shell command. 193 | * 194 | * @param string $command 195 | * Command. 196 | * 197 | * @throws \RuntimeException 198 | * When the command fails. 199 | */ 200 | protected function runCommand(string $command): void { 201 | $process = Process::fromShellCommandline($command); 202 | $process->setTimeout(300); 203 | $process->run(); 204 | if (!$process->isSuccessful()) { 205 | throw new \RuntimeException(sprintf('Error running "%s" command: %s', $command, $process->getErrorOutput())); 206 | } 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/PostUpdate/PostUpdateInterface.php: -------------------------------------------------------------------------------- 1 | postUpdateProcessors[] = new PostUpdateDDQG(); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | protected function configure() { 69 | $this->setHelp('Update composer packages. 70 | 71 | Update includes: 72 | - Commit current configuration not exported (Drupal +8). 73 | - Identify updatable composer packages (outdated) 74 | - For each package try to update and commit it (recovers previous state if fails)'); 75 | $this->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Configuration file', '.drupal-updater.yml'); 76 | $this->addOption('environments', 'envs', InputOption::VALUE_OPTIONAL, 'List of drush aliases that are needed to update'); 77 | $this->addOption('author', 'a', InputOption::VALUE_OPTIONAL, 'Git author'); 78 | $this->addOption('security', 's', InputOption::VALUE_NEGATABLE, 'Choose to update only security packages'); 79 | $this->addOption('dev', 'nd', InputOption::VALUE_NEGATABLE, 'Choose to update dev requirements.'); 80 | $this->addOption('packages', 'pl', InputOption::VALUE_OPTIONAL, 'Comma separated list of packages to update'); 81 | $this->addOption('consolidate-configuration', 'cc', InputOption::VALUE_NEGATABLE, 'If false, configuration will not be consolidated.'); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | protected function initialize(InputInterface $input, OutputInterface $output) { 88 | $this->output = $output; 89 | $this->setupConfig($input->getOption('config')); 90 | $this->mapInputToConfiguration($input); 91 | $this->logConfiguration(); 92 | } 93 | 94 | /** 95 | * Maps all the input data provided to the configuration. 96 | * 97 | * Anything passed to input can override configuration. 98 | * 99 | * @param InputInterface $input 100 | * Input. 101 | */ 102 | protected function mapInputToConfiguration(InputInterface $input) { 103 | if (!empty($input->getOption('environments'))) { 104 | $this->getConfiguration()->setEnvironments(explode(',', $input->getOption('environments'))); 105 | } 106 | 107 | if (!empty($input->getOption('author'))) { 108 | $this->getConfiguration()->setAuthor($input->getOption('author')); 109 | } 110 | 111 | if (!empty($input->getOption('security'))) { 112 | $this->getConfiguration()->setOnlySecurities(true); 113 | } 114 | elseif (!empty($input->getOption('no-security'))) { 115 | $this->getConfiguration()->setOnlySecurities(false); 116 | } 117 | 118 | if (!empty($input->getOption('dev'))) { 119 | $this->getConfiguration()->setNoDev(false); 120 | } 121 | elseif (!empty($input->getOption('no-dev'))) { 122 | $this->getConfiguration()->setNoDev(true); 123 | } 124 | 125 | if (!empty($input->getOption('consolidate-configuration'))) { 126 | $this->getConfiguration()->setConsolidateConfiguration(true); 127 | } 128 | elseif (!empty($input->getOption('no-consolidate-configuration'))) { 129 | $this->getConfiguration()->setConsolidateConfiguration(false); 130 | } 131 | 132 | $packages_to_update_parameter = $input->getOption('packages') ?? ''; 133 | if (!empty($packages_to_update_parameter)) { 134 | $this->getConfiguration()->setPackages(explode(',', filter_var($packages_to_update_parameter, FILTER_SANITIZE_ADD_SLASHES))); 135 | $this->showFullReport = FALSE; 136 | } 137 | 138 | if (!empty($this->getConfiguration()->getPackages())) { 139 | $this->packagesToUpdate = $this->getConfiguration()->getPackages(); 140 | } 141 | } 142 | 143 | /** 144 | * Show users what config will be applied to the current update. 145 | */ 146 | protected function logConfiguration() { 147 | $this->printHeader1('SETUP'); 148 | $this->output->writeln('Drupal web root found at ' . $this->findDrupalWebRoot()); 149 | $this->output->writeln(sprintf('Environments: %s', implode(', ', $this->getConfiguration()->getEnvironments()))); 150 | $this->output->writeln(sprintf('GIT author will be overriden with: %s', $this->getConfiguration()->getAuthor())); 151 | 152 | if ($this->getConfiguration()->onlyUpdateSecurities()) { 153 | $this->output->writeln('Only security updates will be done'); 154 | } 155 | 156 | if ($this->getConfiguration()->noDev()) { 157 | $this->output->writeln("Dev packages won't be updated"); 158 | } 159 | 160 | if (!empty($this->getConfiguration()->getPackages())) { 161 | $this->output->writeln(sprintf('Selected packages: %s', implode(', ', $this->getConfiguration()->getPackages()))); 162 | } 163 | 164 | $this->output->writeln(''); 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | protected function execute(InputInterface $input, OutputInterface $output) : int { 171 | $this->runCommand('cp composer.lock composer.drupalupdater.lock'); 172 | $this->printSummary(); 173 | $this->printHeader1('1. Consolidating configuration'); 174 | 175 | if ($this->config->getConsolidateConfiguration()) { 176 | $this->consolidateConfiguration(); 177 | } 178 | else { 179 | $this->output->writeln('Configuration consolidation skipped'); 180 | $this->output->writeln(''); 181 | } 182 | 183 | $this->printHeader1('2. Checking packages'); 184 | 185 | if (!isset($this->packagesToUpdate) || empty($this->packagesToUpdate)) { 186 | $this->checkPackages(); 187 | } 188 | else { 189 | $this->output->writeln(sprintf('Packages to update:')); 190 | $this->output->writeln(implode("\n", $this->packagesToUpdate)); 191 | } 192 | $this->output->writeln(''); 193 | $this->printHeader1('3. Updating packages'); 194 | $this->updatePackages($this->packagesToUpdate); 195 | $this->printHeader1('4. Report'); 196 | $this->showUpdatedPackages(); 197 | 198 | if ($this->showFullReport) { 199 | $this->showPendingUpdates(); 200 | $this->showObsoleteDrupalModules(); 201 | } 202 | 203 | $this->cleanup(); 204 | 205 | return 0; 206 | } 207 | 208 | protected function setupConfig(string $configuration_filepath) { 209 | $this->output->writeln(sprintf('Selected configuration file: %s', $configuration_filepath)); 210 | 211 | if (file_exists($configuration_filepath)) { 212 | $this->output->writeln(sprintf('Configuration file found at %s', $configuration_filepath)); 213 | $config = Config::createFromConfigurationFile($configuration_filepath); 214 | } 215 | else { 216 | $this->output->writeln(sprintf('No configuration file found at %s. Using command line parameters.', $configuration_filepath)); 217 | $config = new Config(); 218 | } 219 | 220 | $this->setConfiguration($config); 221 | } 222 | 223 | protected function setConfiguration(ConfigInterface $config) { 224 | $this->config = $config; 225 | } 226 | 227 | protected function getConfiguration() { 228 | return $this->config; 229 | } 230 | 231 | /** 232 | * Run a drush command. 233 | * 234 | * @param string $command 235 | * Command to execute. 236 | * @param array $environments 237 | * Environments where the command needs to be executed. 238 | * If empty, it will be executed in the environments passed to the command. 239 | */ 240 | protected function runDrushCommand(string $command, array $environments = []) { 241 | if (empty($environments)) { 242 | $environments = $this->getConfiguration()->getEnvironments(); 243 | } 244 | 245 | foreach ($environments as $environment) { 246 | $this->output->writeln(sprintf("Running drush %s on the \"%s\" environment.", $command, $environment)); 247 | $this->runCommand(sprintf('drush %s %s', $environment, $command)); 248 | } 249 | } 250 | 251 | /** 252 | * Runs a shell command. 253 | * 254 | * @param string $command 255 | * Command. 256 | * 257 | * @return Process 258 | * It can be used to obtain the command output if needed. 259 | * 260 | * @throws \RuntimeException 261 | * When the command fails. 262 | */ 263 | protected function runCommand(string $command) { 264 | $process = Process::fromShellCommandline($command); 265 | $process->setTimeout(300); 266 | $process->run(); 267 | if (!$process->isSuccessful()) { 268 | throw new \RuntimeException(sprintf('Error running "%s" command: %s', $command, $process->getErrorOutput())); 269 | } 270 | return $process; 271 | } 272 | 273 | /** 274 | * Run a composer command. 275 | * 276 | * @param string $command 277 | * Composer command. 278 | * @param array $parameters 279 | * List of parameters the command needs. 280 | * 281 | * @return Process 282 | * Process result. 283 | */ 284 | protected function runComposer(string $command, array $parameters) { 285 | return $this->runCommand(sprintf('composer %s %s', $command, implode(' ', $parameters))); 286 | } 287 | 288 | /** 289 | * Get the no dev parameter. 290 | * 291 | * No dev parameter is only added if the --no-dev 292 | * argument is passed to the command. 293 | * 294 | * @return string 295 | */ 296 | protected function getNoDevParameter(){ 297 | return $this->getConfiguration()->noDev() ? '--no-dev' : ''; 298 | } 299 | 300 | /** 301 | * Prints a summary listing what will be done in the script. 302 | */ 303 | protected function printSummary() { 304 | $this->printHeader1('Summary'); 305 | $this->output->writeln('1. Consolidating configuration'); 306 | $this->output->writeln('2. Checking packages'); 307 | $this->output->writeln('3. Updating packages'); 308 | $this->output->writeln('4. Report'); 309 | $this->output->writeln(''); 310 | } 311 | 312 | /** 313 | * Consolidate configuration for all the environments. 314 | * 315 | * All the configuration that is changed is commited, 316 | * doing one commit per environment. This implies that 317 | * configuration must be consistent before running the command. 318 | */ 319 | protected function consolidateConfiguration() { 320 | $this->runDrushCommand('cr'); 321 | $this->runDrushCommand('cim -y'); 322 | $this->output->writeln(''); 323 | $this->output->writeln(''); 324 | 325 | foreach ($this->getConfiguration()->getEnvironments() as $environment) { 326 | $this->output->writeln(sprintf('Consolidating %s environment', $environment)); 327 | $this->runDrushCommand('cex -y', [$environment]); 328 | 329 | $changes = trim($this->runCommand('git status config -s')->getOutput()); 330 | if (!empty($changes)) { 331 | $this->output->writeln("\nChanges done:\n"); 332 | $git_status_output = trim($this->runCommand('git status config')->getOutput()); 333 | $this->output->writeln("$git_status_output\n"); 334 | } 335 | 336 | $this->runCommand(sprintf( 337 | 'git add config && git commit -m "CONFIG - Consolidate current configuration on %s" --author="%s" -n || echo "No changes to commit"', 338 | $environment, 339 | $this->getConfiguration()->getAuthor(), 340 | )); 341 | $this->output->writeln(''); 342 | } 343 | 344 | $this->runDrushCommand('cr'); 345 | $this->runDrushCommand('cim -y'); 346 | $this->output->writeln(''); 347 | } 348 | 349 | /** 350 | * Check the packages that needs update. 351 | * 352 | * By default, all direct packages will be updated. 353 | * If security parameter is set, only security packages 354 | * will be updated. 355 | */ 356 | protected function checkPackages() { 357 | if ($this->getConfiguration()->onlyUpdateSecurities()) { 358 | $package_list = $this 359 | ->runCommand(sprintf('composer audit --locked %s --format plain 2>&1 | grep ^Package | cut -f2 -d: | sort -u', $this->getNoDevParameter())) 360 | ->getOutput(); 361 | } 362 | else { 363 | $package_list = $this 364 | ->runCommand(sprintf('composer show --locked --outdated --name-only %s 2>/dev/null', $this->getNoDevParameter())) 365 | ->getOutput(); 366 | } 367 | $package_list_massaged = $this->massagePackageList($package_list); 368 | $this->packagesToUpdate = $this->findDirectPackagesFromList($package_list_massaged); 369 | 370 | $this->output->writeln(implode("\n", $this->packagesToUpdate)); 371 | } 372 | 373 | /** 374 | * Given a list of packages , find its direct packages. 375 | * 376 | * @see UpdaterCommand::findDirectPackage() 377 | * 378 | * @param array $packages_list 379 | * Packages list. 380 | * @return array 381 | * List of direct packages. 382 | */ 383 | protected function findDirectPackagesFromList(array $packages_list) { 384 | if (empty($packages_list)) { 385 | return []; 386 | } 387 | 388 | $direct_packages = $this->massagePackageList($this 389 | ->runCommand(sprintf('composer show --locked --direct --name-only %s 2>/dev/null', $this->getNoDevParameter())) 390 | ->getOutput()); 391 | 392 | $direct_packages_found = array_intersect($packages_list, $direct_packages); 393 | 394 | $not_direct_packages = array_diff($packages_list, $direct_packages); 395 | 396 | foreach ($not_direct_packages as $package) { 397 | $direct_packages_found[] = $this->findDirectPackage($package, $direct_packages); 398 | } 399 | 400 | return array_unique($direct_packages_found); 401 | } 402 | 403 | /** 404 | * Find the direct package for a specific not direct dependency. 405 | * 406 | * If no direct package is found, consider the self package as direct. It is a extreme use 407 | * case where a too deep dependency is not found. For module convenience, it is needed to consider 408 | * package as direct so it can be updated. 409 | * 410 | * @param string $package 411 | * Package. 412 | * @param array $direct_packages 413 | * List of direct packages. 414 | * 415 | * @return string 416 | * The direct package. 417 | */ 418 | protected function findDirectPackage(string $package, array $direct_packages) { 419 | $composer_why_recursive_timeout = 2; 420 | $commands = [ 421 | sprintf("composer why %s --locked | awk '{print $1}'", $package), 422 | sprintf("timeout %s composer why %s --locked -r | awk '{print $1}'", $composer_why_recursive_timeout, $package), 423 | ]; 424 | 425 | foreach ($commands as $command) { 426 | $direct_package = $this->findPackageInPackageListCommand($command, $direct_packages); 427 | if (!empty($direct_package)) { 428 | return $direct_package; 429 | } 430 | } 431 | 432 | return $package; 433 | } 434 | 435 | /** 436 | * Finds a package from a command that is present in a package list command. 437 | * 438 | * This is used to get direct apckages from not direct packages. 439 | * 440 | * @see UpdaterCommand::findDirectPackage() 441 | * 442 | * @param string $command 443 | * List that return packages list. 444 | * @param array $package_list 445 | * List of packages we want to look for. 446 | * 447 | * @return string|null 448 | * FIrst package from command that is present in package list. 449 | */ 450 | protected function findPackageInPackageListCommand(string $command, array $package_list) { 451 | $package_list_output = array_filter(explode("\n", (string) trim($this->runCommand($command) 452 | ->getOutput()))); 453 | foreach ($package_list_output as $package) { 454 | if (in_array($package, $package_list)) { 455 | return $package; 456 | } 457 | } 458 | 459 | return null; 460 | } 461 | 462 | /** 463 | * Masssages a list of packages. 464 | * 465 | * It converts a package list bash output into a package list array, 466 | * removing any element that is not a package and removing spaces. 467 | * 468 | * @param string $package_list 469 | * List of packages coming from a bash command (s.e.: composer show --names-only). 470 | * 471 | * @return array 472 | * List of packages. Example: 473 | * - metadrop/drupal-updater 474 | * - metadrop/drupal-artifact-builder 475 | */ 476 | protected function massagePackageList(string $package_list) { 477 | $package_list = explode("\n", $package_list); 478 | $package_list = array_map(function ($package) { 479 | return trim($package); 480 | }, $package_list); 481 | return array_filter($package_list, function ($package) { 482 | return preg_match('/^([A-Za-z0-9_-]*\/[A-Za-z0-9_-]*)/', $package); 483 | }); 484 | } 485 | 486 | /** 487 | * Updates the packages. 488 | * 489 | * @param array $package_list 490 | * List of packages to update. 491 | */ 492 | protected function updatePackages(array $package_list) { 493 | foreach ($package_list as $package) { 494 | $this->updatePackage($package); 495 | } 496 | } 497 | 498 | /** 499 | * Gets the list of outdated packages. 500 | * 501 | * It calculates the outdated packages only the first time. 502 | * 503 | * @return array 504 | * List of all outdated packages. 505 | */ 506 | protected function getAllOutdatedPackages() { 507 | if (empty($this->outdatedPackages)) { 508 | $this->outdatedPackages = json_decode($this->runCommand('composer show --locked --outdated --format json')->getOutput())->locked; 509 | } 510 | return $this->outdatedPackages; 511 | } 512 | 513 | /** 514 | * Get an available update of a specific module. 515 | * 516 | * @param string $package_name 517 | * Package name. 518 | * 519 | * @return object|null 520 | * Available update information for the specific package. 521 | */ 522 | protected function getAvailableUpdate(string $package_name) { 523 | $outdated_packages = $this->getAllOutdatedPackages(); 524 | foreach ($outdated_packages as $package) { 525 | if ($package->name == $package_name && $package->version != $package->latest) { 526 | return $package; 527 | } 528 | } 529 | return NULL; 530 | } 531 | 532 | /** 533 | * Updates a specific package. 534 | * 535 | * After the command, all the modified files will be commited. 536 | * 537 | * When the package is a drupal module, the updates will be applied 538 | * and the configuration will be exported and commited. 539 | * 540 | * @param string $package 541 | * PAckage to update. 542 | */ 543 | protected function updatePackage(string $package) { 544 | $this->printHeader2(sprintf('Updating: %s', $package)); 545 | try { 546 | $result = $this->runComposer('update', [$package, '--with-dependencies']); 547 | } 548 | catch (\Exception $e) { 549 | $this->handlePackageUpdateErrors($e); 550 | return; 551 | } 552 | 553 | $composer_lock_is_changed = (int) $this->runCommand('git status --porcelain composer.lock | wc -l')->getOutput() > 0; 554 | 555 | $available_update = $this->getAvailableUpdate($package); 556 | if (!empty($available_update) && !empty($available_update->latest) && !$composer_lock_is_changed) { 557 | $this->output->writeln(sprintf("Package %s has an update available to %s version. Due to composer.json constraints, it hasn't been updated.\n", $package, $available_update->latest)); 558 | 559 | $error_output = trim($result->getOutput()); 560 | $valid_errors = [ 561 | 'but it conflicts with your root composer.json require', 562 | 'Your requirements could not be resolved to an installable set of packages.', 563 | ]; 564 | 565 | foreach ($valid_errors as $error) { 566 | if (str_contains($error_output, $error)) { 567 | $this->output->writeln("\n$error_output"); 568 | } 569 | } 570 | 571 | } 572 | 573 | if (!$composer_lock_is_changed) { 574 | if (empty($available_update)) { 575 | $this->output->writeln(sprintf("There aren't available updates for %s package.\n", $package)); 576 | } 577 | return; 578 | } 579 | 580 | $this->runCommand('git add composer.json composer.lock'); 581 | 582 | if ($this->isDrupalExtension($package)) { 583 | try { 584 | $this->runCommand(sprintf('git add %s', $this->findDrupalWebRoot())); 585 | $this->runDrushCommand('cr'); 586 | $this->runDrushCommand('updb -y'); 587 | if ($this->config->getConsolidateConfiguration()) { 588 | $this->runDrushCommand('cex -y'); 589 | $this->output->writeln(''); 590 | $this->runCommand('git add config'); 591 | } 592 | } 593 | catch (\Exception $e) { 594 | $this->handlePackageUpdateErrors($e); 595 | return; 596 | } 597 | 598 | } 599 | 600 | $composerLockDiff = $this->getComposerLockDiffJsonDecoded(); 601 | foreach ($this->postUpdateProcessors as $processor) { 602 | $processor->execute($package, $composerLockDiff, $this->output); 603 | } 604 | 605 | $updated_packages = trim($this->runCommand('composer-lock-diff')->getOutput()); 606 | if (!empty($updated_packages)) { 607 | $this->output->writeln("Updated packages:"); 608 | $this->output->writeln("$updated_packages\n"); 609 | } 610 | 611 | $commit_message = $this->calculateModuleUpdateCommitMessage($package); 612 | 613 | $this->runCommand(sprintf('git commit -m "%s" -m "%s" --author="%s" -n', $commit_message, $updated_packages, $this->getConfiguration()->getAuthor())); 614 | 615 | } 616 | 617 | /** 618 | * Finds the Drupal root. 619 | * 620 | * Drupal root standard is web, but there are other projects where the path is docroot, 621 | * and another complex structures that requires changing the root folder. 622 | * 623 | * @return string|void 624 | * Drupal root. 625 | */ 626 | protected function findDrupalWebRoot() { 627 | $core = InstalledVersions::getInstallPath('drupal/core') . '/../'; 628 | 629 | if (!empty($core)) { 630 | return realpath($core); 631 | } 632 | 633 | foreach (['web', 'docroot', 'public_html'] as $folder) { 634 | if (!is_link($folder) && is_dir($folder)) { 635 | return $folder; 636 | } 637 | } 638 | } 639 | 640 | /** 641 | * Calculate the commit message. 642 | * 643 | * Commit message is different depending on what have changed 644 | * so that at a first glance developers may know what 645 | * happened to the module. 646 | * 647 | * Changes can be: 648 | * - Package. 649 | * - Dependencies. 650 | * - Configuration. 651 | * 652 | * @param string $package 653 | * Package name. 654 | * 655 | * @return string 656 | * Format: UPDATE - : (package)(, dependencies)(, configuration changes). 657 | */ 658 | protected function calculateModuleUpdateCommitMessage(string $package) { 659 | 660 | $changes = []; 661 | $composer_lock_diff = $this->getComposerLockDiffJsonDecoded(); 662 | if ($this->isPackageUpdated($package, $composer_lock_diff)) { 663 | [$package_update_from, $package_update_to] = $this->getPackageUpdate($package, $composer_lock_diff); 664 | $changes[] = sprintf('package (%s -> %s)', $package_update_from, $package_update_to); 665 | } 666 | 667 | if ($this->areDependenciesUpdated($package, $composer_lock_diff)) { 668 | $changes[] = 'dependencies'; 669 | } 670 | 671 | if ($this->isConfigurationChanged()) { 672 | $changes[] = 'configuration changes'; 673 | } 674 | 675 | if (empty($changes)) { 676 | $changes[] = 'other'; 677 | } 678 | 679 | return sprintf('UPDATE - %s: %s', $package, implode(', ', $changes)); 680 | } 681 | 682 | /** 683 | * Gets package update information. 684 | * 685 | * @param string $package_name 686 | * Package name. 687 | * @param array $composer_lock_diff 688 | * Composer lock diff. 689 | * 690 | * @return array 691 | * Data indicating what has been updated. 692 | */ 693 | protected function getPackageUpdate(string $package_name, array $composer_lock_diff) { 694 | return $composer_lock_diff['changes'][$package_name] ?? $composer_lock_diff['changes-dev'][$package_name] ?? []; 695 | } 696 | 697 | /** 698 | * Check package has been updated. 699 | * 700 | * @param string $package_name 701 | * Package name. 702 | * @param array $composer_lock_diff 703 | * Composer lock diff. 704 | * 705 | * @return bool 706 | * TRUE when the package is updated. 707 | */ 708 | protected function isPackageUpdated(string $package_name, array $composer_lock_diff) { 709 | return !empty($this->getPackageUpdate($package_name, $composer_lock_diff)); 710 | } 711 | 712 | /** 713 | * Check package dependencies has been updated. 714 | * 715 | * @param string $package_name 716 | * Package name. 717 | * @param array $composer_lock_diff 718 | * Composer lock diff. 719 | * 720 | * @return bool 721 | * TRUE when any dependency that isn't the package has changed. 722 | */ 723 | protected function areDependenciesUpdated(string $package_name, array $composer_lock_diff) { 724 | if (isset($composer_lock_diff['changes'][$package_name])) { 725 | unset($composer_lock_diff['changes'][$package_name]); 726 | } 727 | 728 | if (isset($composer_lock_diff['changes-dev'][$package_name])) { 729 | unset($composer_lock_diff['changes-dev'][$package_name]); 730 | } 731 | return !empty($composer_lock_diff['changes']) || !empty($composer_lock_diff['changes-dev']); 732 | } 733 | 734 | /** 735 | * Checks that the configuration has changed. 736 | * 737 | * @return bool 738 | * TRUE when the configuration has changed. 739 | */ 740 | protected function isConfigurationChanged() { 741 | return ((int) trim($this->runCommand('git status -s config | wc -l')->getOutput())) > 0; 742 | } 743 | 744 | /** 745 | * Get composer lock diff decoded. 746 | * 747 | * @return array 748 | * Associative composer lock diff. 749 | */ 750 | protected function getComposerLockDiffJsonDecoded() { 751 | return json_decode( 752 | trim($this->runCommand('composer-lock-diff --json')->getOutput()), 753 | TRUE, 754 | ); 755 | } 756 | 757 | /** 758 | * Handle errors produced in a update. 759 | * 760 | * There are errors either in composer update or drush updb, in those 761 | * case all the possible changes are reverted and the error message is shown. 762 | * 763 | * @param \Exception $e 764 | * Exception. 765 | */ 766 | protected function handlePackageUpdateErrors(\Exception $e) { 767 | $this->output->writeln("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); 768 | $error_ouput = $e instanceof ProcessFailedException ? $e->getProcess()->getErrorOutput() : $e->getMessage(); 769 | $this->output->writeln($error_ouput); 770 | $this->output->writeln('Updating package FAILED: recovering previous state.'); 771 | $this->output->writeln('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); 772 | $this->runCommand('git checkout composer.json composer.lock'); 773 | } 774 | 775 | /** 776 | * Shows all the pending updates 777 | */ 778 | protected function showPendingUpdates() { 779 | 780 | if ($this->getConfiguration()->onlyUpdateSecurities()) { 781 | $this->printHeader2('Not Updated Securities:'); 782 | $this->output->writeln( 783 | $this->runCommand('composer audit --locked --format plain 2>&1 | grep ^Package | cut -f2 -d: | sort -u')->getOutput(), 784 | ); 785 | 786 | } 787 | else { 788 | $this->printHeader2('Not Updated Packages (Direct):'); 789 | $this->output->writeln( 790 | $this->runCommand('composer show --locked --outdated --direct')->getOutput() 791 | ); 792 | 793 | $this->output->writeln(''); 794 | $this->printHeader2('Not Updated Packages (ALL):'); 795 | $this->output->writeln( 796 | $this->runCommand('composer show --locked --outdated')->getOutput() 797 | ); 798 | 799 | $this->output->writeln(''); 800 | $this->printHeader2('Not Updated Securities (ALL):'); 801 | $this->output->writeln( 802 | trim($this->runCommand('composer audit --locked --format plain 2>&1 | grep ^Package | cut -f2 -d: | sort -u')->getOutput()) 803 | ); 804 | $this->output->writeln(""); 805 | } 806 | } 807 | 808 | /** 809 | * Show updated packages. 810 | */ 811 | protected function showUpdatedPackages() { 812 | $updated_packages = $this->runCommand('composer-lock-diff --from composer.drupalupdater.lock --to composer.lock')->getOutput(); 813 | if (!empty($updated_packages)) { 814 | $this->output->writeln( 815 | trim($updated_packages), 816 | ); 817 | } 818 | else { 819 | $this->output->writeln("No packages have been updated\n"); 820 | } 821 | } 822 | 823 | /** 824 | * Shows all the drupal modules that are obsolete. 825 | */ 826 | protected function showObsoleteDrupalModules() { 827 | $this->printHeader2('Unsupported Drupal modules:'); 828 | 829 | $unsupported_modules_list = []; 830 | foreach ($this->getConfiguration()->getEnvironments() as $environment) { 831 | try { 832 | $unsupported_modules = json_decode(trim($this 833 | ->runCommand(sprintf('drush %s php-script %s/../scripts/unsupported-modules.php', $environment, __DIR__)) 834 | ->getOutput())); 835 | foreach ($unsupported_modules as $unsupported_module) { 836 | $unsupported_module = (array) $unsupported_module; 837 | if (!isset($unsupported_modules_list[$unsupported_module['project_name']])) { 838 | $unsupported_modules_list[$unsupported_module['project_name']] = $unsupported_module; 839 | } 840 | $unsupported_modules_list[$unsupported_module['project_name']]['environments'][] = $environment; 841 | } 842 | } 843 | catch (\RuntimeException $exception) { 844 | $this->output->writeln(''); 845 | $this->output->write($exception->getMessage()); 846 | } 847 | } 848 | 849 | $unsupported_modules_list = array_values(array_map (function ($unsupported_module) { 850 | $unsupported_module['environments'] = implode("\n", $unsupported_module['environments']); 851 | return array_values($unsupported_module); 852 | }, $unsupported_modules_list)); 853 | 854 | if (!empty($unsupported_modules_list)) { 855 | $unsupported_modules_list_table_rows = []; 856 | foreach ($unsupported_modules_list as $unsupported_module_info) { 857 | $unsupported_modules_list_table_rows[] = $unsupported_module_info; 858 | $unsupported_modules_list_table_rows[] = new TableSeparator(); 859 | } 860 | $fixed_drupal_advisories_table = new Table($this->output); 861 | $fixed_drupal_advisories_table->setHeaders(['Module', 'Current version', 'Recommended version', 'Environment(s)']); 862 | 863 | array_pop($unsupported_modules_list_table_rows); 864 | $fixed_drupal_advisories_table->setRows($unsupported_modules_list_table_rows); 865 | $fixed_drupal_advisories_table->render(); 866 | } 867 | else { 868 | $this->output->writeln('No obsolete modules have been found. Perhaps Update module is not installed?'); 869 | } 870 | } 871 | 872 | /** 873 | * Cleanup the residual files. 874 | */ 875 | protected function cleanup() { 876 | $this->runCommand('rm composer.drupalupdater.lock'); 877 | } 878 | 879 | /** 880 | * Checks that the package is a drupal extension. 881 | * 882 | * By drupal extension we mean: 883 | * - Module 884 | * - Theme 885 | * - Library 886 | * - Drush command package. 887 | * 888 | * @param string $package 889 | * Package. 890 | * 891 | * @return bool 892 | * TRUE when the package is a drupal extension. 893 | */ 894 | protected function isDrupalExtension(string $package) { 895 | $package_type = $this->runCommand(sprintf("composer show %s | grep ^type | awk '{print $3}'", $package))->getOutput(); 896 | return $package_type != 'drupal-library' && str_starts_with($package_type, 'drupal'); 897 | } 898 | 899 | /** 900 | * Print a primary header. 901 | * 902 | * @param string $text 903 | * Header text. 904 | */ 905 | protected function printHeader1(string $text) { 906 | $this->output->writeln(sprintf("// %s //\n", strtoupper($text))); 907 | } 908 | 909 | /** 910 | * Prints a secondary header. 911 | * 912 | * @param string $text 913 | * Header text. 914 | */ 915 | protected function printHeader2(string $text) { 916 | $this->output->writeln(sprintf("/// %s ///\n", $text)); 917 | } 918 | 919 | 920 | } 921 | --------------------------------------------------------------------------------