Git post-receive hook to deploy WordPress and plugins as submodules

Wall with graffiti and letters PUSH.
Git yourself to push toward automation.

In a previous blog post I discussed how best to manage file and folder permissions when deploying with Git, but today I’ll show a specific example of what that post-receive hook might look like for a WordPress project that uses submodules.

I have three servers that I can deploy to, but the post-receive hook only deploys a project when it encounters the specified branch as defined per server:

  1. QA waits for a release branch
  2. Staging waits for the master branch
  3. Production waits for the master branch

Other than this small difference, the post-receive hook is identical on all three servers to reduce maintenance.

Lastly, each server has two repositories per project:

  1. Bare repository – storage unit that uses a post-receive hook to deploy the project.
  2. Working repository – web root that will serve the project to the end-user.

This method has several benefits:

  1. View a combined list of all version-controlled projects via the /opt/repositories directory.
  2. Recover a corrupted web root by cloning a fresh copy from the origin.
  3. Prevent some issues that may occur during the deployment, since it will fail before post-receive hook is fired.

1. Initial setup

I use a wrapper so that I don’t have to update the actual post-receive hook in all my bare repositories each time I make a change to the script.

This is what you’ll find in the post-receive hook:

source /opt/scripts/git/hooks/post-receive.sh FULL_DOMAIN ROOT_DOMAIN SUB_DOMAIN

You can then just pass the host-specific details as arguments, for example:

  • FULL_DOMAIN – test.domain.com
  • ROOT_DOMAIN – domain.com
  • SUB_DOMAIN – test

I actually have another script that creates the bare repository, copies the above template into the repository’s hooks directory, sets execute permissions accordingly, and then dynamically replaces the placeholder arguments.

2. Listening for desired branch

The post-receive.sh script that the post-receive hook calls should only perform an action if the desired branch was pushed, the reason being that we don’t want to pull or checkout a branch into a working directory just to then overwrite it with another subsequent branch in the same deployment. We’ll want to ignore other branches that may accidentally or inherently (tags) get pushed.

That’s not to say you couldn’t tweak this to match whatever procedures and protocols you have in place for the function of your servers.

We’ll start our script with the following:

if [[ (-n $1) && (-n $2) && (-n $3) ]]; then

  # Set path to htdocs
  htdocs="/var/www/domains/$2/$3/htdocs"

  # For each branch that was pushed
  while read oldrev newrev refname
  do

    # Get branch name
    branch=$(git rev-parse --symbolic --abbrev-ref $refname)
    echo "> Received $branch branch"

    # If branch is release branch
    if [[ "$branch" == "release/"* ]]; then
      ...
    fi
  done

# Print arguments to debug
else
  echo "Not all required variables have values:"
  echo "> FULL_DOMAIN: $1"
  echo "> ROOT_DOMAIN: $2"
  echo "> SUB_DOMAIN: $3"
fi
  1. Ensure that FULL_DOMAIN ($1), ROOT_DOMAIN ($2), and SUB_DOMAIN ($3) are set, and if not, let the user know.
  2. Set the htdocs path, which is where the code will ultimately be cloned or pulled into via the post-receive hook.
  3. Loop through all the branches that have been pushed.
  4. Continue with the script if the branch name matches the one we’re looking for.

The example above is for the QA server. "$branch" == "release/"* will match all of my releases e.g. release/1.0.0, release/1.1.0, release/1.2.1, etc.

For the staging and production servers you could change this to "$branch" == "master".

3. Determining first or subsequent deployment

If it’s the first time I’m deploying a project, I’ll clone it into the web root, but if it’s not, then I simply pull or checkout the branch that was pushed.

# If branch is release branch
if [[ "$branch" == "release/"* ]]; then
  echo "> Processing $branch branch"

  # Unset global GIT_DIR so script can leave repository
  unset GIT_DIR
  echo "> Unset GIT_DIR"

  # Move into htdocs
  cd $htdocs
  echo "> Moved into $htdocs"

  # If htdocs is empty
  if find . -maxdepth 0 -empty | read; then
    echo "> Determined htdocs is empty"
    ...

  # If htdocs is not empty	
  else
    echo "> Determined htdocs contains files"
    ...
  fi
fi

We look at the web root and if there’s anything in it, we know we’ve deployed before, but if it’s empty, we know this is the first time.

4. Cloning, pulling, or checking out

# If htdocs is empty
if find . -maxdepth 0 -empty | read; then
  echo "> Determined htdocs is empty"

  # Clone branch from origin repository into htdocs
  git clone /opt/repositories/$1.git -b $branch .
  echo "> Cloned origin/$branch branch from $1.git repository into htdocs"

  # Create empty file to remember created date
  touch $htdocs/.created
  echo "> Created .created file in htdocs"

# If htdocs is not empty	
else
  echo "> Determined htdocs contains files"

  # Get HEAD of working directory
  current_branch=$(git rev-parse --abbrev-ref HEAD)
  echo "> Determined working directory is on $current_branch branch"

  # If branch matches HEAD of working directory
  if [ "$branch" == "$current_branch" ]; then
    echo "> Determined updates affect current branch"

    # Fetch and merge changes into htdocs
    git pull origin $branch
    echo "> Pulled origin/$branch into $branch branch in htdocs"

  # If branch does not match HEAD of working directory
  else
    echo "> Determined updates belong to new branch"

    # Fetch changes from origin
    git fetch origin
    echo "> Fetched changes from origin"

    # Checkout new branch
    git checkout $branch
    echo "> Checked out $branch branch in htdocs"
  fi				

  # Create or update empty file to remember last updated date
  touch $htdocs/.updated
  echo "> Updated .updated file in htdocs"
fi
  1. Clone the project if the web root is empty.
  2. Create a timestamp (.created) for when the project was first deployed.
  3. Pull the project if the HEAD‘s branch gets pushed again, so that we can merge in any changes we might have made.
  4. Fetch and checkout the project if an entirely new branch was pushed.
  5. Create a timestamp (.updated) for when the project was updated.

The timestamps are purely for informational purposes. I use the updated timestamp sometimes to verify that the code was indeed deployed into the working directory.

5. Updating all submodules

If we have WordPress and plugins setup as submodules, we need to make sure that they get updated on your deployment server if they were updated in the origin.

# Fetch commits and tags for each submodule
git submodule foreach git fetch --tags
echo "> Fetched commits and tags for each submodule"

# Update all submodules
git submodule update --init --recursive
echo "> Initialized and updated all submodules"

6. Creating web server writable directories

This part will only work correctly if you followed my previously mentioned blog post. We’re creating necessary non-version-controlled directories that WordPress or plugins may need to write to.

# If website is WordPress powered
if [ -d "$htdocs/wordpress" ]; then
  echo "> Determined website runs WordPress"

  # If uploads directory does not exist
  if [ ! -d "$htdocs/wp-content/uploads" ]; then
    echo '> Determined htdocs/wp-content/uploads does not exist'

    # Create uploads directory
    mkdir $htdocs/wp-content/uploads
    echo '> Created uploads in htdocs/wp-content'

    # Give Apache permissions to write to uploads
    chmod g+w $htdocs/wp-content/uploads
    echo '> Added write permissions to htdocs/wp-content/uploads'
  fi

  # If w3-total-cache directory exists
  if [ -d "$htdocs/wp-content/plugins/w3-total-cache" ]; then
    echo "> Determined WordPress has W3 Total Cache installed"

    # If cache directory does not exist
    if [ ! -d "$htdocs/wp-content/cache" ]; then
      echo "> Determined htdocs/wp-content/cache does not exist"

      # Create cache directory for cache files
      mkdir $htdocs/wp-content/cache
      echo "> Created cache in htdocs/wp-content"

      # Allow Apache to write to cache
      chmod g+w $htdocs/wp-content/cache
      echo "> Added write permissions to htdocs/wp-content/cache"
    fi

    # If w3tc-config directory does not exist
    if [ ! -d "$htdocs/wp-content/w3tc-config" ]; then
      echo "> Determined htdocs/wp-content/w3tc-config does not exist"

      # Create w3tc-config directory for configuration files
      mkdir $htdocs/wp-content/w3tc-config
      echo "> Created w3tc-config in htdocs/wp-content"

      # Allow Apache to write to w3tc-config
      chmod g+w $htdocs/wp-content/w3tc-config
      echo "> Added write permissions to htdocs/wp-content/w3tc-config"
    fi
  fi
fi
  1. Ensure that the project we deployed is powered by WordPress by checking that there’s a wordpress directory containing the WordPress submodule.
  2. Create the uploads directory if it doesn’t already exist, and give the web server write permissions to it.
  3. Create additional directories required by installed plugins, as determined by the plugin directory name in the plugin directory.

Conclusion

I’ve used this setup for a while now and it has worked quite well. You can view the complete script in my post-receive Gist over at GitHub. If you have any questions, problems or improvements, share them in the comments below.

Featured image by Robert Katzki.


Comments (9)

Previously posted in WordPress and transferred to Ghost.

Brendon
June 6, 2014 at 9:38 am

Hi Ryan, great writeup, great technique!
I’m scratching my head wondering where you set the $FH_ENVIRONMENT variable.

Ryan Sechrest
June 6, 2014 at 11:17 pm

Hi Brendon,

1. You can actually call this whatever you like. Generally something like APP_ENVIRONMENT, since FH doesn’t really mean a whole lot to you.

2. You set this within an Apache configuration file. For example, you can place it in /etc/httpd/conf/httpd.conf to make this variable available to all virtual hosts, or you can simply place it within a specific virtual host file. All you need to do is put something like this on its own line: APP_ENVIRONMENT DEVELOPMENT and change out the environment name on each server. That variable and its value will then be available as a global in PHP.

Brendon
June 12, 2014 at 1:55 pm

Excellent, thank you.

Thiago
December 4, 2014 at 4:32 pm

Thank you for this brilliant guide! I was just wondering, where do I modify APP_ENVIRONMENT DEVELOPMENT for each server? I have added APP_ENVIRONMENT DEVELOPMENT to app_environment.conf and included it into httpd.conf as you suggested, but as a newbie at server management i have no idea how to modify the individual server

Thanks for any help in advance!

Ryan Sechrest
December 4, 2014 at 8:27 pm

Hi Thiago,

Can you clarify what you mean by “modify the individual server”? Once you’ve added the environment variable to an Apache configuration file, which it sounds like you have, and you reload Apache (service httpd reload), you should be good to go.

Thiago
December 5, 2014 at 6:04 am

Hi Ryan, thank you for such a prompt reply!
I think I got a little confused, I really didn’t get much sleep because I have been trying so hard to get this working! I currently own a VPS with mediatemple and am hosting a few client’s sites on the server.

I was a little confused about setting APP_ENVIRONMENT DEVELOPMENT server-wide. So all we are doing here is making this variable available am I right?

So far I have been going through your guides and have been able to create a ‘web’ group with users ‘git’ and ‘apache’. I give the group access to /var/repo/ in which I create bare git repos. In them I have create post-receive hooks which set the work tree to /var/www/vhosts/domain.com/httpdocs/ (which is how the server came set up). I can successfully push to my bare repos, which in turn copy the repo contents to the set webroot.

My next mission is to now set up the post-hook you have laid out here, using WordPress. I currently use Mark Jaquith’s WordPress Skeleton which includes WordPress as a submodule. Unfortunately there seems to be no clear guide on how to get a submodule to work with hooks so I am hoping yours will do the trick!

PS I am a self-taught web developer, with less than a year experience, and man is it time-consuming and challenging having to learn git, gulp, linux-shell, server-admin, business, client relations etc etc etc! And people think its HTML+CSS…

Ryan Sechrest
December 5, 2014 at 11:07 am

Hi Thiago,

Yes, putting APP_ENVIRONMENT in the Apache configuration file makes it available to the shell script and your PHP application (WordPress). It is used by the shell script to determine what type of server it is (development, staging, production) and then listens for the appropriate branch being pushed in that environment. This value will also then be available in PHP, should you have, for example, different database credentials for different environments. If you only have one environment, you don’t even really need this, because it will default to production and listen for the master branch.

Glad to hear your making progress and with regard to updating submodules on a push, yes, this post-receive hook will take care of that (both the WordPress submodule and any plugins that are installed as submodules).

Keep hanging in there– we all started where you are today!

Thiago
December 5, 2014 at 12:53 pm

Thank you so much for your help so far Ryan, and the encouragement. I can get quite stubborn, I NEED to know how to do this hehe.

So far I have managed to wrap my head around the Plesk user permissions, I ended up adding plesks user to web, and adding git to plesk’s client group, with permissions to write in httpdocs (webroot). So far I have modified your file and removed the staging variable as I am only going to be using two servers, develop and production (master).

I could not get the script to do anything yet, but when I execute it myself via bash I get:
sh-4.1$ /opt/scripts/git/hooks/post-receive.sh dev.domain.com domain.com dev

> Received  branch
> Determined environment is PRODUCTION
But that’s all it tells me. I will dig into the logs, after I find them to see what else I can do. So far, if I use a post-receive hook with a simple work tree directive, it works:
#!/bin/sh
git --work-tree=/var/www/vhosts/domain.com/dev --git-dir=/var/repo/hooks-test.git checkout -f
Except for the damn submodules, the wp folder remains empty. I am contemplating using Plesk to manage wordpress and only use git for the actual theme itself. Its a battle between determination and patience now!

Ryan Sechrest
December 6, 2014 at 3:16 pm

Hi Thiago,

I’m glad to hear you figured out the permissions issue and happy that you shared that for anyone else who is running into the same issues.

When you manually run the post-receive.sh shell script, you will only see those two lines, because you’re not actually pushing a branch. So, once the environment has been detected, it checks to see which branch it should work with, but since there isn’t one, there are no other steps to take.

If you use something like SourceTree to push your code, go to Preferences > General and make sure “Always display full console output” is checked. That will give you more feedback than simply success or fail. If you’re pushing via the command line, you should see everything already.

Does the script seem to stop at the same place when you actually push a branch via something like git push prd master, where prd is the production server remote?