During the development stage, it is highly recommended to push your code in a production-like environment regularly. Thus, you’ll get immediate feedback on your code behaviour outside your local computer. It can also be useful when you wish to share your work with another team.
A typical deployment use-case could be:
- Complete your Git flow to have a releasable code base
- Pushing code to the remote origin of your git repository server
- Login to your production/staging server
- Git pull the latest code version
- You could add some additional build operations here – for Reactjs, Angular, Vuejs projects for instance
The process described here is quite simple. But, you might find yourself doing these steps repeatedly, and it can become time-consuming in the long run. You might want to consider using an “auto-deploy” feature so that you won’t have to perform all these actions manually. In this article, we’ll see how to implement a simple auto-deploy feature using Git features only. You will be able to deploy a code version from your local computer to a remote server with a single push command.
Our setup will require the following three notions;
- Bare repositories
We’ll set a custom remote to receive the push. Create the corresponding hook file to update the application code with the latest code revision. But first, let’s introduce hooks, bare repositories and remotes.
Introduction to Git Hooks
Git provides a set of hooks. They are scripts to be executed on a specific action. Here is the list of all available hooks that you can find by default in your .git/hooks folder:
- applypatch-msg: called when commit message is set or edited – can abort the commit
- pre-applypatch: called after the patch is applied, but before the changes are committed – can leave in uncommitted state
- post-applypatch: called after the patch is applied and committed
- pre-commit: used to check the commit itself
- prepare-commit-msg: called before opening the commit message prompt – can abort the commit
- commit-msg: Called to adjust the message after been edited – can abort the commit
- post-commit: called after commit is made – used for notifications
- pre-rebase: called before rebasing a branch
- post-checkout: called after git clone or checkout to another branch
- post-merge: called after a merge, cannot abort the merge
- pre-push: called before push is done – can exit the push
- pre-receive: called on the remote repository before updating the pushed refs
- update: run on the remote repository for each ref being pushed
- post-receive: runs on remote after all refs had been updated
- post-update: runs on remote repository once all refs have been pushed
As you can see from the list above, some of these hooks will be executed from the local repository whereas others will run on the remote repository. It leads us to separate the hooks into two categories: client-side hooks and server-side hooks.
The name of the file describes when it is meant to run. By default Git generates some hook files with the extension .sample that should be removed so that the hook can be executed by Git. When adding a server-side hook, make sure that the file is executable, and owned by the corresponding ssh user. Otherwise, the hook execution will fail.
Check if file is executable in Linux
$: ls -l filename
-rw-r--r-- 1 root root 0 Apr 12 11:07 myscript.sh
This will output the file permissions in a readable format. Here, three different permissions chunks are displayed. As this is off-topic, let’s not focus on Linux permissions here. But I would still recommend to learn more about file permissions in Linux. Look for a x pattern, that indicates an execute permission. In the previous example, we only have read and write permission for owner, read access for the rest.
Grant execute permission on a file
$: chmod +x filename
$: ls -l filename
-rwxr-xr-x 1 root root 0 Apr 12 11:07 myscript.sh
Running the ls -l command, you can now see an execute permission on the file. Most importantly, you grant the execute permission to anyone, allowing any ssh user to execute the file.
Change file owner
During the workshop, you might need to set a different owner for a file or directory, just use the following:
$: chown user:group filename #to change file ownership
$: chown -R user:group directory #to change directory and sub-directories ownership
Introduction to Git bare repositories
If you are familiar with Git, you should have noticed the .git folder that is created on Git initialisation. This hidden folder contains information that Git uses to operate correctly. This folder is called the Git Directory. A project’s source code on Git is called the Working Directory. So when cloning a project, you are fetching all code from the working directory and also Git configuration and commits and more, from the Git directory. This is also what you’ll end up with when running the git init command.
-- .git # Git directory
-- file A # Following is the working directory
-- file B
A bare repository is a Git repository that only holds Git information, not the working directory, the content of the .git folder. To create a bare repository, run the following command:
$: git init --bare [folder-name]
The previous line should generate the following elements:
To dive into details of the Git configuration files is not the purpose of this article but here are a few things that you might want to know :
- HEAD: keep track of tip of the branch you are working on, references a path in the /refs folder
- config: this file contains definitions for your remotes, branches, user information, etc …
- hooks: this folder contains some default / disabled hook definitions
- info/exclude: this file is similar to .gitignore file except that changes to exclude are not committed. It is rarely used.
- objects: when performing a commit in Git, you generate objects. Basically, the updated files in the working directory get compressed and store in Git’s data structure under a hash name in the objects directory.
- refs: contains references to hashes
So, a git bare repository is a Git repository that does not provide a working directory, meaning you won’t be able to create new objects from that repository but, you’ll have all information about branches and commits in its internal structure.
Why use a bare repository ?
Knowing that a bare repository is not meant for coding – since it doesn’t provide a working directory – what is it used for? Well, it is mostly used as a Remote Repository. A remote repository is for sharing code between developers or maintain releases on named remotes.
Introduction to Git remotes
A Git remote is remotely hosted version of your project. You can have multiple remotes to manage your different environments or versions. You can see the registered remotes by typing:
$: git remote
By default when cloning a project from Github, you’ll find the reference to the master project under the remote name origin. To get more information about your remote, type the following command:
$: git remote -v
With the -v option you get more than the remote name: the server address hosting your remote repository. Should be github.com if you cloned a repository from there. When creating the first commit on your master branch, you might need to push with a bit of extra information such as:
$: git push -u origin master
This line tells Git to set the default upstream remote to origin. Thus, when pushing and pulling changes, it will address the origin remote. If you wish to push/pull changes to another remote, let’s say production, go with:
$: git push production master
Create a new remote
A remote is a reference to an upstream repository, so, in order to create a remote one must have a repository hosted somewhere on an accessible server. Once your host is set, you can configure your remote. Here is an example using a SSH connection :
$: git remote add production [sshuser]@[host]:path/to/remote/repository
Now when typing git remote, you should be able to see something like:
Push changes to a specific remote
The push command comes with some options to specify a remote and a branch. By default, the git push command without arguments will push the current branch on the origin remote.
Push the current branch to a specific remote :
$: git push [remote-name]
Specify a branch to push to a specific remote :
$: git push [remote-name] [branch-name]
WORKSHOP : deploy your code in a single command line
With this in mind, we can start our workshop that will consist of a server-side hook to update a remote code-base with the last revision. We’ll assume that we have a website versioned with Git and running somewhere on a server accessible over SSH. To get a sight of our target setup, here is a scheme:
To achieve such setup, here is our task list:
- First we’ll SSH into the remote server and create a bare git repository.
- Then, from our local computer, we will create a new remote pointing to the bare git repository that we created on the server.
- Lastly, we will write a hook to be triggered when the bare repository receives a push. The hook will update the website content with the latest changes.
Thus, we will performed live updates in our remote server using just a git push command from our local computer.
Creating the git bare repository
This step is very straightforward, log into your server, and enter:
$: git init --bare ./dev.git
This line will create a new Git bare repository in a folder called test.git. If you navigate to test.git you’ll see the bare repository files & folders. You’re done…so far!
Creating a new remote on local computer
In order to push commits to our bare repository, we need to add it as a new remote from our local computer.
$: git remote add development [sshuser]@[host]:path/to/dev.git
Refer to the “Create a new remote” section if you have any further question. The path to dev.git is relative to the home of the SSH user you use. In our example, we created the dev.git repository at /home/user/dev.git so the command line would end with: dev.git. You should see your remote using git remote command. At this point you can try to push your commits to the brand new remote:
$: git push development
It will display a prompt asking for the SSH user password and will perform the push afterwards.
If the process stops without exiting: you might have a permission issue, go to the server and check that the SSH user has the proper privileges over the bare repository.
It worked? Great! Now, go the server, head to the bare repository and run:
$: git log
You should see you repository’s commit history!
Adding the post-receive hook
As is, our bare repository does not do anything more than receive the commits pushed on it. The objective is to add a hook that will copy the last git version in our target web-site directory. Because we are using a bare-repository, we have to get another folder that will hold our working directory. It is a typical scenario to put a website code in the /var/www/ folder. For this workshop, the target web-site folder will be at /var/www/web-site.
note : A git hook executes a script so you can perform basically any operation you want. For this workshop, we’ll take a simple procedure, but you could imagine running some test scripts and deploy if test process is complete without errors.
Head back to the remote server, in the dev.git/hooks directory and create – if not already created – a file named post-receive. The filename must match exactly, without extension so that Git can find and execute our hook. Here it the hook content:
while read oldrev newrev ref
#Adding a condition to perform action only for "dev" branch
if [[ $ref = refs/heads/$BRANCH ]]; then
#Pulls last revision in $TARGET directory
git --git-dir=$GIT_REMOTE --work-tree=$TARGET checkout -f
Here we want to update code from /var/www/web-site with last git commit. To perform this action, we want to execute a checkout instruction in the $TARGET folder. Let’s inspect the git command line :
–git-dir : Sets the path to the git directory, in a common working directory the git directory is the .git at the root your directory. Here we tell Git that the git directory is our custom remote.
–work-tree : Sets the path of the working tree, can be an absolute path or relative. Remember, the work tree will hold our application code in a regular format, so it will be readable by our web-server.
checkout -f : here is the command we execute, it sets the current commit to the last pushed commit on branch. The -f option is to discard local changes – if any – in working directory.
If you wish to learn more about Git hooks here is a useful resource.
Before testing make sure your hook file:
- Has the correct name
- Is executable
- Is executable by the SSH user
If not sure, refer to the first part of the article in the “Introduction to Git Hooks” section.
Will these elements in place, go to your local working directory, and make some changes. Then add, commit and push to the remote:
$: git add -A && git commit -m "Testing auto-deploy on remote" && git push development
Again, this command should ask for the SSH user credentials and then perform the requested push. Head to the server and open your $TARGET folder. Every changes made locally should be available in the $TARGET folder without any additional action!