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:
- Building a simple project including caching concerns
- Automated releases of libraries or executables
- 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:
~/.stack
./.stack-work
-
${path/to/package}/.stack-work
(if in a multi-package project)
All of these directories should be cached, and caches should be keyed such that:
- Source changes save new caches
- Cache misses fall back to the same set of dependencies, then
- 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:
- Locate project directories by looking for
.cabal
files, - Cache appropriate
.stack-work
s, and - 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.