Dead simple cross-platform GitHub Actions for Haskell

May 7, 2020

I have been looking for the perfect Continuous Integration (CI) for my Haskell projects for a while and found Github Actions to be absolute gold for that. I have been going back and forth between different CI platforms (Travis, AppVeyor, CircleCI), until recently I finally figured out the settings that include everything I need.

So, in this short blog post, I would like to present the easiest way to set up the GitHub Actions CI for Haskell projects I come up with.

Motivation

In one of my previous blog posts I’ve described Dead simple Travis CI settings for Haskell projects. There I’ve also explained why it’s important to support both build tools and multiple GHC versions on CI. However, it’s tricky to define Travis CI config with a matrix for multiple GHC versions, multiple operating systems and multiple build tools at the same time. This is where GitHub Actions come to play!

The resulting GitHub Actions configuration has the following features:

  • It is fast
  • It is short
  • Works on Linux, macOS and Windows
  • Supports multiple GHC versions
  • Builds your project with both cabal and stack
  • Contains only single copy-pasteable file: the file doesn’t have any project-specific properties or variables
  • Can be enabled automatically: no need to visit some third-party site to start CI, just push a single file to your repo
  • It is free for open-source projects

The Config

To not beat around the bush and just get to the point, below is the full config:

name: CI

# Trigger the workflow on push or pull request, but only for the master branch
on:
  pull_request:
  push:
    branches: [master]

jobs:
  cabal:
    name: ${{ matrix.os }} / ghc ${{ matrix.ghc }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macOS-latest, windows-latest]
        cabal: ["3.2"]
        ghc:
          - "8.6.5"
          - "8.8.3"
          - "8.10.1"
        exclude:
          - os: macOS-latest
            ghc: 8.8.3
          - os: macOS-latest
            ghc: 8.6.5
          - os: windows-latest
            ghc: 8.8.3
          - os: windows-latest
            ghc: 8.6.5

    steps:
    - uses: actions/checkout@v2
      if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'

    - uses: actions/setup-haskell@v1.1
      id: setup-haskell-cabal
      name: Setup Haskell
      with:
        ghc-version: ${{ matrix.ghc }}
        cabal-version: ${{ matrix.cabal }}

    - uses: actions/cache@v1
      name: Cache cabal-store
      with:
        path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }}
        key: ${{ runner.os }}-${{ matrix.ghc }}-cabal

    - name: Build
      run: |
        cabal update
        cabal build all --enable-tests --enable-benchmarks --write-ghc-environment-files=always

    - name: Test
      run: |
        cabal test all --enable-tests

  stack:
    name: stack / ghc ${{ matrix.ghc }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        stack: ["2.1.3"]
        ghc: ["8.8.3"]

    steps:
    - uses: actions/checkout@v2
      if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.ref == 'refs/heads/master'

    - uses: actions/setup-haskell@v1.1
      name: Setup Haskell Stack
      with:
        ghc-version: ${{ matrix.ghc }}
        stack-version: ${{ matrix.stack }}

    - uses: actions/cache@v1
      name: Cache ~/.stack
      with:
        path: ~/.stack
        key: ${{ runner.os }}-${{ matrix.ghc }}-stack

    - name: Build
      run: |
        stack build --system-ghc --test --bench --no-run-tests --no-run-benchmarks

    - name: Test
      run: |
        stack test --system-ghc

Enabling such CI for your projects is completely effortless. All you need to do is create a file named .github/workflows/ci.yml with the above content and push it to your repository. That’s all! GitHub takes care of everything else for you.

Once all build errors are fixed, you can enjoy your well-deserved green CI 💚

All CI checks pass

Badge

After you enable GitHub Actions CI for your project, you can also add a cute badge to your README.md. Copy the following line and replace userName and repoName with your own settings:

[![GitHub CI](https://github.com/userName/repoName/workflows/CI/badge.svg)](https://github.com/userName/repoName/actions)

And it will look like this:

GitHub CI

Configuration explanation

If you want to understand why the configuration looks like it looks, below is a short explanation:

  1. The configuration is powered by two GitHub Actions: setup-haskell and cache. The setup-haskell action is responsible for installing GHC, cabal and stack on different operating systems and providing some convenient utilities. The cache action is responsible for caching your built artefacts as you might guess.
  2. It builds your project using cabal with different GHC versions on Ubuntu. The GitHub Actions virtual environment comes with GHC, Cabal and Stack already pre-installed in there, so the CI is faster on Linux than on Windows or macOS at the moment.
  3. Your project builds on macOS and Windows only using the latest GHC version. It usually won’t give you a lot to build with multiple GHC versions on all three platforms. So instead of having a N x M matrix, it’s enough to have a N + M - 1 matrix. Though, you can easily change this behaviour by removing relevant exclude sections.
  4. The CI also configures the testing environment properly and runs your tests automatically with each build.
  5. The config is extensible and easily customizable. You can change step names and their commands, add new steps. You can even add releases in an easy way with such a system!