Drupal 8 patch workflow for files in docroot
When we deploy sites we use composer to build a code artefact we can push to the site. The build process is done with either with Jenkins or Acquia pipelines. Basically they run a "composer install" just like we would do on our local development machines.
In almost all Drupal 8 projects we need to change some of the files in the docroot folder, typically setting up redirects in the .htaccess file. We can accomplish this in different ways:
- Add the .htaccess file to git and tell composer not to modify it
- Copy the modified files as part of the deployment script
- Patch core with the necessary changes just like any other module
All three solutions has problems:
The first method will fail as the relevant composer plugins aren't compatible. The most widely used composer plugin used to tell composer not to overwrite files seems to be "derhasi/composer-preserve-paths" (1). This plugin is not compatible with the way the "cweagans/composer-patches" (2) works. There is a ticket in their issue-queue (3) and there might come an solution at some point.
The second method could work, but there are some drawbacks. We would have to maintain 3 ways of doing this as we use Phing scripts with Jenkins, YAML-based config to Acquia Pipelines (or a cloud hook script) and shell scripts for local development.
The third method fails as the patch plugin can't patch files outside of the individual package installation root. It is something that might come in version 2 of the composer-patches plugin.
Workaround
My solution to this problem was a small script (se below) added to the "post-install-cmd" and "post-update-cmd" scripts in the composer.json file. The script applies the patch configured as the "root-patch" in the "extra" section of the composer.json file.
With this we can use the same method for modifying the .htaccess and other files in the docroot folder across the different build platforms.
References
- https://github.com/derhasi/composer-preserve-paths
- https://github.com/cweagans/composer-patches
- https://github.com/cweagans/composer-patches/issues/42
<?php
/**
* @file
* Contains \Adapt\RootPatcher.
* @author Tom Helmer Hansen <tom@adapt.dk>
*/
namespace Adapt;
use Composer\Script\Event;
use Composer\Util\ProcessExecutor;
class RootPatcher {
/**
* Patches .htacces file in docroot.
*
* @param \Composer\Script\Event $event
* The script event.
*/
public static function deployPatch(Event $event) {
$extra = $event->getComposer()->getPackage()->getExtra();
if (isset($extra['root-patch'])) {
try {
if (file_exists($extra['root-patch'])) {
$filename = escapeshellarg(realpath($extra['root-patch']));
$patched = FALSE;
// The order here is intentional. p1 is most likely to apply with git apply.
// p0 is next likely. p2 is extremely unlikely, but for some special cases,
// it might be useful.
$executor = new ProcessExecutor($event->getIO());
$patch_levels = array('-p1', '-p0', '-p2');
foreach ($patch_levels as $patch_level) {
$check = sprintf('cd %s && GIT_DIR=. git apply --check %s %s', 'docroot', $patch_level, $filename);
$output = NULL;
$checked = ($executor->execute($check, $output) == 0);
if ($checked) {
// Apply the first successful style.
$command = sprintf('cd %s && GIT_DIR=. git apply %s %s', 'docroot', $patch_level, $filename);
$patched = ($executor->execute($command, $output) == 0);
break;
}
}
if (!$patched) {
throw new \Exception("Cannot apply patch " . $extra['root-patch']);
}
}
}
catch (\Exception $e) {
$event->getIO()->write('<error>Could not apply patch! Skipping. The error was: ' . $e->getMessage() . '</error>');
}
}
}
}
Hi Tom, It's a nice work around drupal-composer limitation, however this script tries to apply patch each time composer install or update is run even if drupal/core isn't modified. Have you improved it since this post ? Thanks a lot.