Setting up Push-to-Deploy with git
I first set up a push-to-deploy system with git and puppet for a side project a few years back. It worked so well I transitioned my company's development process onto it for all of our new projects starting last year. It offers the simplicity of the "push-to-deploy" model Heroku pioneered, with full control and flexibility over the operating system environment.
I've started thinking about my next iteration of this system for Didsum, and using pushing for more than just deployment purposes. The Push-to-_______ pattern is powerful and easy to use, once you know how the pieces fit together. In this post, I'll walk through the setting up Push-to-Deploy from the ground up.
(I'm assuming a working knowledge of: terminal, git, and a scripting language.)
Preparing our Repositories
Let's keep things straightforward by placing our development git repository, remote git repository, and deploy directory under the same local directory.
$ mkdir push-to-deploy
$ cd push-to-deploy
$ mkdir {development,remote,deploy}
Awesome, now let's setup the remote git repository. With a real project, this would be a directory on your production server. There is a special setup for git repositories whose purpose is to receive pushes from developers, they're called "bare" repositories. You can read more about "bare" repositories, but for our purposes their purpose is just to receive pushes.
$ cd remote
$ git init --bare
$ cd ..
Perfect, now let's setup our development directory with git. (You could also copy one of your git project's contents into the development folder.)
$ cd development
$ git init
$ echo "Hello, world." >file.txt
$ git add file.txt
$ git commit -m 'First commit.'
We now have a development repository with its first commit. Our last preparation step is to register the "remote" repository. If you're fuzzy on git remotes, the official git site has you covered.
$ git remote add production ../remote
$ git push production master
Boom. You've just pushed your commit from development to your bare, 'remote' repository. We're ready to setup push-to-deploy.
Set up Push-to-Deploy
Now that our remote repository is setup, we're ready to write a script for what it'll do when it receives a push. Let's navigate to the hooks folder of our remote repository. Hooks are scripts that git runs when certain events happen.
$ cd ../remote/hooks
$ touch post-receive
$ chmod +x post-receive
The hook we care about for push-to-deploy is post-receive. It is run after receiving and accepting a push of commits. In the commands above, we're creating the post-receive script file with touch
, and making sure it's an executable file.
Next, open the post-receive
script in your preferred text editor. Copy the contents below:
#!/usr/bin/env ruby
# post-receive
# 1. Read STDIN (Format: "from_commit to_commit branch_name")
from, to, branch = ARGF.read.split " "
# 2. Only deploy if master branch was pushed
if (branch =~ /master$/) == nil
puts "Received branch #{branch}, not deploying."
exit
end
# 3. Copy files to deploy directory
deploy_to_dir = File.expand_path('../deploy')
`GIT_WORK_TREE="#{deploy_to_dir}" git checkout -f master`
puts "DEPLOY: master(#{to}) copied to '#{deploy_to_dir}'"
# 4.TODO: Deployment Tasks
# i.e.: Run Puppet Apply, Restart Daemons, etc
Let's walk through each of the steps:
When git runs post-receive, the data about what was received is provided to the script via
STDIN
. It contains three arguments separated by spaces: the previous HEAD commit ID, the new HEAD commit ID, and the name of the branch being pushed to. We're reading these values and assigning them to from, to, and branch variables, respectively.Our purpose here is to automate push-to-deploy. Assuming a workflow that keeps production on the master branch, we want to exit this script prior to deploying if the branch being pushed is not master.
The first deploy step is to "checkout", basically export or copy, files from the master branch to the directory where our project is deployed to in production. (Remember, in this demo it's the fake "deploy" directory, in the real world this might be /var/www, or wherever your project expects to be in production.)
Now that our deploy directory is up-to-date, we can run whatever deployment tasks we need to run. This could be applying Puppet scripts (I'll write a post on this scenario soon), restarting a web or application server, clearing cache files, recompiling static assets, etc. Whatever steps you'd normally need to do manually after updating your project's files, automate them here!
Save your post-receive hook, and let's test it out!
Testing with Pushing
We can test our script manually, by creating a new commit in our development directory and pushing:
$ cd ../../development
$ echo "New line." >> file.txt
$ git add file.txt
$ git commit -m 'Testing push-to-deploy'
$ git push production master
In the output of the git push command, you should see lines starting with "remote:". These lines are the output of our post-receive script:
Already on 'master'
DEPLOY: master($TO_ID) copied to 'push-to-deploy/deploy'
The first line is noisy output from the git checkout command in step 3, we can ignore it. The second line, is from the puts command, also from step 3 in our post-receive script.
The directory we're deploying to should now be populated and up-to-date:
$ ls ../deploy
$ diff file.txt ../deploy/file.txt
Pretty awesome, right?
Testing without Pushing
When you're working on a post-receive hook, it's annoying to muck up your project's commit history and push each time you make a change. Luckily, because it's just a script, we can fake it from the command-line.
$ cd ../remote
$ git log -2 --format=oneline --reverse
First, we need to get the IDs of our most recent 2 commits. The git log command, above, will give us these two IDs in the order you'll want to replace the $FROM_ID and $TO_ID variables with, respectively.
$ echo "$FROM_ID $TO_ID master" | ./hooks/post-receive
This method makes setting up your post-receive hooks enjoyable, enabling you to quickly iterate on your script and execute it repeatedly.
Next Steps
In this post, we've walked through how to setup the push-to-deploy with git. For a real world project, your 'remote' and 'deploy' folders would usually be setup on a server, not locally. The details of doing that and properly configuring SSH is beyond the post of this scope (note to self: I should write on SSH configuration, too!).
From here, it's up to your project to determine what actions to automate! Happy pushing!