Continuous Deployments with Github Actions

This article has been written for anyone interested in getting started with GitHub Actions. As I have been working with Capistrano in the past, I'll do the GitHub action in a Capistrano style or flavour.

For the last couple of years, I have been working with Ruby-based Capistrano to deploy my projects to staging and production environments. In my experience the tool is not  hassle-free but minor problems and hickups are solvable.

One advantage it gives developers a lot of flexibility because any branch can be deployed to any previously configured environment at any time. All that by typing a one-liner on the command line cap production deploy.

Goodbye Capistrano.

Last year GitHub launched a new feature called GitHub actions which allows users to automate tests, code validations and so on, based on events which are triggered by interacting with the code repository. Getting started is easy because one can use pre-made actions, shared by the community.

Our first Action

Let's create our first GitHub action. We create a new file in our code repository .github/workflows/deploy.yml. Therefor you need to create the folders .github/workflows and save the file deploy.yml there. 

Something important to mention here is that the yml Syntax requires exact indentation, otherwise it cannot be processed properly. So keep that in mind. 

name: Deploy Action

on: 
   push:
      branches:
         - master
jobs:
  deploy:
     name: Deploy
     runs-on: ubuntu-latest
     steps:
       - uses: actions/[email protected]
       - name: Zip entire project
         uses: montudor/[email protected]
         with:
             args: zip -qq -r ./latest.zip .
       - name: Send zip to remote server
         uses: horochx/[email protected]
         with:
           local: "./latest.zip"
           remote: "/var/www/html"
           host: ${{ secrets.PRODUCTION_IP_ADDRESS }}
           port: "22"
           user: "deploy"
           key: ${{ secrets.PRODUCTION_SSH_KEY }}

       - name: Execute post-deploy.sh
         uses: appleboy/[email protected]
         with:
           host: ${{ secrets.PRODUCTION_IP_ADDRESS }}
           username: "deploy"
           key: ${{ secrets.PRODUCTION_SSH_KEY }} 
           port: "22"
           script: sh ~/post-deploy.sh
  1. The first bit, defines a name for our action and defines that this action will only be executed when we push changes into our master branch.
  2. Then we have the actual job, defining what need to be done. Starting with a checkout of the projects' code (the contents of our repository).
  3. Next step is to zip everything into latest.zip
  4. Now we scp the file over to the remote production server. That said we are working on the master branch so it makes sense that the code goes to our production environment. Similarly you can set up a similar deploy.yml which acts on the development branch and sends it to your staging environment. When you don't deploy to a single production server but to a farm of servers behind a load balancer you will need to also take care of file replication between the servers via lsyncd or rsync same as you would with Capistrano. Note the ${{ secrets.SECRET_NAME }} syntax here, which is used by GitHub to inject credentials into the Action. You can create and store these in GitHub by opening your repository, clicking Settings and then chose Secrets from the menu. 

    Note that we need to provide a username, an SSH key and of course the servers' IP address in order for having GitHub interact with the server – just like you would when you SSH into your server.  
  5. In the last step we will execute a file called `post-deploy.sh` on our remote server. This shell script will take care of  unpacking of the zip file, setting file permissions, symlinking asset folders and finally reloading our server. Before looking at the post-deploy.sh 

User & Credentials

I recommend that – similar to working with Capistrano – you create a deploy user, with limited rights and permissions on your server, like this: 

adduser deploy
passwd -l deploy
sudo su - deploy
ssh-keygen -t rsa -C '[email protected]'

What we do here is: create a user called deploy, switch to that user and create a public and private SSH Key pair.

The contents of the file /home/deploy/.ssh/id_rsa is what you will save as PRODUCTION_SSH_KEY secret in GitHub.

Alternatively you can also create SSH Key pair on your local development computer and the contents of your id_rsa_deploy.pub to the remote servers ~/.ssh/authorized_keys file and similarly store the contents of your id_rsa_deploy as the GitHub secret.   

The post-deploy Shell Script

Now that Github is zipping your project and sending it to the production server, we need to make sure the code ends up in your servers document root. 

#!/bin/sh

# create release folder
datestring=$(date +"%Y-%m-%d-%H-%M")
mkdir -p /var/www/html/releases/$datestring

# unzip to release
unzip -o -qq /var/www/html/latest.zip -d /var/www/html/releases/$datestring 


# symlink assets and user uploads into repo rm -rf /var/www/html/releases/$datestring/public/assets ln -s /var/www/html/shared/assets /var/www/html/releases/$datestring/public/assets

# symlink .env rm /var/www/html/releases/$datestring/.env ln -s /var/www/html/shared/.env /var/www/html/releases/$datestring/.env # symlink the new release rm /var/www/html/current ln -s /var/www/html/releases/$datestring /var/www/html/current

# set permissions

chown -R www-data:www-data /var/www/html/current

chmod -R 0755 /var/www/html/current

# finally refresh nginx service nginx reload

As you can see we use a couple of folders here:

/var/www/html/releases where every deploy is extracted to,

/var/www/html/shared which is used to store user uploads, assets and a .env configuration which holds credentials.

/var/www/html/current holds the current (latest) release, so our servers document root should be something like /var/www/html/current/public.

Depending on your setup and CMS you might need to adjust things here and there, but I think this a good setup to get you up and running.

After about half a year of using this technique, I have to say that this way of doing continuous code deploys with GitHub Actions runs smooth and stable even in more complex setups where you rsync/lsyncd complete folder structures between servers.