A simple setup for a Build and Deploy system using GitHub Actions Dec 12 2020

I've been using GitHub Actions on a few projects now, and I find them like the future of what bash scripting was back in the day. I feel I can do pretty much everything in a quick and concise way. In this post, I'll show you the setup I use for a simple Build and Deploy GitHub Action.

This post is not an in-depth view on GitHub Actions, but let's review some basic concepts, so we know what we are talking about.

GitHub Actions

If you haven't used GitHub Actions before, they are small workflows you can define for your repository that can be triggered by different events. The following types of events can trigger the workflows:

If you want to learn more about it, check the events documentation.

Now we need to define where our workflow will run, in GitHub lingo, they are called Runners. You can define your own runners, but we'll use GitHub-hosted runners, the limits are enough for small projects.

To define the runner's action and type, we use a .yml file, which, if you are familiar with Infrastructure as Code, you'll feel at home. This .yml file is known as a metadata file.

We have three types of Actions:

You can learn more about defining the metadata file by reading the metadata documentation.

OK, that should be enough theory to get us started.

The build and deploy architecture.

Let's define what we want to accomplish. We want to be able to build our final product using the GitHub-Hosted runner, and when it is ready, we want it to push it to a server via SSH automatically. This simple schema can be used for static sites, react applications, etcetera. Anything really, we can use this schema for anything that can be built, packaged, and published somewhere else.

This is what our action will do in broad strokes:

  1. Copy our repository code to the runner.
  2. Build the project.
  3. Copy the built product to the public server.

We are going to use rsync(1) to copy the built product to the public server. That means that we need to create an ssh key-pair and add our public key to the ~/.ssh/authorized_keys file on the public server. If you are unfamiliar with the process, read my post Understanding SSH Keys and using Keychain to manage passphrase on macOS, especially the section called "Key pairs".

Because we don't want to hard-code our private information on the metadata file we are going to make use of GitHub Secrets. Of course, you have to be careful who can connect to your server, and which privileges they have. I would suggest removing all privileges not related to putting the files in a specific directory. This way, we try to minimise the damage someone can do if they login with that user to the server.

That is the general gist of our build and deploy architecture. Let's start implementing it.

Using GitHub Secrets for private information

If you are following along, now is the time where we would create our SSH key-pair if you don't have the keys already. I will use the following command to generate the key-pair:

1
$  ssh-keygen -t rsa -b 4096 -C " key for GitHubAction" -f ~/.ssh/id_github

The command will generate two files, the private key (id_github) and the public key (id_github.pub). Copy the content of the public key and copy it to your public server's ~/.ssh/authorized_keys file. If you are in macOS, you can use the following command to copy the public key to the clipboard:

1
$ cat id_github.pub | pbcopy

Then connect via ssh to the server and add it to your ~/.ssh/authorized_keys file. If the file doesn't exist, create it. You could also use ssh-copy-id(1) command:

1
$ ssh-copy-id -i id_github user@YOURSERVER.com

Whichever method you chose make sure you can log in via ssh with the private key.

Create the following secrets in the GitHub repository. You can do this by going to the Secret section inside the repository Settings:

The DEV_ENV_FILE is only required if you want to include it on your public server, I'll use a React app as a demo, but you can modify it to suit your needs.

Creating the metadata file

GitHub repositories use the .github directory in the root of your repository to keep track of files that relate specifically to GitHub. For example, if you add the file pull_request_template.md to your .github directory, that file will be used as a template for your pull requests.

Create .github folder in your repository if you don't have it already, and create the directory workflows inside it. Inside the workflows directory, we'll store our actions metadata files. I'll name my file deploy-to-server.yml (you can give it another name if you want), and add the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: "Deploy to Server"

on:
  workflow_dispatch:
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@master
      - name: "Build deployment code for standalone"
        env:
          remote_host: ${{ secrets.DEPLOY_HOST }}
          remote_port: ${{ secrets.DEPLOY_PORT }}
          remote_user: ${{ secrets.DEPLOY_USER }}
          remote_key: ${{ secrets.DEPLOY_KEY }}
          dev_env_file: ${{ secrets.DEV_ENV_FILE }}
          local_dir: "build/"
          remote_dir: "/var/html"
        run: |
          npm install
          CI=false npm run build
          mkdir ~/.ssh
          echo "$remote_key" > ~/.ssh/id_github
          chmod 600 ~/.ssh/id_github
          echo "$dev_env_file" > .env
          chmod 600 .env
          rsync -avzr --delete -e "ssh -p ${remote_port} -i ~/.ssh/id_github -o StrictHostKeyChecking=no" ${local_dir} ${remote_user}@${remote_host}:${remote_dir}
          rm ~/.ssh/id_github

The on: section specifies which events will trigger this action. In my case, I decided to use workflow_dispatch, so I can run the action manually from the Actions option in the GitHub web interface of the repository.

The Action will run on the GitHub-Hosted runner based on ubuntu-latest. It includes the rsync(1) command. The action also uses a predefined action checkout that will get our code from the master branch into the runner. You can learn more about it from the GitHub Checkout repository, and about other actions on the GitHub Toolkit repository.

We define environment variables extracting the information from the GitHub secrets and finally run the series of commands to compile the code, set up the SSH keys, and send the files to the server using rsync.

Running the Action

Once you have the files ready your repository should have something similar to the following structure for the .github directory:

1
2
3
.github/
└── workflows
    └── deploy-to-server.yml

You can now commit the code and push it to GitHub. Once in your GitHub repository, you can run the action manually by going to the Actions section on GitHub's web interface. Under Workflows you'll see the Deploy To Server workflow, select it and you'll be able to click Run workflow. Run it, and if everything worked fine, you'd see a green tick next to the workflow.

And that's it, congratulations on your simple build and deploy pipeline.

Final Thoughts

I hope you can see how this approach could easily be adapted to any other build and deploy systems. As I mentioned before, it feels like GitHub Actions are the bash scripts of the CI/CD pipeline options. GitHub actions are easy to set up, and you don't need to set up a Jenkins server for such a simple build and deploy pipeline.

Just remember to keep your private key secure and also to limit the commands the user can run when it logs in. In the end, you are setting up the credentials that will allow someone to connect to your server using ssh.

I hope you find the post useful.

Related topics/notes of interest


** If you want to check what else I'm currently doing be sure to follow me on twitter @rderik or subscribe to the newsletter. If you want to send me a direct message you can send it to derik@rderik.com.