Haskell on Actions

by @pbrisbin on May 18, 2021

At Freckle, we’ve been migrating to GitHub Actions and are really happy with the results so far. GitHub’s done a great job surveying the current CI/CD landscape and building on all the best ideas. The availability of Actions to do common steps, the low friction in writing custom Actions when needed, and the tight integration for adding CI/CD to a repository already on GitHub all make for a great experience.

This is the first post in a series about our Haskell projects on GitHub Actions:

  1. Building a simple project including caching concerns :point_left:
  2. Automated releases of libraries or executables
  3. Docker-based deployments from Actions

It’s early days, so all of this is subject to change, but here goes.

Getting Started

For our open source projects, we set up a “CI” Workflow to trigger for all PR events plus pushes to our default branch:

.github/workflows/ci.yml:

name: CI

on:
  pull_request:
  push:
    branches: main

We run a simple Linux Job that begins by checking out the code:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

Caching

Stack-based Haskell projects can cache in a number of places:

All of these directories should be cached, and caches should be keyed such that:

  1. Source changes save new caches
  2. Cache misses fall back to the same set of dependencies, then
  3. The same Stackage resolver

Accomplishing this by crafting an actions/cache stanza by hand is doable (with caveats), but tedious and error-prone. Therefore, we’ve created our own Action that handles it all:

- uses: freckle/stack-cache-action@main

This will:

  1. Locate project directories by looking for .cabal files,
  2. Cache appropriate .stack-works, and
  3. Key them by stack.yaml.lock and the .cabal files themselves

This is all required to achieve complete caches and proper fall-back behavior.

This Action also hashes all git-tracked files for the final component of the cache key, to ensure a new version is uploaded if source files change (but dependencies have not).

Build, test, lint

Again, we have our own Action that compiles, runs tests, and lints with HLint and Weeder:

- uses: freckle/stack-action@main

As we migrate more projects to Actions, this will certainly grow support for real-world needs like customized parallelism or JUnit-style failure reports. For now, options are available to pass extra arguments to stack invocations, work in a specific directory, or disable either of the linters.

- uses: freckle/stack-action@main
  with:
    stack-arguments: --flag my-package:my-flag
    weeder: false

Multi-GHC

To test on multiple versions of GHC, we use dedicated stack.yaml files for each case:

  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        stack-yaml:
          - stack.yaml
          - stack-lts-17.4.yaml
          - stack-lts-16.10.yaml
          - stack-lts-13.2.yaml
      fail-fast: false

    steps:
      - uses: actions/checkout@v2
      - uses: freckle/stack-cache-action@main
        with:
          stack-yaml: ${{ matrix.stack-yaml }}
      - uses: freckle/stack-action@main
        with:
          stack-yaml: ${{ matrix.stack-yaml }}

That’s it for now. Keep an eye out for our future posts, where we talk about automated releases and Docker-based deployments.