Automating Git Deployment
Tips and tricks and a script…
Disclaimer
This post assumes a basic understanding of git version control. If you don’t know about it, I recommend reading chapter two of the free git book.
Contents
∘ A Clear Case for Automation
∘ Checking Out
∘ Merging the Branches
∘ Logging and Pushing
∘ Confirmation
∘ Adding Variables
∘ Getting Back to Work
∘ Putting Everything Together
∘ Was it Worth Taking the Time to Automate?
A Clear Case for Automation
Several companies I’ve worked for have deployed using the concept of a “trunk” branch, used for development branching and a “main” branch in line with what is actually deployed.
Deployment then involves pulling down the latest versions of both branches, merging “trunk” into “main” and then pushing “main” back to the origin git repo, where the automated CI/CD (Continuous Integration / Continuous Delivery) process will run tests and builds. If these processes succeed it will deploy the built artifacts or Docker containers to production servers.
This local pulling, merging and pushing process involves only a few steps but it still takes time and has room for error. So it seems like the perfect opportunity for a script, and in this case, since git commands work beautifully on the command line, a bash script felt just right to me.
Checking Out
We first need to ensure we’re on our “trunk” branch and pull down the latest approved changes to be sent to production. Then we need to switch to our “main” branch and fetch the latest changes there. This already sounds repetitive. Why not add a function:
function checkoutpull {
git checkout "$1"
git pull
}checkoutpull trunk
checkoutpull main
There’s a slightly safer and more elegant way of doing this which should avoid any unnecessary merge commits. It starts by getting the latest changes for our origin repository with git remote update
—doing this once covers both branches — and then, rather than calling git pull
, merging each branch with its associated upstream branch using git merge --ff-only @{u}
.
function checkoutpull {
git checkout "$1"
git merge --ff-only @{u}
}git remote update
checkoutpull trunk
checkoutpull main
Merging the Branches
Now let’s merge “trunk” into our “main” branch. Once again, we’ll use the ff-only
strategy.
...
checkoutpull trunk
checkoutpull main
git merge $STAGING_BRANCH --ff-only
Logging and Pushing
Then there’s just one more thing to do, right? Push the branch and we’re done. But as responsible developers, we should have a quick peek at git’s log file to ensure we’re really deploying what we intend to. It’s possible that someone else accidentally merged in an unintended change just before we updated our “trunk” branch. Stranger things have happened.
Se we add:
git log
git push
git log
will open the commit log file in the configured viewer, which is “less” by default. When we press q
less will exit and the script will push to master.
Done.
Except…
What if we don’t like what we see in git log
? We want to avoid pushing to prod then.
Confirmation
Like most languages, bash has the ability to ask a user for input, so let’s use it! To be extra safe, let’s make ourselves type the word “yes” to execute a deployment. Typing anything else will exit the script without deploying.
git log
read -p "Deploy? yes/[n]: " SHOULD_DEPLOY
if [ "$SHOULD_DEPLOY" == "yes" ]; then
git push
fi
This also gives us a chance to automatically roll back our local “main” branch when we decide not to deploy.
if [ "$SHOULD_DEPLOY" == "yes" ]; then
git push
else
git reset --hard "origin/main"
fi
Adding Variables
To make our script more reusable, we want to allow for different branch names. Why not allow optional arguments to override our default “main” and “trunk” branch names? Easily done, and while we’re at it, let’s wrap our code in a “main” function for tidiness.
...function main {
local DEPLOY_BRANCH=${1:-main}
local STAGING_BRANCH=${2:-trunk}
git remote update
checkoutpull "$STAGING_BRANCH"
...
}main "$@"
Now we just need to find the uses of branch name “main” (not function “main”) and replace them with “$DEPLOY_BRANCH” and replace “trunk” with “$STAGING_BRANCH” and we’ve got a nicely reusable script that’s still quick to call when we are using default values.
Getting Back to Work
One more nice optimization springs to mind. When we’re finished deploying we don’t want to keep working from our “main” branch. Instead, we’ll want to go back to where we were when we started the deployment process, be that the “trunk” branch or another feature branch. We can easily memoize that branch name at the beginning of the process and have our script return us there at the end.
...function main {
local CURRENT_BRANCH=$(git branch --show-current)
...
git checkout "$CURRENT_BRANCH"
}
Putting Everything Together
Here’s a full version of this script, with some extra lines of logging added in for clarity. I made another function, called printemphasized
(print emphasized) to make the script’s logging lines more noticeable.
Was it Worth Taking the Time to Automate?
As with any automation, it’s always worth taking the time to consider whether the total outlay of time to develop the solution was more than we’d ever spend on doing the task manually. More importantly, does the end solution decrease the chances of errors and the amount of repetitive toil in our regular work?
Judging by any of these metrics, I personally consider this script a success, although I’m also curious to hear about any improvements other developers would make to it.
In any case, I hope this script helps to speed up and eliminate potential errors in your deployment workflow and get you back to developing cool and useful software as quickly as possible.
If you liked this article, you might be interested in my medium series about tools for new developers.