Automate blog deployment via Github Actions

As my blog is mostly a static website running via Hugo, I always thought that bothering with setting some automated workflow to deploy it is a rather pointless task.

As I wanted to have an automated deployment via external Github agent, I had to provide it access to my server. For this purpose I created a separate account, which only has access to add/edit content within my nginx blog directory.

Afterwards it was time for proper authorization method - providing new SSH keys via ssh-keygen. For the purpose of this blog post let’s say I used the default values for everything - id_rsa.pub public key and id_rsa private key located in my ~/.ssh directory. As this key was supposed to access the server on which the blog is hosted, the public key was added to ~/.ssh/authorized_keys on the newly created user account.

Then it was time to provide proper configuration to GitHub. As I planned to use shimataro/ssh-key-action action for SSH configuration, there were a few steps I had to do:

  1. Add public key to Deploy keys settings in a project
  2. Add private key to Secrets settings in a project
  3. Add known_hosts content to Secrets settings in a project
  4. Add config content to Secrets settings in a project

While the first two are pretty obvious for anyone who has used SSH, known_hosts and config may be a little surprising.

known_hosts is responsible for keeping a list of all hosts that you agreed to access. If you go to your ~/.ssh/ directory on any machine where you used SSH to connect to other hosts (e.g. for git), you will find that file there. It contains information about all machines to which you connected via SSH. If there is no entry for a host when you are connecting to a new host, SSH will ask you if you are sure you want to connect to it. For the GitHub Actions process this should be automated, so we should not get any prompts during the workflow. That’s why the part of my known_hosts that has info about my deployment server has been added as another config option to my Secrets.

Last but not least, config file. config in SSH is pretty handy feature which can be used i.e. for preparing pre-set environments. For example, instead of typing ssh username@hostname.example.com -p 3999 each time you want to connect to the server, I can provide following in config file:

Host myhost
        HostName hostname.example.com
        Port 3999
        User username

And just use ssh myhost to connect to this server. This makes the SSH connection part in the definition file much simpler, and helps hide the connection details from people who can read the repository code.

After preparation, this part is pretty simple. My whole workflow for deployment was always to connect via SSH to the server, git pull the latest changes from the repo and run the hugo command, which already has the proper publish directory pointed to in the Hugo config.toml file.

So basically all I had to do was access the server, get to the repository directory (let’s call it ~/blog/), run git pull and follow it with the hugo command. I can even chain it all into a single command like this:

ssh myhost 'cd blog && git pull && hugo'

So now, moving it into a proper Github action pipeline, we receive something like this:

name: Blog CI
on:
  push:
    branches: [ "master" ]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Install SSH Key
        uses: shimataro/ssh-key-action@v2.3.1
        with:
          key: ${{ secrets.PRIVATE_KEY }}  
          config: ${{ secrets.CONFIG }}
          known_hosts: ${{ secrets.KNOWN_HOSTS }}
      
      - name: Refresh content
        run: ssh myhost 'cd blog && git pull && hugo'

And just like that, after each push to the master branch, the latest content is almost immediately available on this blog.

With this post I am almost able to achieve my one-per-year trend on new posts. Hopefully I will be able to write more of them in the upcoming time, as I recently realized that this blog mostly helps me recall all the tricks from my toolset, which after some time are forgotten. Or, as it was the case a few weeks ago — my carefully prepared snippets were left within repositories of a company that I had left, and suddenly I lost access to them. So for this purpose, I think it is better to write them down in a place such as this. If this comes in handy for other people in the future — good for you! Starting from now, I am treating this blog mostly as my private notebook for all sneaky little tricks. Expect more of them soon.