DIY bare bones CI server with Bash and Nix

Posted on November 12, 2020

With a server with Nix installed (no need for NixOS), you can leverage its build isolation for running CI jobs by adding a post-receive Git hook to the server.

In most of my project I like to keep a test attribute which runs the test with nix-build -A test. This way, a post-receive hook could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash
set -Eeuo pipefail
set -x

LOGS_DIR="/data/static/ci-logs/libedn"
mkdir -p "$LOGS_DIR"
LOGFILE="${LOGS_DIR}/$(date -Is)-$(git rev-parse master).log"
exec &> >(tee -a "${LOGFILE}")

unset GIT_DIR
CLONE="$(mktemp -d)"
git clone . "$CLONE"
pushd "$CLONE"

finish() {
  printf "\n\n>>> exit status was %s\n" "$?"
}
trap finish EXIT

nix-build -A test

We initially (lines #5 to #8) create a log file, named after when the run is running and for which commit it is running for. The exec and tee combo allows the output of the script to go both to stdout and the log file. This makes the logs output show up when you do a git push.

Lines #10 to #13 create a fresh clone of the repository and line #20 runs the test command.

After using a similar post-receive hook for a while, I now even generate a simple HTML file to make the logs available through the browser.

Upsides

No vendor lock-in, as all you need is a server with Nix installed.

And if you pin the Nixpkgs version you’re using, this very simple setup yields extremely sandboxed runs on a very hermetic environment.

Downsides

Besides the many missing shiny features of this very simplistic CI, nix-build can be very resource intensive. Specifically, it consumes too much memory. So if it has to download too many things, or the build closure gets too big, the server might very well run out of memory.