A collection of GitHub Actions designed to reduce repetition across projects.
Builds and collects the binaries for all provided cabal targets. Each target
must resolve to a single binary with cabal list-bin
(so e.g. all
is not
a valid target). Because we use cabal list-bin
, this requires the use of
cabal
3.4.0.0 or later, although it is recommended to use cabal-3.8.1.0
or later to make sure that a fix for
this list-bin
issue
is included.
Inputs:
targets
: Space or newline-delimited string of cabal targets.dest
: Location to which the built binaries are copied. It is created if it does not already exist.
Outputs:
targets-json
: JSON array of the input targets, useful as an input into workflow job matrices for mapping target binaries to separate jobs.
Workflow Example:
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
outputs:
bins-json: ${{ steps.bins.outputs.targets-json }}
steps:
- uses: actions/checkout@v4
- uses: haskell-actions/setup@v2
- uses: GaloisInc/.github/actions/cabal-collect-bins@v1.1.2
id: bins
with:
targets: |
saw
cryptol
dest: dist/bin
- uses: actions/upload-artifact@v4
with:
name: dist-bins
path: dist
run:
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
bin: ${{ fromJson(needs.build.outputs.bins-json) }}
steps:
- uses: actions/download-artifact@v4
with:
name: dist-bins
path: dist
- run: |
chmod +x dist/bin/*
dist/bin/${{ matrix.bin }} --version
Build and test single-package Haskell projects using Cabal. Performs a sequence of steps that are appropriate for many projects, namely:
- Uses
haskell-actions/setup
to install Cabal and GHC - Runs
cabal sdist
, unpack thesdist
, and perform subsequent steps there - Uses
actions/cache
to cache the Cabal store and build directory (dist-newstyle
) - Runs
cabal configure
,build
,test
,sdist
,check
, andhaddock
- Unconditionally saves the cache
- Checks that the package is compatible with GHC's bundled version of Cabal
This workflow will not be applicable to all projects, but it is applicable to enough to be useful.
For a description of the inputs, see the workflow itself.
This workflow shares some goals with the haskell-ci tool. Here are a few points of comparison:
-
The haskell-ci tool generates a standalone workflow. This means that it will generally produce noisier diffs when updating, as it may require recreating large chunks of autogenerated YAML.
-
The haskell-ci tool has a few extra features, such as integrations with
hlint
anddocspec
. We recommend running linters in a separate job, so that they can run in parallel with the build and test. -
This workflow can be updated by Dependabot.
-
This workflow provides extensive documentation and commentary, both user- facing and in the implementation.
-
As a reusable workflow, this workflow is used in the
uses
part of a job. So:- The calling workflow is free to customize other fields such as
the workflow-level
on:
field.1 - Other jobs may run as part of the same calling workflow, and in particular,
can set
inputs
.
- The calling workflow is free to customize other fields such as
the workflow-level
This workflow aims to support:
- The latest two major/LTS versions of each macOS, Ubuntu, and Windows image supported by the hosted GitHub Actions runners
- As many GHC versions as is feasible without elaborate workarounds
(in particular, we are limited to the GHC versions supported by
haskell-actions/setup
, which is itself limited to those supported byghcup
) - The latest two versions of Cabal
Support for a wide range of Cabal versions is not a priority because this workflow contains an explicit step to check a package's compatibility against the version of the Cabal library shipped with GHC. This ensures that the package is compatible with older versions of Cabal without having to use them throughout the whole workflow.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
ci:
name: ${{ matrix.os }} GHC-${{ matrix.ghc }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-24.04]
ghc: [9.8.4, 9.10.1, 9.12.1]
fail-fast: false
uses:
haskell-ci:
uses: GaloisInc/.github/workflows/haskell-ci@v1
inputs:
ghc: ${{ matrix.ghc }}
os: ${{ matrix.os }}
Libraries that are published to Hackage must provide bounds on every
dependency in Cabal's build-depends
field. Unfortunately, it
is very hard to get these bounds right. The following jobs help check
(but do not exhaustively verify or enforce) that such bounds are accurate, and
are recommended for library packages that will be published to Hackage.
# ... same as above ...
jobs:
ci:
name: ${{ matrix.os }} ghc-${{ matrix.ghc }} ${{ matrix.cache-key-prefix }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
cache-key-prefix: [""] # see `include` below
configure-flags: [""] # see `include` below
ghc: [9.8.4, 9.10.1, 9.12.2]
haddock: [true] # see `include` below
os: [ubuntu-24.04]
pre-hook: [""] # see `include` below
include:
# Include a single build (with latest Linux OS and oldest GHC version)
# using `--prefer-oldest` to verify that we still build against the
# lower bounds in the Cabal `build-depends`.
- cache-key-prefix: prefer-oldest
configure-flags: --prefer-oldest
ghc: 9.4.8
haddock: false
os: ubuntu-24.04
# Include a single build (with latest Linux OS and latest GHC version)
# using `cabal-force-upper-bounds` to verify that we actually build
# against the upper bounds in the Cabal `build-depends`.
- cache-key-prefix: upper-bounds
ghc: 9.12.2
haddock: false
os: ubuntu-24.04
pre-hook: |
curl \
--fail \
--location \
--proto '=https' \
--show-error \
--silent \
--tlsv1.2 \
https://github.com/nomeata/cabal-force-upper-bound/releases/download/0.1/cabal-force-upper-bound.linux.gz | \
gunzip > \
/usr/local/bin/cabal-force-upper-bound
chmod +x /usr/local/bin/cabal-force-upper-bound
echo 'packages: .' > cabal.project
cabal-force-upper-bound --cabal-project *.cabal >> cabal.project
# Include a build (with latest Linux OS) against Stackage LTS package
# sets for each supported version of GHC that has one to verify that we
# support building against these widely-used package sets.
- cache-key-prefix: stackage
ghc: 9.8.4
haddock: false
os: ubuntu-24.04
pre-hook: |
echo 'packages: .' > cabal.project
echo 'import: https://www.stackage.org/lts-23.19/cabal.config' >> cabal.project
- cache-key-prefix: stackage
ghc: 9.4.8
haddock: false
os: ubuntu-24.04
pre-hook: |
echo 'packages: .' > cabal.project
echo 'import: https://www.stackage.org/lts-21.25/cabal.config' >> cabal.project
uses:
haskell-ci:
uses: GaloisInc/.github/workflows/haskell-ci@v1
inputs:
cache-key-prefix: ${{ matrix.cache-key-prefix }}
# See Note [Parallelism] in `haskell-ci.yml` for why `--ghc-options='-j'`
# and `--semaphore`.
configure-flags: --enable-tests --ghc-options='-j' --semaphore ${{ matrix.configure-flags }}
ghc: ${{ matrix.ghc }}
haddock: ${{ matrix.haddock }}
os: ${{ matrix.os }}
pre-hook: ${{ matrix.pre-hook }}
Footnotes
-
As an example of why such customization is important, it appears that the haskell-ci tool hardcodes
on:
to trigger on pushes and pull requests into the main branch. Not only may this not be appropriate for all projects (consider, for example, projects that use pull requests into a separatedev
branch), it causes double-builds for pull requests targeting the main branch from a branch in the same repo. ↩