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-receive
hook 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
/opt/repositories
directory. - 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 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
- Ensure that
FULL_DOMAIN
($1
),ROOT_DOMAIN
($2
), andSUB_DOMAIN
($3
) are set, and if not, let the user know. - Set the
htdocs
path, which is where the code will ultimately be cloned or pulled into via thepost-receive
hook. - 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. 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
- 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
wordpress
directory containing the WordPress submodule. - Create the
uploads
directory 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
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 likeAPP_ENVIRONMENT
, sinceFH
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 modifyAPP_ENVIRONMENT DEVELOPMENT
for each server? I have addedAPP_ENVIRONMENT DEVELOPMENT
toapp_environment.conf
and included it intohttpd.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 settingAPP_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, puttingAPP_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 likegit push prd master
, whereprd
is the production server remote?