Deploying a Static Website with Jekyll and GitHub Actions

This website is served using Jekyll, a static site generator, and hosted directly from a GitHub repository using Pages. While I won’t really get into any Jekyll details, I will describe how the site is built and deployed using custom GitHub Actions.

GitHub Pages

While Pages can handle building the static site itself, my process is made more complicated by the fact that I use a few Jekyll plugins that are not included in the standard process. For the longest time I had been building the static site locally and storing only the final, static html in the repository—the static html could then be trivially deployed by Pages. While this works, it makes keeping the source site and the static site in sync an annoying process. To combat this I actually maintained two repositories for each, however inevitably I would forget to push one of them (and even worse often follow that up by making further changes on an outdated copy on another machine).

Luckily, deployment to Pages can now be done using GitHub Actions which allows the build process to be customized. While this deployment process is currently in Beta, it’s been rock solid for me so far. This means I can instead maintain a single repository for the source site and build and deploy the static site whenever I push to GitHub.

Specifying Jekyll Dependencies

First I’ll assume we are in a directory that has been initialized with a Jekyll site (for more information see the Jekyll Quickstart). We can then create a file Gemfile at the base directory specifying the necessary gems (i.e. packages) to build the site. For more information see Jekyll’s deployment documentation, however mine looks something like:

source "https://rubygems.org"
gem "jekyll"

group :jekyll_plugins do
  gem "jekyll-scholar"
  gem "jekyll-sitemap"
  # ... any other jekyll plugins.
end

Here any gems specified in the :jekyll_plugins group will be automatically included by Jekyll when it builds the site and as a result they do not need to be specified elsewhere in the configuration. Next we can install and record these gems by running

bundle install
bundle lock --add-platform=x86_64-linux

This will ensure that all of the relevant gems are installed as well as creating Gemfile.lock which records the name and version of each gem (including their dependencies). The second of these two commands will also include any platform-specific gems for x86_64-linux which is what we will use when building on GitHub. While not necessary now, running bundle update will update the installed gems and record them in the lockfile.

Creating the Deployment Action

We can add a deployment action by creating .github/workflows/build-and-deploy.yml (the name of the file is arbitrary) with the following contents:

name: Build and deploy

on:
  push:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 'ruby'
          bundler-cache: true

      - name: Build site
        run: bundle exec jekyll build

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v2

  deploy:
    needs: build

    permissions:
      pages: write
      id-token: write

    environment:
      name: github-pages
      url: $

    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Pages
        uses: actions/deploy-pages@v2

This defines two jobs, build and deploy, which run in sequence on every push to the master branch. One of the keys to the build job is that while it installs ruby and all the packages specified in the lockfile, the bundler-cache line ensures that the gems are cached between runs. This dramatically speeds up the runtime of the job. The final step of the build job creates an intermediate artifact that will be used by the deploy job to upload the site, albeit only after the build job finishes successfully.

Finishing up

If we ignore all the other Jekyll-specific files, we should now have a directory with the following structure:

.github/workflows/build-and-deploy.yml
Gemfile
Gemfile.lock
...

All that’s left to do is create the GitHub repository, e.g. https://github.com/USER/REPO, and enable GitHub Actions as the build and deployment source. This can be done by updating the settings at https://github.com/USER/REPO/settings/pages. With that in place, and assuming the above files are checked into the repository, the next push should build and deploy the site to https://USER.github.io/REPO

Note: by naming the repository USER.github.io this creates a user site which will deploy to https://USER.github.io.