Automated Drupal Updates using Drush

Drupal updates can be a real pain. Either the update path from one of the modules is broken, you need to reapply a patch for a bug, which still isn't fixed or something else goes wrong. In seven years of Drupal development I basically had it all and I grew so accustomed to it, that I started to do updates one module after another. I also create a separate commit for every single updated module. This way I can revert the update immediately, if something goes wrong. Luckily I found a way to automate this process.

A while ago I had enough and created a little update script for me. It is called drupdate and it does all the work for you, if you are nice to it and comply to some concepts. You can get the most recent version from Gist. But first let me walk you through it line by line.

Step 1

First I will define a few variables.

ROOT_DIR=$(pwd)
PUBLIC_DIR="$ROOT_DIR/web"
PATCHES_DIR="$ROOT_DIR/patches"
DRUPGNORE_PATH="$ROOT_DIR/.drupgnore"
  • ROOT_DIR stores the folder we are currently in (this should be the project's root folder).
  • PUBLIC_DIR is the web directory of the project. I prefer to create a separate web directory, where I put the drupal installation. This way the folder isn't cluttered with development specific stuff, like Linter configurations and so on.
  • PATCHES_DIR defines the folder where I put all my patches. Whenever I have to patch a module or Drupal core, I create a patch and put it in this directory. Normally I prefix the patch with the name of the module it belongs to, so I can easily determine if its relevant for an update or not.
  • DRUPGNORE_PATH defines the path to the .drupgnore file. This file contains names of modules I want to exclude from the automated update process.

Step 2

issue="$1"
if [[ $issue == "" ]]; then
  echo "Usage: drupdate [Issue number]"
  exit 1
fi

Most of the time I have some sort of ticket or issue, where I track the time used for the update and the process itself. Here I check if I remembered to add the ticket number to the script. This way I can later use it to add it to the commit messages.

Step 3

ignore=""
if [ -f "$DRUPGNORE_PATH" ]; then
  ignore=$(cat $DRUPGNORE_PATH)
fi

Before I defined a path for my .drupgnore file. Here I check, if it is present and load its contents to a variable ignore.

Step 4

echo "Starting update..."
if [ -d "$PUBLIC_DIR" ]; then
  cd $PUBLIC_DIR
fi

In the more recent projects I use a web folder where the actual Drupal installation. In older projects this might not be the case. So I check if a folder with the name web exists and change the directory, if necessary. Drush needs to be run in the Drupal root folder to be able to perform update tasks.

Step 5

modules=$(drush ups -p)
while read module; do
  if [[ $ignore == *"$module"* ]]; then
    echo "Ignoring $module..."
  else
    echo "Updating $module..."
    description=$(drush up $module -y)

    if [ "$module" == "drupal" ]; then
      git checkout HEAD -- .htaccess
      git checkout HEAD -- .gitignore
    fi

    git add --all
    git commit -m "Updated $module. #$issue" -m "$description"
  fi
done <<< "$(echo "$modules")"

echo "Updates finished."

This step is a little bigger and this is were the magic happens. I load a list of all available updates through Drush and store the output in a variable modules. Then I loop over this variable, dealing with every module one by one.

Inside the loop I check, if the module is ignored. Currently I use a pretty simple check, which will not work in all cases. For example, if I want to ignore webform, webform_features will also be ignored.

If the module isn't ignored I will download and perform the update. The results of this process will be stored in a variable description. If the "module" is Drupal core, I will have to reset two files before committing anything, .htaccess and .gitignore. Most of the time I will have some custom logic in these files, which I will like to preserve.

Last, but not least, I add all files to the index and commit them to the current branch. Please note, that you should have a clean workstate before using this script. The commit message contains update information, the issue number and the results of the update stored earlier.

Step 6

cd $ROOT_DIR
if [ -d "$PATCHES_DIR" ]; then
  echo "Applying patches.."
  for file in $PATCHES_DIR/*; do
    echo "Applying $(basename $file)..."
    git apply $file
  done

  git add --all
  git commit -m "Applied patches. #$issue"
fi

In the final step I loop through all patches in the patches folder and try to apply them. This will actually create a lot of errors, as patches might not apply. Most of the time you can just ignore them. It would be smarter to just use the patches relevant to the updated modules, but I can't always assume that I named the patches correctly.

Installation

You can download the script from Gist, change it to match your personal requirements or just use it as is.

wget -O drupdate https://gist.githubusercontent.com/christianhanne/eab02899f413c927104ce94e11510e53/raw/cbb9341ad0c18fc783728cf4dbbbfe0a94bf2f9e/drupdate
chmod +x drupdate
sudo mv drupdate /usr/local/bin/drupdate