Deploy Jekyll blog with Github Actions

Github Actions is a dev/ops automation service that has recently became generally available to public after quite a long beta period. Additionally, the workflow files that define actions underwent a major change from HCL format to the more widely adopted YAML, which I'd argue is better for readability. I have used Actions for various tasks while in beta for things like building Go binaries and find it pretty useful since there's no need to involve a thid party service, the only thing you really need is Docker.

Today i'm going through a pretty basic build and deployment pipeline for a Jekyll blog. In fact, this very blog is powered by Jekyll and I've been pretty happy with the setup over the years after migrating from Wordpress. There is nothing exotic about the pipeline and I'm sure a lot of people are aware of services like Netlify (i'm a customer too!) that automate static site deployments from git providers like Github or Gitlab.

The way I publish my blog posts is pretty simple: create a post file, get the article going, fix typos and then finally preview the page before I decide to roll it out. The blog is hosted on a cheap Digital Ocean instance which I happen to use for other things. Usually I don't remember which commands are needed run to deploy so i created a simple rake task, but the meat of it is:

desc "Clean site"
task :clean do
  sh "rm -rf ./_site"
end

desc "Build site"
task build: :clean do
  sh "bundle exec jekyll build"
end

desc "Deploy site"
task :deploy do
  exec "rsync -rtzh -e 'ssh -p PORT' --progress --delete _site/ USER@sosedoff.com:PATh"
end

Now, let's implement the Github Actions workflow. First, create a new directory in your blog repository .github. We plan on running automatic deploys on every push to master branch. We would also like to receive a Slack message when deploy is complete. The workflow file should look like this:

name: deploy

on:
  push:
    # Action run will only be triggered on updates to master branch
    branches:
      - master

jobs:
  run-deploy:
    runs-on: ubuntu-latest
    steps:
      # Check out git repository
      - name: checkout
        uses: actions/checkout@master

      # Install dependencies
      - name: dependencies
        uses: docker://ruby:2.5
        env:
          BUNDLE_PATH: .bundle
        with:
          entrypoint: bundle
          args: install -j=4

      # Build Jekyll site
      - name: build
        uses: docker://ruby:2.5
        env:
          BUNDLE_PATH: .bundle
          LANG: en_US.UTF-8
          LANGUAGE: en_US.UTF-8
          LC_ALL: C.UTF-8
        with:
          entrypoint: bundle
          args: exec rake build

      # Publish blog to DigitalOcean
      - name: publish
        uses: ./.github/rsync
        env:
          JEKYLL_DEPLOY_KEY: ${{ secrets.JEKYLL_DEPLOY_KEY }}
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
          DEPLOY_DIR: ${{ secrets.DEPLOY_DIR }}

      # Send me a slack notification
      - name: notify
        uses: ./.github/slack
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        with:
          args: Blog has been deployed

Jekyll steps are executed using the offical ruby 2.5 image, this is mostly done since I want to be able to customize any command without forking the step. As for the publish step - it grabs the _site directory produced by the build step and uploads it to the server using rsync, a tool to synchronize files between machines. There'a a custom action for this step, create a directory .github/rsync with the following files.

Dockerfile:

FROM alpine:3.6

RUN \
  apk update && \
  apk add --no-cache rsync openssh-client && \
  rm -rf /var/cache/apk/*

ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh:

#!/bin/sh

# Save private ssh key from env var
echo $JEKYLL_DEPLOY_KEY | base64 -d > /key
chmod 0600 /key

rsync \
  -e "ssh -i /key -o StrictHostKeyChecking=no -p $DEPLOY_PORT" \
  -rtzh \
  _site/ \
  --progress \
  $DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_DIR

Slack notification action is pretty small, go ahead and copy .github/rsync/Dockerfile into .github/slack/Dockerfile and add a new entrypoint.sh file:

#!/bin/bash
curl -X POST $SLACK_WEBHOOK -d "{\"text\": \"$*\"}"

Majority of the configuration is done via environment variables (like JEKYLL_DEPLOY_KEY). They are set in Github settings interface below:

Once everything is setup go ahead and commit changes to the workflow files and do a git push to master branch (or whatever branch you specify in workflow file). Successful result will look like the screenshot below:

And that's it! Of course I could have created an Action specifically tailored for Jekyll deployments (Action Marketplace might have some already) but this post illustrates that creating your own actions and customizing them with exactly what you need is not hard or complicated at all. The setup described in this blog post could easily be tweaked to support other static website generator tools like Middleman or Gastby.

UPDATE: I went ahead and refactored out all the code and merged everything into a single docker image. Check out source on Github