Deploying with GitHub Actions and more Nix

David Wagner

In July I described how I use Travis CI to deploy this static site to GitHub Pages using a Nix pipeline. Before continuing I suggest reading that article because rest of this post builds on top of that.

This article is about the changes I made in this blog’s deployment process during the last months. These include switching to Nix Flakes, adding more checks to the pipeline and moving from Travis CI to GitHub Actions.

Flakes

Flakes are an experimental mechanism to package Nix expressions into composable entities. Flakes define a standard structure of Nix projects for hermetic and reproducible evaluation.

The blog is still built using Nix, but now the entry point is flake.nix. The heart of this expression is a function:

outputs = { self, inputs.. }:
{
  checks = ..
  modules = ..
  packages = ..
  ..
}

The function outputs takes the project’s external dependencies as inputs and returns the build artifacts in a record. Build artifacts can be Nix packages, NixOS modules, test results, container images, virtual machine images. Basically anything Nix can build. We’ll see a concrete example of the checks attribute in subsequent section. There’s an ongoing effort to define a standard structure for the returned value so that specific tools can understand and use the built derivations.

The source tree also contains a lock file flake.lock to ensure that pages of this blog are always built with exactly the same set of tools independently where the build is executed. The same environment is used on my workstation, on yours, on a worker of a hosted CI system.

To learn more about Flakes I recommend the following resources:

  • Presentation by Eelco Dolstra at NixCon 2019 (Youtube)
  • Three-part series on Tweag’s technical blog (part 1, part 2, part 3)
  • RFC documenting the flake’s structure (link)

Compatibility

Currently flakes are unstable and experimental in Nix. You need to explicitly enable flake support if you want to use them. The repository flake-compat provides a compatibility function to allow flakes to be used by non-flake-enabled Nix versions.

If you install the Nix package manager on your platform and clone the repository you can build the static pages of this site by running nix-build in the source tree.

With a flake-enabled, experimental Nix version you can even build the project without cloning, directly referencing the repository on GitHub:

nix build github:wagdav/thewagner.net

Building the static pages of this blog has no practical use for anybody but me. But imagine if your favorite project would build on your machine without installing anything but one standalone binary?

Language-specific package managers such as cargo, go get, npm, pip work well if your project uses only that specific language. The reality is that even the simplest projects, such as the source code of this blog, may require tools from any language ecosystems.

Checks

The checks attribute of the structure returned by the flake’s outputs function describes self-tests. For this blog’s source the checks look like this:

outputs = { self, nixpkgs }:
  checks.x86_64-linux = {

    build = self.defaultPackage.x86_64-linux;

    shellcheck = pkgs.runCommand "shellcheck" { .. }

    markdownlint = pkgs.runCommand "mdl" { .. }

    yamllint = pkgs.runCommand "yamllint" { .. }
  };
};

The checks are grouped per supported platform, in this case there’s only one: x86_64-linux. For this blog’s source checking means:

  • Build the static the blog’s static HTML files
  • Run shellcheck on all scripts in the source code
  • Run mdl on all markdown files in the source code
  • Run yamllint on all YAML files in the source code

If any of these steps fail, the project is considered broken. You can see the full code here.

If you use the experimental Nix version with flake support you can execute all the checks with the following command:

nix flake check  # using Nix experimental

Or with stable Nix without flake-support:

nix-build -a checks.x86_64-linux  # using Nix stable

Again, running the checks only requires the Nix package manager to be installed.

GitHub Actions

Previously the build and deployment scripts ran on Travis CI. I was curious to see how the deployment would work on GitHub Actions, which has become popular during the past year.

The transition from Travis CI to GitHub Actions was trivial. The workflow definition contains the minimal required boilerplate. 21 lines specify the following steps:

  • Check out the repository
  • Install Nix using Cachix’s install-nix-action
  • Run the checks
  • Deploy the site if on the master branch

The workflow is not concerned with installing or configuring anything but Nix and it’s merely coordinating the build and deploy steps.

Summary

I use a Nix expression to build the static pages of my blog. The build runs locally and on the workers of hosted CI/CD systems such as GitHub Actions and Travis CI. The build requires no external dependencies other than the Nix package manager. The build is reproducible and hermetic: no matter where the project is built, which packages are installed, the result is always the same everywhere.