Git post-receive hook to deploy WordPress and plugins as submodules
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:
- QA waits for a release branch
- Staging waits for the master branch
- 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:
- Bare repository – storage unit that uses a
post-receivehook to deploy the project.
- Working repository – web root that will serve the project to the end-user.
This method has several benefits:
- View a combined list of all version-controlled projects via the
- Recover a corrupted web root by cloning a fresh copy from the origin.
- 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
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
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
- Ensure that
$3) are set, and if not, let the user know.
- Set the
htdocspath, which is where the code will ultimately be cloned or pulled into via the
- Loop through all the branches that have been pushed.
- 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.
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
- Clone the project if the web root is empty.
- Create a timestamp (
.created) for when the project was first deployed.
- Pull the project if the
HEAD‘s branch gets pushed again, so that we can merge in any changes we might have made.
- Fetch and checkout the project if an entirely new branch was pushed.
- 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
- Ensure that the project we deployed is powered by WordPress by checking that there’s a
wordpressdirectory containing the WordPress submodule.
- Create the
uploadsdirectory if it doesn’t already exist, and give the web server write permissions to it.
- Create additional directories required by installed plugins, as determined by the plugin directory name in the
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.
Previously posted in WordPress and transferred to Ghost.
June 6, 2014 at 9:38 am
Hi Ryan, great writeup, great technique!
I’m scratching my head wondering where you set the
June 6, 2014 at 11:17 pm
1. You can actually call this whatever you like. Generally something like
FHdoesn’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.confto 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 DEVELOPMENTand change out the environment name on each server. That variable and its value will then be available as a global in PHP.
June 12, 2014 at 1:55 pm
Excellent, thank you.
December 4, 2014 at 4:32 pm
Thank you for this brilliant guide! I was just wondering, where do I modify
APP_ENVIRONMENT DEVELOPMENTfor each server? I have added
app_environment.confand included it into
httpd.confas 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!
December 4, 2014 at 8:27 pm
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.
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 DEVELOPMENTserver-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…
December 5, 2014 at 11:07 am
APP_ENVIRONMENTin 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!
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!
December 6, 2014 at 3:16 pm
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
prdis the production server remote?